Revved to chromium 4dfb55c9cf0950b8bac8b10070c9b8f3e7de66c2 refs/remotes/origin/HEAD
diff --git a/build/android/gyp/create_device_library_links.py b/build/android/gyp/create_device_library_links.py
index 30e050c..3e630b6 100755
--- a/build/android/gyp/create_device_library_links.py
+++ b/build/android/gyp/create_device_library_links.py
@@ -64,7 +64,8 @@
   mkdir_cmd = ('if [ ! -e %(dir)s ]; then mkdir -p %(dir)s; fi ' %
       { 'dir': device_dir })
   RunShellCommand(device, mkdir_cmd)
-  device.PushChangedFiles(options.script_host_path, options.script_device_path)
+  device.PushChangedFiles([(options.script_host_path,
+                            options.script_device_path)])
 
   trigger_cmd = (
       'APK_LIBRARIES_DIR=%(apk_libraries_dir)s; '
diff --git a/build/android/gyp/java_cpp_enum.py b/build/android/gyp/java_cpp_enum.py
index ad09742..6a1d5c1 100755
--- a/build/android/gyp/java_cpp_enum.py
+++ b/build/android/gyp/java_cpp_enum.py
@@ -36,22 +36,22 @@
     assert self.entries
 
   def _AssignEntryIndices(self):
-    # Supporting the same set enum value assignments the compiler does is rather
-    # complicated, so we limit ourselves to these cases:
-    # - all the enum constants have values assigned,
-    # - enum constants reference other enum constants or have no value assigned.
-
+    # Enums, if given no value, are given the value of the previous enum + 1.
     if not all(self.entries.values()):
-      index = 0
+      prev_enum_value = -1
       for key, value in self.entries.iteritems():
         if not value:
-          self.entries[key] = index
-          index = index + 1
+          self.entries[key] = prev_enum_value + 1
         elif value in self.entries:
           self.entries[key] = self.entries[value]
         else:
-          raise Exception('You can only reference other enum constants unless '
-                          'you assign values to all of the constants.')
+          try:
+            self.entries[key] = int(value)
+          except ValueError:
+            raise Exception('Could not interpret integer from enum value "%s" '
+                            'for key %s.' % (value, key))
+        prev_enum_value = self.entries[key]
+
 
   def _StripPrefix(self):
     if not self.prefix_to_strip:
@@ -69,7 +69,7 @@
   single_line_comment_re = re.compile(r'\s*//')
   multi_line_comment_start_re = re.compile(r'\s*/\*')
   enum_start_re = re.compile(r'^\s*enum\s+(\w+)\s+{\s*$')
-  enum_line_re = re.compile(r'^\s*(\w+)(\s*\=\s*([^,\n]+))?,?\s*$')
+  enum_line_re = re.compile(r'^\s*(\w+)(\s*\=\s*([^,\n]+))?,?')
   enum_end_re = re.compile(r'^\s*}\s*;\s*$')
   generator_directive_re = re.compile(
       r'^\s*//\s+GENERATED_JAVA_(\w+)\s*:\s*([\.\w]+)$')
diff --git a/build/android/gyp/push_libraries.py b/build/android/gyp/push_libraries.py
index 63421e9..6b31a2e 100755
--- a/build/android/gyp/push_libraries.py
+++ b/build/android/gyp/push_libraries.py
@@ -40,7 +40,7 @@
       if needs_directory:
         device.RunShellCommand('mkdir -p ' + options.device_dir)
         needs_directory[:] = [] # = False
-      device.PushChangedFiles(host_path, device_path)
+      device.PushChangedFiles([(host_path, device_path)])
 
     record_path = '%s.%s.push.md5.stamp' % (host_path, serial_number)
     md5_check.CallAndRecordIfStale(
diff --git a/build/android/gyp/write_build_config.py b/build/android/gyp/write_build_config.py
index ab70a79..722d18c 100755
--- a/build/android/gyp/write_build_config.py
+++ b/build/android/gyp/write_build_config.py
@@ -171,6 +171,9 @@
     config['resources'] = {}
     config['resources']['dependency_zips'] = [
         c['resources_zip'] for c in all_resources_deps]
+    config['resources']['extra_package_names'] = []
+
+  if options.type == 'android_apk':
     config['resources']['extra_package_names'] = [
         c['package_name'] for c in all_resources_deps if 'package_name' in c]
 
diff --git a/build/android/java_cpp_template.gypi b/build/android/java_cpp_template.gypi
index fe4238a..036f32c 100644
--- a/build/android/java_cpp_template.gypi
+++ b/build/android/java_cpp_template.gypi
@@ -22,8 +22,8 @@
 #
 # The 'sources' entry should only list template file. The template file
 # itself should use the 'ClassName.template' format, and will generate
-# 'gen/templates/<package-name>/ClassName.java. The files which template
-# dependents on and typically included by the template should be listed
+# 'gen/templates/<target-name>/<package-name>/ClassName.java. The files which
+# template dependents on and typically included by the template should be listed
 # in template_deps variables. Any change to them will force a rebuild of
 # the template, and hence of any source that depends on it.
 #
@@ -32,7 +32,7 @@
   # Location where all generated Java sources will be placed.
   'variables': {
     'include_path%': '<(DEPTH)',
-    'output_dir': '<(SHARED_INTERMEDIATE_DIR)/templates/<(package_name)',
+    'output_dir': '<(SHARED_INTERMEDIATE_DIR)/templates/<(_target_name)/<(package_name)',
   },
   'direct_dependent_settings': {
     'variables': {
diff --git a/build/android/provision_devices.py b/build/android/provision_devices.py
index 54c90c3..468ef3f 100755
--- a/build/android/provision_devices.py
+++ b/build/android/provision_devices.py
@@ -72,7 +72,7 @@
   logging.info('  Pushing adb_reboot ...')
   adb_reboot = os.path.join(constants.DIR_SOURCE_ROOT,
                             'out/%s/adb_reboot' % target)
-  device.PushChangedFiles(adb_reboot, '/data/local/tmp/')
+  device.PushChangedFiles([(adb_reboot, '/data/local/tmp/')])
   # Launch adb_reboot
   logging.info('  Launching adb_reboot ...')
   device.old_interface.GetAndroidToolStatusAndOutput(
diff --git a/build/android/push_libraries.gypi b/build/android/push_libraries.gypi
index d74fb21..c96e5a5 100644
--- a/build/android/push_libraries.gypi
+++ b/build/android/push_libraries.gypi
@@ -32,6 +32,7 @@
     '<(strip_stamp)',
     '<(strip_additional_stamp)',
     '<(build_device_config_path)',
+    '<(pack_arm_relocations_stamp)',
   ],
   'outputs': [
     '<(push_stamp)',
diff --git a/build/android/pylib/chrome_test_server_spawner.py b/build/android/pylib/chrome_test_server_spawner.py
index e1fe7b1..052c2fd 100644
--- a/build/android/pylib/chrome_test_server_spawner.py
+++ b/build/android/pylib/chrome_test_server_spawner.py
@@ -64,17 +64,14 @@
   return False
 
 
-def _CheckPortStatus(port, expected_status):
-  """Returns True if port has expected_status.
+def _CheckPortAvailable(port):
+  """Returns True if |port| is available."""
+  return _WaitUntil(lambda: ports.IsHostPortAvailable(port))
 
-  Args:
-    port: the port number.
-    expected_status: boolean of expected status.
 
-  Returns:
-    Returns True if the status is expected. Otherwise returns False.
-  """
-  return _WaitUntil(lambda: ports.IsHostPortUsed(port) == expected_status)
+def _CheckPortNotAvailable(port):
+  """Returns True if |port| is not available."""
+  return _WaitUntil(lambda: not ports.IsHostPortAvailable(port))
 
 
 def _CheckDevicePortStatus(device, port):
@@ -167,7 +164,7 @@
     port_json = json.loads(port_json)
     if port_json.has_key('port') and isinstance(port_json['port'], int):
       self.host_port = port_json['port']
-      return _CheckPortStatus(self.host_port, True)
+      return _CheckPortNotAvailable(self.host_port)
     logging.error('Failed to get port information from the server data.')
     return False
 
@@ -236,7 +233,7 @@
       if self.pipe_out:
         self.is_ready = self._WaitToStartAndGetPortFromTestServer()
       else:
-        self.is_ready = _CheckPortStatus(self.host_port, True)
+        self.is_ready = _CheckPortNotAvailable(self.host_port)
     if self.is_ready:
       Forwarder.Map([(0, self.host_port)], self.device, self.tool)
       # Check whether the forwarder is ready on the device.
@@ -346,7 +343,7 @@
     logging.info('Handling request to kill a test server on port: %d.', port)
     self.server.test_server_instance.Stop()
     # Make sure the status of test server is correct before sending response.
-    if _CheckPortStatus(port, False):
+    if _CheckPortAvailable(port):
       self._SendResponse(200, 'OK', {}, 'killed')
       logging.info('Test server on port %d is killed', port)
     else:
diff --git a/build/android/pylib/constants.py b/build/android/pylib/constants.py
index 8b800ab..292ff3b 100644
--- a/build/android/pylib/constants.py
+++ b/build/android/pylib/constants.py
@@ -172,6 +172,10 @@
   os.environ['BUILDTYPE'] = build_type
 
 
+def SetBuildDirectory(build_directory):
+  os.environ['CHROMIUM_OUT_DIR'] = build_directory
+
+
 def GetOutDirectory(build_type=None):
   """Returns the out directory where the output binaries are built.
 
diff --git a/build/android/pylib/device/commands/__init__.py b/build/android/pylib/device/commands/__init__.py
new file mode 100644
index 0000000..4d6aabb
--- /dev/null
+++ b/build/android/pylib/device/commands/__init__.py
@@ -0,0 +1,3 @@
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
diff --git a/build/android/pylib/device/commands/commands.gyp b/build/android/pylib/device/commands/commands.gyp
new file mode 100644
index 0000000..d173e39
--- /dev/null
+++ b/build/android/pylib/device/commands/commands.gyp
@@ -0,0 +1,18 @@
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+{
+  'targets': [
+    {
+      'target_name': 'chromium_commands',
+      'type': 'none',
+      'variables': {
+        'java_in_dir': ['java'],
+      },
+      'includes': [
+        '../../../../../build/java.gypi',
+      ],
+    }
+  ],
+}
diff --git a/build/android/pylib/device/commands/install_commands.py b/build/android/pylib/device/commands/install_commands.py
new file mode 100644
index 0000000..35b11e3
--- /dev/null
+++ b/build/android/pylib/device/commands/install_commands.py
@@ -0,0 +1,51 @@
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import os
+
+from pylib import constants
+
+BIN_DIR = '%s/bin' % constants.TEST_EXECUTABLE_DIR
+_FRAMEWORK_DIR = '%s/framework' % constants.TEST_EXECUTABLE_DIR
+
+_COMMANDS = {
+  'unzip': 'org.chromium.android.commands.unzip.Unzip',
+}
+
+_SHELL_COMMAND_FORMAT = (
+"""#!/system/bin/sh
+base=%s
+export CLASSPATH=$base/framework/chromium_commands.jar
+exec app_process $base/bin %s $@
+""")
+
+
+def Installed(device):
+  return (all(device.FileExists('%s/%s' % (BIN_DIR, c)) for c in _COMMANDS)
+          and device.FileExists('%s/chromium_commands.jar' % _FRAMEWORK_DIR))
+
+def InstallCommands(device):
+  if device.IsUserBuild():
+    raise Exception('chromium_commands currently requires a userdebug build.')
+
+  chromium_commands_jar_path = os.path.join(
+      constants.GetOutDirectory(), constants.SDK_BUILD_JAVALIB_DIR,
+      'chromium_commands.dex.jar')
+  if not os.path.exists(chromium_commands_jar_path):
+    raise Exception('%s not found. Please build chromium_commands.'
+                    % chromium_commands_jar_path)
+
+  device.RunShellCommand(['mkdir', BIN_DIR, _FRAMEWORK_DIR])
+  for command, main_class in _COMMANDS.iteritems():
+    shell_command = _SHELL_COMMAND_FORMAT % (
+        constants.TEST_EXECUTABLE_DIR, main_class)
+    shell_file = '%s/%s' % (BIN_DIR, command)
+    device.WriteTextFile(shell_file, shell_command)
+    device.RunShellCommand(
+        ['chmod', '755', shell_file], check_return=True)
+
+  device.adb.Push(
+      chromium_commands_jar_path,
+      '%s/chromium_commands.jar' % _FRAMEWORK_DIR)
+
diff --git a/build/android/pylib/device/commands/java/src/org/chromium/android/commands/unzip/Unzip.java b/build/android/pylib/device/commands/java/src/org/chromium/android/commands/unzip/Unzip.java
new file mode 100644
index 0000000..4d2a045
--- /dev/null
+++ b/build/android/pylib/device/commands/java/src/org/chromium/android/commands/unzip/Unzip.java
@@ -0,0 +1,94 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.android.commands.unzip;
+
+import android.util.Log;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+/**
+ *  Minimal implementation of the command-line unzip utility for Android.
+ */
+public class Unzip {
+
+    private static final String TAG = "Unzip";
+
+    public static void main(String[] args) {
+        try {
+            (new Unzip()).run(args);
+        } catch (RuntimeException e) {
+            Log.e(TAG, e.toString());
+            System.exit(1);
+        }
+    }
+
+    private void showUsage(PrintStream s) {
+        s.println("Usage:");
+        s.println("unzip [zipfile]");
+    }
+
+    private void unzip(String[] args) {
+        ZipInputStream zis = null;
+        try {
+            String zipfile = args[0];
+            zis = new ZipInputStream(new BufferedInputStream(new FileInputStream(zipfile)));
+            ZipEntry ze = null;
+
+            byte[] bytes = new byte[1024];
+            while ((ze = zis.getNextEntry()) != null) {
+                File outputFile = new File(ze.getName());
+                if (ze.isDirectory()) {
+                    if (!outputFile.exists() && !outputFile.mkdirs()) {
+                        throw new RuntimeException(
+                                "Failed to create directory: " + outputFile.toString());
+                    }
+                } else {
+                    File parentDir = outputFile.getParentFile();
+                    if (!parentDir.exists() && !parentDir.mkdirs()) {
+                        throw new RuntimeException(
+                                "Failed to create directory: " + parentDir.toString());
+                    }
+                    OutputStream out = new BufferedOutputStream(new FileOutputStream(outputFile));
+                    int actual_bytes = 0;
+                    int total_bytes = 0;
+                    while ((actual_bytes = zis.read(bytes)) != -1) {
+                        out.write(bytes, 0, actual_bytes);
+                        total_bytes += actual_bytes;
+                    }
+                    out.close();
+                }
+                zis.closeEntry();
+            }
+
+        } catch (IOException e) {
+            throw new RuntimeException("Error while unzipping: " + e.toString());
+        } finally {
+            try {
+                if (zis != null) zis.close();
+            } catch (IOException e) {
+                throw new RuntimeException("Error while closing zip: " + e.toString());
+            }
+        }
+    }
+
+    public void run(String[] args) {
+        if (args.length != 1) {
+            showUsage(System.err);
+            throw new RuntimeException("Incorrect usage.");
+        }
+
+        unzip(args);
+    }
+}
+
diff --git a/build/android/pylib/device/device_utils.py b/build/android/pylib/device/device_utils.py
index fccdd61..b726cb9 100644
--- a/build/android/pylib/device/device_utils.py
+++ b/build/android/pylib/device/device_utils.py
@@ -8,15 +8,22 @@
 """
 # pylint: disable=W0613
 
+import logging
+import multiprocessing
+import os
 import pipes
 import sys
+import tempfile
 import time
+import zipfile
 
 import pylib.android_commands
 from pylib.device import adb_wrapper
 from pylib.device import decorators
 from pylib.device import device_errors
+from pylib.device.commands import install_commands
 from pylib.utils import apk_helper
+from pylib.utils import host_utils
 from pylib.utils import parallelizer
 
 _DEFAULT_TIMEOUT = 30
@@ -61,17 +68,23 @@
                        operation should be retried on failure if no explicit
                        value is provided.
     """
+    self.adb = None
     self.old_interface = None
     if isinstance(device, basestring):
+      self.adb = adb_wrapper.AdbWrapper(device)
       self.old_interface = pylib.android_commands.AndroidCommands(device)
     elif isinstance(device, adb_wrapper.AdbWrapper):
+      self.adb = device
       self.old_interface = pylib.android_commands.AndroidCommands(str(device))
     elif isinstance(device, pylib.android_commands.AndroidCommands):
+      self.adb = adb_wrapper.AdbWrapper(device.GetDevice())
       self.old_interface = device
     elif not device:
+      self.adb = adb_wrapper.AdbWrapper('')
       self.old_interface = pylib.android_commands.AndroidCommands()
     else:
       raise ValueError('Unsupported type passed for argument "device"')
+    self._commands_installed = None
     self._default_timeout = default_timeout
     self._default_retries = default_retries
     assert(hasattr(self, decorators.DEFAULT_TIMEOUT_ATTR))
@@ -91,6 +104,9 @@
     Raises:
       CommandTimeoutError on timeout.
     """
+    return self._IsOnlineImpl()
+
+  def _IsOnlineImpl(self):
     return self.old_interface.IsOnline()
 
   @decorators.WithTimeoutAndRetriesFromInstance()
@@ -111,17 +127,6 @@
     return self._HasRootImpl()
 
   def _HasRootImpl(self):
-    """Implementation of HasRoot.
-
-    This is split from HasRoot to allow other DeviceUtils methods to call
-    HasRoot without spawning a new timeout thread.
-
-    Returns:
-      Same as for |HasRoot|.
-
-    Raises:
-      Same as for |HasRoot|.
-    """
     return self.old_interface.IsRootEnabled()
 
   @decorators.WithTimeoutAndRetriesFromInstance()
@@ -141,6 +146,24 @@
           'Could not enable root.', device=str(self))
 
   @decorators.WithTimeoutAndRetriesFromInstance()
+  def IsUserBuild(self, timeout=None, retries=None):
+    """Checks whether or not the device is running a user build.
+
+    Args:
+      timeout: timeout in seconds
+      retries: number of retries
+
+    Returns:
+      True if the device is running a user build, False otherwise (i.e. if
+        it's running a userdebug build).
+
+    Raises:
+      CommandTimeoutError on timeout.
+      DeviceUnreachableError on missing device.
+    """
+    return self._GetPropImpl('ro.build.type') == 'user'
+
+  @decorators.WithTimeoutAndRetriesFromInstance()
   def GetExternalStoragePath(self, timeout=None, retries=None):
     """Get the device's path to its SD card.
 
@@ -156,6 +179,9 @@
       CommandTimeoutError on timeout.
       DeviceUnreachableError on missing device.
     """
+    return self._GetExternalStoragePathImpl()
+
+  def _GetExternalStoragePathImpl(self):
     try:
       return self.old_interface.GetExternalStorage()
     except AssertionError as e:
@@ -183,21 +209,6 @@
     self._WaitUntilFullyBootedImpl(wifi=wifi, timeout=timeout)
 
   def _WaitUntilFullyBootedImpl(self, wifi=False, timeout=None):
-    """Implementation of WaitUntilFullyBooted.
-
-    This is split from WaitUntilFullyBooted to allow other DeviceUtils methods
-    to call WaitUntilFullyBooted without spawning a new timeout thread.
-
-    TODO(jbudorick) Remove the timeout parameter once this is no longer
-    implemented via AndroidCommands.
-
-    Args:
-      wifi: Same as for |WaitUntilFullyBooted|.
-      timeout: timeout in seconds
-
-    Raises:
-      Same as for |WaitUntilFullyBooted|.
-    """
     if timeout is None:
       timeout = self._default_timeout
     self.old_interface.WaitForSystemBootCompleted(timeout)
@@ -281,8 +292,8 @@
             str(e), device=str(self)), None, sys.exc_info()[2]
 
   @decorators.WithTimeoutAndRetriesFromInstance()
-  def RunShellCommand(self, cmd, check_return=False, as_root=False,
-                      timeout=None, retries=None):
+  def RunShellCommand(self, cmd, check_return=False, as_root=False, cwd=None,
+                      env=None, timeout=None, retries=None):
     """Run an ADB shell command.
 
     TODO(jbudorick) Switch the default value of check_return to True after
@@ -294,6 +305,8 @@
                     be checked.
       as_root: A boolean indicating whether the shell command should be run
                with root privileges.
+      cwd: The device directory in which the command should be run.
+      env: The environment variables with which the command should be run.
       timeout: timeout in seconds
       retries: number of retries
 
@@ -305,35 +318,23 @@
       CommandTimeoutError on timeout.
       DeviceUnreachableError on missing device.
     """
-    return self._RunShellCommandImpl(cmd, check_return=check_return,
-                                     as_root=as_root, timeout=timeout)
+    return self._RunShellCommandImpl(
+        cmd, check_return=check_return, as_root=as_root, cwd=cwd, env=env,
+        timeout=timeout)
 
   def _RunShellCommandImpl(self, cmd, check_return=False, as_root=False,
-                           timeout=None):
-    """Implementation of RunShellCommand.
-
-    This is split from RunShellCommand to allow other DeviceUtils methods to
-    call RunShellCommand without spawning a new timeout thread.
-
-    TODO(jbudorick) Remove the timeout parameter once this is no longer
-    implemented via AndroidCommands.
-
-    Args:
-      cmd: Same as for |RunShellCommand|.
-      check_return: Same as for |RunShellCommand|.
-      as_root: Same as for |RunShellCommand|.
-      timeout: timeout in seconds
-
-    Raises:
-      Same as for |RunShellCommand|.
-
-    Returns:
-      Same as for |RunShellCommand|.
-    """
+                           cwd=None, env=None, timeout=None):
+    # TODO(jbudorick): Remove the timeout parameter once this is no longer
+    # backed by AndroidCommands.
     if isinstance(cmd, list):
       cmd = ' '.join(cmd)
-    if as_root and not self.HasRoot():
+    if as_root and not self._HasRootImpl():
       cmd = 'su -c %s' % cmd
+    if env:
+      cmd = '%s %s' % (
+          ' '.join('%s=%s' % (k, v) for k, v in env.iteritems()), cmd)
+    if cwd:
+      cmd = 'cd %s && %s' % (cwd, cmd)
     if check_return:
       code, output = self.old_interface.GetShellCommandStatusAndOutput(
           cmd, timeout_time=timeout)
@@ -501,15 +502,15 @@
   @decorators.WithTimeoutAndRetriesDefaults(
       PUSH_CHANGED_FILES_DEFAULT_TIMEOUT,
       PUSH_CHANGED_FILES_DEFAULT_RETRIES)
-  def PushChangedFiles(self, host_path, device_path, timeout=None,
+  def PushChangedFiles(self, host_device_tuples, timeout=None,
                        retries=None):
     """Push files to the device, skipping files that don't need updating.
 
     Args:
-      host_path: A string containing the absolute path to the file or directory
-                 on the host that should be minimally pushed to the device.
-      device_path: A string containing the absolute path of the destination on
-                   the device.
+      host_device_tuples: A list of (host_path, device_path) tuples, where
+        |host_path| is an absolute path of a file or directory on the host
+        that should be minimially pushed to the device, and |device_path| is
+        an absolute path of the destination on the device.
       timeout: timeout in seconds
       retries: number of retries
 
@@ -518,7 +519,165 @@
       CommandTimeoutError on timeout.
       DeviceUnreachableError on missing device.
     """
-    self.old_interface.PushIfNeeded(host_path, device_path)
+
+    files = []
+    for h, d in host_device_tuples:
+      if os.path.isdir(h):
+        self._RunShellCommandImpl(['mkdir', '-p', '"%s"' % d],
+                                  check_return=True)
+      files += self._GetChangedFilesImpl(h, d)
+
+    if not files:
+      return
+
+    size = sum(host_utils.GetRecursiveDiskUsage(h) for h, _ in files)
+    file_count = len(files)
+    dir_size = sum(host_utils.GetRecursiveDiskUsage(h)
+                   for h, _ in host_device_tuples)
+    dir_file_count = 0
+    for h, _ in host_device_tuples:
+      if os.path.isdir(h):
+        dir_file_count += sum(len(f) for _r, _d, f in os.walk(h))
+      else:
+        dir_file_count += 1
+
+    push_duration = self._ApproximateDuration(
+        file_count, file_count, size, False)
+    dir_push_duration = self._ApproximateDuration(
+        len(host_device_tuples), dir_file_count, dir_size, False)
+    zip_duration = self._ApproximateDuration(1, 1, size, True)
+
+    self._InstallCommands()
+
+    if dir_push_duration < push_duration and (
+        dir_push_duration < zip_duration or not self._commands_installed):
+      self._PushChangedFilesIndividually(host_device_tuples)
+    elif push_duration < zip_duration or not self._commands_installed:
+      self._PushChangedFilesIndividually(files)
+    else:
+      self._PushChangedFilesZipped(files)
+      self._RunShellCommandImpl(
+          ['chmod', '-R', '777'] + [d for _, d in host_device_tuples],
+          as_root=True)
+
+  def _GetChangedFilesImpl(self, host_path, device_path):
+    real_host_path = os.path.realpath(host_path)
+    try:
+      real_device_path = self._RunShellCommandImpl(
+          ['realpath', device_path], check_return=True)
+      real_device_path = real_device_path[0]
+    except device_errors.CommandFailedError:
+      return [(host_path, device_path)]
+
+    # TODO(jbudorick): Move the md5 logic up into DeviceUtils or base
+    # this function on mtime.
+    # pylint: disable=W0212
+    host_hash_tuples, device_hash_tuples = self.old_interface._RunMd5Sum(
+        real_host_path, real_device_path)
+    # pylint: enable=W0212
+
+    if os.path.isfile(host_path):
+      if (not device_hash_tuples
+          or device_hash_tuples[0].hash != host_hash_tuples[0].hash):
+        return [(host_path, device_path)]
+      else:
+        return []
+    else:
+      device_tuple_dict = dict((d.path, d.hash) for d in device_hash_tuples)
+      to_push = []
+      for host_hash, host_abs_path in (
+          (h.hash, h.path) for h in host_hash_tuples):
+        device_abs_path = '%s/%s' % (
+            real_device_path, os.path.relpath(host_abs_path, real_host_path))
+        if (device_abs_path not in device_tuple_dict
+            or device_tuple_dict[device_abs_path] != host_hash):
+          to_push.append((host_abs_path, device_abs_path))
+      return to_push
+
+  def _InstallCommands(self):
+    if self._commands_installed is None:
+      try:
+        if not install_commands.Installed(self):
+          install_commands.InstallCommands(self)
+        self._commands_installed = True
+      except Exception as e:
+        logging.warning('unzip not available: %s' % str(e))
+        self._commands_installed = False
+
+  @staticmethod
+  def _ApproximateDuration(adb_calls, file_count, byte_count, is_zipping):
+    # We approximate the time to push a set of files to a device as:
+    #   t = c1 * a + c2 * f + c3 + b / c4 + b / (c5 * c6), where
+    #     t: total time (sec)
+    #     c1: adb call time delay (sec)
+    #     a: number of times adb is called (unitless)
+    #     c2: push time delay (sec)
+    #     f: number of files pushed via adb (unitless)
+    #     c3: zip time delay (sec)
+    #     c4: zip rate (bytes/sec)
+    #     b: total number of bytes (bytes)
+    #     c5: transfer rate (bytes/sec)
+    #     c6: compression ratio (unitless)
+
+    # All of these are approximations.
+    ADB_CALL_PENALTY = 0.1 # seconds
+    ADB_PUSH_PENALTY = 0.01 # seconds
+    ZIP_PENALTY = 2.0 # seconds
+    ZIP_RATE = 10000000.0 # bytes / second
+    TRANSFER_RATE = 2000000.0 # bytes / second
+    COMPRESSION_RATIO = 2.0 # unitless
+
+    adb_call_time = ADB_CALL_PENALTY * adb_calls
+    adb_push_setup_time = ADB_PUSH_PENALTY * file_count
+    if is_zipping:
+      zip_time = ZIP_PENALTY + byte_count / ZIP_RATE
+      transfer_time = byte_count / (TRANSFER_RATE * COMPRESSION_RATIO)
+    else:
+      zip_time = 0
+      transfer_time = byte_count / TRANSFER_RATE
+    return (adb_call_time + adb_push_setup_time + zip_time + transfer_time)
+
+  def _PushChangedFilesIndividually(self, files):
+    for h, d in files:
+      self.adb.Push(h, d)
+
+  def _PushChangedFilesZipped(self, files):
+    if not files:
+      return
+
+    with tempfile.NamedTemporaryFile(suffix='.zip') as zip_file:
+      zip_proc = multiprocessing.Process(
+          target=DeviceUtils._CreateDeviceZip,
+          args=(zip_file.name, files))
+      zip_proc.start()
+      zip_proc.join()
+
+      zip_on_device = '%s/tmp.zip' % self._GetExternalStoragePathImpl()
+      try:
+        self.adb.Push(zip_file.name, zip_on_device)
+        self._RunShellCommandImpl(
+            ['unzip', zip_on_device],
+            as_root=True, check_return=True,
+            env={'PATH': '$PATH:%s' % install_commands.BIN_DIR})
+      finally:
+        if zip_proc.is_alive():
+          zip_proc.terminate()
+        if self._IsOnlineImpl():
+          self._RunShellCommandImpl(['rm', zip_on_device])
+
+  @staticmethod
+  def _CreateDeviceZip(zip_path, host_device_tuples):
+    with zipfile.ZipFile(zip_path, 'w') as zip_file:
+      for host_path, device_path in host_device_tuples:
+        if os.path.isfile(host_path):
+          zip_file.write(host_path, device_path, zipfile.ZIP_DEFLATED)
+        else:
+          for hd, _, files in os.walk(host_path):
+            dd = '%s/%s' % (device_path, os.path.relpath(host_path, hd))
+            zip_file.write(hd, dd, zipfile.ZIP_STORED)
+            for f in files:
+              zip_file.write(os.path.join(hd, f), '%s/%s' % (dd, f),
+                             zipfile.ZIP_DEFLATED)
 
   @decorators.WithTimeoutAndRetriesFromInstance()
   def FileExists(self, device_path, timeout=None, retries=None):
@@ -537,23 +696,6 @@
       CommandTimeoutError on timeout.
       DeviceUnreachableError on missing device.
     """
-    return self._FileExistsImpl(device_path)
-
-  def _FileExistsImpl(self, device_path):
-    """Implementation of FileExists.
-
-    This is split from FileExists to allow other DeviceUtils methods to call
-    FileExists without spawning a new timeout thread.
-
-    Args:
-      device_path: Same as for |FileExists|.
-
-    Returns:
-      True if the file exists on the device, False otherwise.
-
-    Raises:
-      Same as for |FileExists|.
-    """
     return self.old_interface.FileExistsOnDevice(device_path)
 
   @decorators.WithTimeoutAndRetriesFromInstance()
@@ -598,7 +740,7 @@
       CommandTimeoutError on timeout.
       DeviceUnreachableError on missing device.
     """
-    # TODO(jbudorick) Evaluate whether we awant to return a list of lines after
+    # TODO(jbudorick) Evaluate whether we want to return a list of lines after
     # the implementation switch, and if file not found should raise exception.
     if as_root:
       if not self.old_interface.CanAccessProtectedFileContents():
@@ -714,6 +856,9 @@
     Raises:
       CommandTimeoutError on timeout.
     """
+    return self._GetPropImpl(property_name)
+
+  def _GetPropImpl(self, property_name):
     return self.old_interface.system_properties[property_name]
 
   @decorators.WithTimeoutAndRetriesFromInstance()
@@ -734,6 +879,22 @@
     self.old_interface.system_properties[property_name] = value
 
   @decorators.WithTimeoutAndRetriesFromInstance()
+  def GetABI(self, timeout=None, retries=None):
+    """Gets the device main ABI.
+
+    Args:
+      timeout: timeout in seconds
+      retries: number of retries
+
+    Returns:
+      The device's main ABI name.
+
+    Raises:
+      CommandTimeoutError on timeout.
+    """
+    return self.GetProp('ro.product.cpu.abi')
+
+  @decorators.WithTimeoutAndRetriesFromInstance()
   def GetPids(self, process_name, timeout=None, retries=None):
     """Returns the PIDs of processes with the given name.
 
@@ -755,21 +916,6 @@
     return self._GetPidsImpl(process_name)
 
   def _GetPidsImpl(self, process_name):
-    """Implementation of GetPids.
-
-    This is split from GetPids to allow other DeviceUtils methods to call
-    GetPids without spawning a new timeout thread.
-
-    Args:
-      process_name: A string containing the process name to get the PIDs for.
-
-    Returns:
-      A dict mapping process name to PID for each process that contained the
-      provided |process_name|.
-
-    Raises:
-      DeviceUnreachableError on missing device.
-    """
     procs_pids = {}
     for line in self._RunShellCommandImpl('ps'):
       try:
@@ -869,4 +1015,3 @@
     return parallelizer_type([
         d if isinstance(d, DeviceUtils) else DeviceUtils(d)
         for d in devices])
-
diff --git a/build/android/pylib/device/device_utils_test.py b/build/android/pylib/device/device_utils_test.py
index 42dc5b2..513b538 100755
--- a/build/android/pylib/device/device_utils_test.py
+++ b/build/android/pylib/device/device_utils_test.py
@@ -85,6 +85,7 @@
                             st_size, st_atime, st_mtime, st_ctime)
 
   MOCKED_FUNCTIONS = [
+    ('os.listdir', []),
     ('os.path.abspath', ''),
     ('os.path.dirname', ''),
     ('os.path.exists', False),
@@ -117,14 +118,23 @@
   def addMockDirectory(self, path, **kw):
     self._addMockThing(path, True, **kw)
 
-  def _addMockThing(self, path, is_dir, size=0, stat=None, walk=None):
+  def _addMockThing(self, path, is_dir, listdir=None, size=0, stat=None,
+                    walk=None):
+    if listdir is None:
+      listdir = []
     if stat is None:
       stat = self.osStatResult()
     if walk is None:
       walk = []
+
+    dirname = os.sep.join(path.rstrip(os.sep).split(os.sep)[:-1])
+    if dirname and not dirname in self.mock_file_info:
+      self._addMockThing(dirname, True)
+
     self.mock_file_info[path] = {
+      'os.listdir': listdir,
       'os.path.abspath': path,
-      'os.path.dirname': '/' + '/'.join(path.strip('/').split('/')[:-1]),
+      'os.path.dirname': dirname,
       'os.path.exists': True,
       'os.path.isdir': is_dir,
       'os.path.getsize': size,
@@ -208,6 +218,24 @@
         '0123456789abcdef', default_timeout=1, default_retries=0)
 
 
+class DeviceUtilsNewImplTest(unittest.TestCase):
+
+  def setUp(self):
+    test_serial = '0123456789abcdef'
+    self.adb = mock.Mock(spec=adb_wrapper.AdbWrapper)
+    self.adb.__str__ = mock.Mock(return_value=test_serial)
+    self.adb.GetDeviceSerial.return_value = test_serial
+    self.device = device_utils.DeviceUtils(
+        self.adb, default_timeout=1, default_retries=0)
+
+
+class DeviceUtilsHybridImplTest(DeviceUtilsOldImplTest):
+
+  def setUp(self):
+    super(DeviceUtilsHybridImplTest, self).setUp()
+    self.device.adb = self.adb = mock.Mock(spec=adb_wrapper.AdbWrapper)
+
+
 class DeviceUtilsIsOnlineTest(DeviceUtilsOldImplTest):
 
   def testIsOnline_true(self):
@@ -260,6 +288,21 @@
         self.device.EnableRoot()
 
 
+class DeviceUtilsIsUserBuildTest(DeviceUtilsOldImplTest):
+
+  def testIsUserBuild_yes(self):
+    with self.assertCalls(
+        'adb -s 0123456789abcdef shell getprop ro.build.type',
+        'user\r\n'):
+      self.assertTrue(self.device.IsUserBuild())
+
+  def testIsUserBuild_no(self):
+    with self.assertCalls(
+        'adb -s 0123456789abcdef shell getprop ro.build.type',
+        'userdebug\r\n'):
+      self.assertFalse(self.device.IsUserBuild())
+
+
 class DeviceUtilsGetExternalStoragePathTest(DeviceUtilsOldImplTest):
 
   def testGetExternalStoragePath_succeeds(self):
@@ -843,132 +886,95 @@
       self.device.SendKeyEvent(66)
 
 
-class DeviceUtilsPushChangedFilesTest(DeviceUtilsOldImplTest):
+class DeviceUtilsPushChangedFilesIndividuallyTest(DeviceUtilsNewImplTest):
+
+  def testPushChangedFilesIndividually_empty(self):
+    test_files = []
+    self.device._PushChangedFilesIndividually(test_files)
+    self.assertEqual(0, self.adb.Push.call_count)
+
+  def testPushChangedFilesIndividually_single(self):
+    test_files = [('/test/host/path', '/test/device/path')]
+    self.device._PushChangedFilesIndividually(test_files)
+    self.adb.Push.assert_called_once_with(
+        '/test/host/path', '/test/device/path')
+
+  def testPushChangedFilesIndividually_multiple(self):
+    test_files = [
+        ('/test/host/path/file1', '/test/device/path/file1'),
+        ('/test/host/path/file2', '/test/device/path/file2')]
+    self.device._PushChangedFilesIndividually(test_files)
+    self.assertEqual(2, self.adb.Push.call_count)
+    self.adb.Push.assert_any_call(
+        '/test/host/path/file1', '/test/device/path/file1')
+    self.adb.Push.assert_any_call(
+        '/test/host/path/file2', '/test/device/path/file2')
 
 
-  def testPushChangedFiles_noHostPath(self):
-    with mock.patch('os.path.exists', return_value=False):
-      with self.assertRaises(device_errors.CommandFailedError):
-        self.device.PushChangedFiles('/test/host/path', '/test/device/path')
+@mock.patch('pylib.device.commands.install_commands.Installed', new=None)
+@mock.patch('pylib.device.commands.install_commands.InstallCommands', new=None)
+class DeviceUtilsPushChangedFilesZippedTest(DeviceUtilsHybridImplTest):
 
-  def testPushChangedFiles_file_noChange(self):
-    self.device.old_interface._push_if_needed_cache = {}
+  def setUp(self):
+    super(DeviceUtilsPushChangedFilesZippedTest, self).setUp()
 
-    host_file_path = '/test/host/path'
-    device_file_path = '/test/device/path'
+  def testPushChangedFilesZipped_empty(self):
+    test_files = []
+    self.device._PushChangedFilesZipped(test_files)
+    self.assertEqual(0, self.adb.Push.call_count)
 
-    mock_fs = MockFileSystem()
-    mock_fs.addMockFile(host_file_path, size=100)
+  def testPushChangedFilesZipped_single(self):
+    test_files = [('/test/host/path/file1', '/test/device/path/file1')]
 
-    self.device.old_interface.GetFilesChanged = mock.Mock(return_value=[])
+    self.device._GetExternalStoragePathImpl = mock.Mock(
+        return_value='/test/device/external_dir')
+    self.device._IsOnlineImpl = mock.Mock(return_value=True)
+    self.device._RunShellCommandImpl = mock.Mock()
+    mock_zip_temp = mock.mock_open()
+    mock_zip_temp.return_value.name = '/test/temp/file/tmp.zip'
+    with mock.patch('multiprocessing.Process') as mock_zip_proc, (
+         mock.patch('tempfile.NamedTemporaryFile', mock_zip_temp)):
+      self.device._PushChangedFilesZipped(test_files)
 
-    with mock_fs:
-      # GetFilesChanged is mocked, so its adb calls are omitted.
-      with self.assertNoAdbCalls():
-        self.device.PushChangedFiles(host_file_path, device_file_path)
+    mock_zip_proc.assert_called_once_with(
+        target=device_utils.DeviceUtils._CreateDeviceZip,
+        args=('/test/temp/file/tmp.zip', test_files))
+    self.adb.Push.assert_called_once_with(
+        '/test/temp/file/tmp.zip', '/test/device/external_dir/tmp.zip')
+    self.assertEqual(2, self.device._RunShellCommandImpl.call_count)
+    self.device._RunShellCommandImpl.assert_any_call(
+        ['unzip', '/test/device/external_dir/tmp.zip'],
+        as_root=True, check_return=True,
+        env={'PATH': '$PATH:/data/local/tmp/bin'})
+    self.device._RunShellCommandImpl.assert_any_call(
+        ['rm', '/test/device/external_dir/tmp.zip'])
 
-  def testPushChangedFiles_file_changed(self):
-    self.device.old_interface._push_if_needed_cache = {}
+  def testPushChangedFilesZipped_multiple(self):
+    test_files = [('/test/host/path/file1', '/test/device/path/file1'),
+                  ('/test/host/path/file2', '/test/device/path/file2')]
 
-    host_file_path = '/test/host/path'
-    device_file_path = '/test/device/path'
+    self.device._GetExternalStoragePathImpl = mock.Mock(
+        return_value='/test/device/external_dir')
+    self.device._IsOnlineImpl = mock.Mock(return_value=True)
+    self.device._RunShellCommandImpl = mock.Mock()
+    mock_zip_temp = mock.mock_open()
+    mock_zip_temp.return_value.name = '/test/temp/file/tmp.zip'
+    with mock.patch('multiprocessing.Process') as mock_zip_proc, (
+         mock.patch('tempfile.NamedTemporaryFile', mock_zip_temp)):
+      self.device._PushChangedFilesZipped(test_files)
 
-    mock_fs = MockFileSystem()
-    mock_fs.addMockFile(
-        host_file_path, size=100,
-        stat=MockFileSystem.osStatResult(st_mtime=1000000000))
-
-    self.device.old_interface.GetFilesChanged = mock.Mock(
-        return_value=[('/test/host/path', '/test/device/path')])
-
-    with mock_fs:
-      with self.assertCalls('adb -s 0123456789abcdef push '
-          '/test/host/path /test/device/path', '100 B/s (100 B in 1.000s)\r\n'):
-        self.device.PushChangedFiles(host_file_path, device_file_path)
-
-  def testPushChangedFiles_directory_nothingChanged(self):
-    self.device.old_interface._push_if_needed_cache = {}
-
-    host_file_path = '/test/host/path'
-    device_file_path = '/test/device/path'
-
-    mock_fs = MockFileSystem()
-    mock_fs.addMockDirectory(
-        host_file_path, size=256,
-        stat=MockFileSystem.osStatResult(st_mtime=1000000000))
-    mock_fs.addMockFile(
-        host_file_path + '/file1', size=251,
-        stat=MockFileSystem.osStatResult(st_mtime=1000000001))
-    mock_fs.addMockFile(
-        host_file_path + '/file2', size=252,
-        stat=MockFileSystem.osStatResult(st_mtime=1000000002))
-
-    self.device.old_interface.GetFilesChanged = mock.Mock(return_value=[])
-
-    with mock_fs:
-      with self.assertCallsSequence([
-          ("adb -s 0123456789abcdef shell 'mkdir -p \"/test/device/path\"'",
-           '')]):
-        self.device.PushChangedFiles(host_file_path, device_file_path)
-
-  def testPushChangedFiles_directory_somethingChanged(self):
-    self.device.old_interface._push_if_needed_cache = {}
-
-    host_file_path = '/test/host/path'
-    device_file_path = '/test/device/path'
-
-    mock_fs = MockFileSystem()
-    mock_fs.addMockDirectory(
-        host_file_path, size=256,
-        stat=MockFileSystem.osStatResult(st_mtime=1000000000),
-        walk=[('/test/host/path', [], ['file1', 'file2'])])
-    mock_fs.addMockFile(
-        host_file_path + '/file1', size=256,
-        stat=MockFileSystem.osStatResult(st_mtime=1000000001))
-    mock_fs.addMockFile(
-        host_file_path + '/file2', size=256,
-        stat=MockFileSystem.osStatResult(st_mtime=1000000002))
-
-    self.device.old_interface.GetFilesChanged = mock.Mock(
-        return_value=[('/test/host/path/file1', '/test/device/path/file1')])
-
-    with mock_fs:
-      with self.assertCallsSequence([
-          ("adb -s 0123456789abcdef shell 'mkdir -p \"/test/device/path\"'",
-           ''),
-          ('adb -s 0123456789abcdef push '
-              '/test/host/path/file1 /test/device/path/file1',
-           '256 B/s (256 B in 1.000s)\r\n')]):
-        self.device.PushChangedFiles(host_file_path, device_file_path)
-
-  def testPushChangedFiles_directory_everythingChanged(self):
-    self.device.old_interface._push_if_needed_cache = {}
-
-    host_file_path = '/test/host/path'
-    device_file_path = '/test/device/path'
-
-    mock_fs = MockFileSystem()
-    mock_fs.addMockDirectory(
-        host_file_path, size=256,
-        stat=MockFileSystem.osStatResult(st_mtime=1000000000))
-    mock_fs.addMockFile(
-        host_file_path + '/file1', size=256,
-        stat=MockFileSystem.osStatResult(st_mtime=1000000001))
-    mock_fs.addMockFile(
-        host_file_path + '/file2', size=256,
-        stat=MockFileSystem.osStatResult(st_mtime=1000000002))
-
-    self.device.old_interface.GetFilesChanged = mock.Mock(
-        return_value=[('/test/host/path/file1', '/test/device/path/file1'),
-                      ('/test/host/path/file2', '/test/device/path/file2')])
-
-    with mock_fs:
-      with self.assertCallsSequence([
-          ("adb -s 0123456789abcdef shell 'mkdir -p \"/test/device/path\"'",
-           ''),
-          ('adb -s 0123456789abcdef push /test/host/path /test/device/path',
-           '768 B/s (768 B in 1.000s)\r\n')]):
-        self.device.PushChangedFiles(host_file_path, device_file_path)
+    mock_zip_proc.assert_called_once_with(
+        target=device_utils.DeviceUtils._CreateDeviceZip,
+        args=('/test/temp/file/tmp.zip', test_files))
+    self.adb.Push.assert_called_once_with(
+        '/test/temp/file/tmp.zip', '/test/device/external_dir/tmp.zip')
+    self.assertEqual(2, self.device._RunShellCommandImpl.call_count)
+    self.device._RunShellCommandImpl.assert_any_call(
+        ['unzip', '/test/device/external_dir/tmp.zip'],
+        as_root=True, check_return=True,
+        env={'PATH': '$PATH:/data/local/tmp/bin'})
+    self.device._RunShellCommandImpl.assert_any_call(
+        ['rm', '/test/device/external_dir/tmp.zip'])
 
 
 class DeviceUtilsFileExistsTest(DeviceUtilsOldImplTest):
diff --git a/build/android/pylib/forwarder.py b/build/android/pylib/forwarder.py
index db6ea03..5e45043 100644
--- a/build/android/pylib/forwarder.py
+++ b/build/android/pylib/forwarder.py
@@ -288,9 +288,9 @@
     if device_serial in self._initialized_devices:
       return
     Forwarder._KillDeviceLocked(device, tool)
-    device.PushChangedFiles(
+    device.PushChangedFiles([(
         self._device_forwarder_path_on_host,
-        Forwarder._DEVICE_FORWARDER_FOLDER)
+        Forwarder._DEVICE_FORWARDER_FOLDER)])
     cmd = '%s %s' % (tool.GetUtilWrapper(), Forwarder._DEVICE_FORWARDER_PATH)
     (exit_code, output) = device.old_interface.GetAndroidToolStatusAndOutput(
         cmd, lib_path=Forwarder._DEVICE_FORWARDER_FOLDER)
diff --git a/build/android/pylib/gtest/setup.py b/build/android/pylib/gtest/setup.py
index 1e52d3b..61a8539 100644
--- a/build/android/pylib/gtest/setup.py
+++ b/build/android/pylib/gtest/setup.py
@@ -14,9 +14,11 @@
 
 from pylib import cmd_helper
 from pylib import constants
+from pylib import valgrind_tools
 
 from pylib.base import base_test_result
 from pylib.base import test_dispatcher
+from pylib.device import device_utils
 from pylib.gtest import test_package_apk
 from pylib.gtest import test_package_exe
 from pylib.gtest import test_runner
@@ -287,6 +289,19 @@
   return tests
 
 
+def PushDataDeps(device, test_options, test_package):
+  valgrind_tools.PushFilesForTool(test_options.tool, device)
+  if os.path.exists(constants.ISOLATE_DEPS_DIR):
+    device_dir = (
+        constants.TEST_EXECUTABLE_DIR
+        if test_package.suite_name == 'breakpad_unittests'
+        else device.GetExternalStoragePath())
+    device.PushChangedFiles([
+        (os.path.join(constants.ISOLATE_DEPS_DIR, p),
+         '%s/%s' % (device_dir, p))
+        for p in os.listdir(constants.ISOLATE_DEPS_DIR)])
+
+
 def Setup(test_options, devices):
   """Create the test runner factory and tests.
 
@@ -314,6 +329,9 @@
   _GenerateDepsDirUsingIsolate(test_options.suite_name,
                                test_options.isolate_file_path)
 
+  device_utils.DeviceUtils.parallel(devices).pMap(
+      PushDataDeps, test_options, test_package)
+
   tests = _GetTests(test_options, test_package, devices)
 
   # Constructs a new TestRunner with the current options.
diff --git a/build/android/pylib/gtest/test_package_apk.py b/build/android/pylib/gtest/test_package_apk.py
index 429cd2b..4a91278 100644
--- a/build/android/pylib/gtest/test_package_apk.py
+++ b/build/android/pylib/gtest/test_package_apk.py
@@ -44,9 +44,9 @@
     # GTest expects argv[0] to be the executable path.
     command_line_file.write(self.suite_name + ' ' + options)
     command_line_file.flush()
-    device.PushChangedFiles(
+    device.PushChangedFiles([(
         command_line_file.name,
-        self._package_info.cmdline_file)
+        self._package_info.cmdline_file)])
 
   def _GetFifo(self):
     # The test.fifo path is determined by:
@@ -131,5 +131,5 @@
 
   #override
   def Install(self, device):
-    self.tool.CopyFiles()
+    self.tool.CopyFiles(device)
     device.Install(self.suite_path)
diff --git a/build/android/pylib/gtest/test_package_exe.py b/build/android/pylib/gtest/test_package_exe.py
index 5f82aad..b0be35c 100644
--- a/build/android/pylib/gtest/test_package_exe.py
+++ b/build/android/pylib/gtest/test_package_exe.py
@@ -105,9 +105,9 @@
                           TestPackageExecutable._TEST_RUNNER_RET_VAL_FILE))
     sh_script_file.flush()
     cmd_helper.RunCmd(['chmod', '+x', sh_script_file.name])
-    device.PushChangedFiles(
+    device.PushChangedFiles([(
         sh_script_file.name,
-        constants.TEST_EXECUTABLE_DIR + '/chrome_test_runner.sh')
+        constants.TEST_EXECUTABLE_DIR + '/chrome_test_runner.sh')])
     logging.info('Conents of the test runner script: ')
     for line in open(sh_script_file.name).readlines():
       logging.info('  ' + line.rstrip())
@@ -148,4 +148,4 @@
              self.suite_name + '_stripped'))
 
     test_binary = constants.TEST_EXECUTABLE_DIR + '/' + self.suite_name
-    device.PushChangedFiles(target_name, test_binary)
+    device.PushChangedFiles([(target_name, test_binary)])
diff --git a/build/android/pylib/gtest/test_runner.py b/build/android/pylib/gtest/test_runner.py
index faffe8f..9f89beb 100644
--- a/build/android/pylib/gtest/test_runner.py
+++ b/build/android/pylib/gtest/test_runner.py
@@ -6,7 +6,6 @@
 import os
 import re
 
-from pylib import constants
 from pylib import pexpect
 from pylib.base import base_test_result
 from pylib.base import base_test_runner
@@ -59,22 +58,6 @@
   def InstallTestPackage(self):
     self.test_package.Install(self.device)
 
-  #override
-  def PushDataDeps(self):
-    self.device.WaitUntilFullyBooted(timeout=20)
-    self.tool.CopyFiles()
-    if os.path.exists(constants.ISOLATE_DEPS_DIR):
-      # TODO(frankf): linux_dumper_unittest_helper needs to be in the same dir
-      # as breakpad_unittests exe. Find a better way to do this.
-      if self.test_package.suite_name == 'breakpad_unittests':
-        device_dir = constants.TEST_EXECUTABLE_DIR
-      else:
-        device_dir = self.device.GetExternalStoragePath()
-      for p in os.listdir(constants.ISOLATE_DEPS_DIR):
-        self.device.PushChangedFiles(
-            os.path.join(constants.ISOLATE_DEPS_DIR, p),
-            os.path.join(device_dir, p))
-
   def _ParseTestOutput(self, p):
     """Process the test output.
 
diff --git a/build/android/pylib/instrumentation/test_runner.py b/build/android/pylib/instrumentation/test_runner.py
index 4f8cdcf..0e6c168 100644
--- a/build/android/pylib/instrumentation/test_runner.py
+++ b/build/android/pylib/instrumentation/test_runner.py
@@ -99,14 +99,15 @@
                       str(self.device))
       return
 
+    host_device_file_tuples = []
     test_data = _GetDataFilesForTestSuite(self.test_pkg.GetApkName())
     if test_data:
       # Make sure SD card is ready.
       self.device.WaitUntilFullyBooted(timeout=20)
-      for p in test_data:
-        self.device.PushChangedFiles(
-            os.path.join(constants.DIR_SOURCE_ROOT, p),
-            os.path.join(self.device.GetExternalStoragePath(), p))
+      host_device_file_tuples += [
+          (os.path.join(constants.DIR_SOURCE_ROOT, p),
+           os.path.join(self.device.GetExternalStoragePath(), p))
+          for p in test_data]
 
     # TODO(frankf): Specify test data in this file as opposed to passing
     # as command-line.
@@ -117,13 +118,15 @@
       host_test_files_path = os.path.join(constants.DIR_SOURCE_ROOT,
                                           host_src)
       if os.path.exists(host_test_files_path):
-        self.device.PushChangedFiles(
+        host_device_file_tuples += [(
             host_test_files_path,
             '%s/%s/%s' % (
                 self.device.GetExternalStoragePath(),
                 TestRunner._DEVICE_DATA_DIR,
-                dst_layer))
-    self.tool.CopyFiles()
+                dst_layer))]
+    if host_device_file_tuples:
+      self.device.PushChangedFiles(host_device_file_tuples)
+    self.tool.CopyFiles(self.device)
     TestRunner._DEVICE_HAS_TEST_FILES[str(self.device)] = True
 
   def _GetInstrumentationArgs(self):
diff --git a/build/android/pylib/ports.py b/build/android/pylib/ports.py
index 34efb52..578152c 100644
--- a/build/android/pylib/ports.py
+++ b/build/android/pylib/ports.py
@@ -9,11 +9,9 @@
 import httplib
 import logging
 import os
-import re
 import socket
 import traceback
 
-from pylib import cmd_helper
 from pylib import constants
 
 
@@ -57,7 +55,7 @@
     with open(constants.TEST_SERVER_PORT_FILE, 'r+') as fp:
       port = int(fp.read())
       ports_tried.append(port)
-      while IsHostPortUsed(port):
+      while not IsHostPortAvailable(port):
         port += 1
         ports_tried.append(port)
       if (port > constants.TEST_SERVER_PORT_LAST or
@@ -67,7 +65,7 @@
         fp.seek(0, os.SEEK_SET)
         fp.write('%d' % (port + 1))
   except Exception as e:
-    logging.info(e)
+    logging.error(e)
   finally:
     if fp_lock:
       fcntl.flock(fp_lock, fcntl.LOCK_UN)
@@ -80,25 +78,23 @@
   return port
 
 
-def IsHostPortUsed(host_port):
-  """Checks whether the specified host port is used or not.
-
-  Uses -n -P to inhibit the conversion of host/port numbers to host/port names.
+def IsHostPortAvailable(host_port):
+  """Checks whether the specified host port is available.
 
   Args:
-    host_port: Port on host we want to check.
+    host_port: Port on host to check.
 
   Returns:
-    True if the port on host is already used, otherwise returns False.
+    True if the port on host is available, otherwise returns False.
   """
-  port_info = '(\*)|(127\.0\.0\.1)|(localhost):%d' % host_port
-  # TODO(jnd): Find a better way to filter the port. Note that connecting to the
-  # socket and closing it would leave it in the TIME_WAIT state. Setting
-  # SO_LINGER on it and then closing it makes the Python HTTP server crash.
-  re_port = re.compile(port_info, re.MULTILINE)
-  if re_port.search(cmd_helper.GetCmdOutput(['lsof', '-nPi:%d' % host_port])):
+  s = socket.socket()
+  try:
+    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+    s.bind(('', host_port))
+    s.close()
     return True
-  return False
+  except socket.error:
+    return False
 
 
 def IsDevicePortUsed(device, device_port, state=''):
diff --git a/build/android/pylib/uiautomator/test_package.py b/build/android/pylib/uiautomator/test_package.py
index d8558c1..fd9120e 100644
--- a/build/android/pylib/uiautomator/test_package.py
+++ b/build/android/pylib/uiautomator/test_package.py
@@ -24,4 +24,4 @@
 
   # Override.
   def Install(self, device):
-    device.PushChangedFiles(self._jar_path, constants.TEST_EXECUTABLE_DIR)
+    device.PushChangedFiles([(self._jar_path, constants.TEST_EXECUTABLE_DIR)])
diff --git a/build/android/pylib/valgrind_tools.py b/build/android/pylib/valgrind_tools.py
index 69f351a..46cf9e3 100644
--- a/build/android/pylib/valgrind_tools.py
+++ b/build/android/pylib/valgrind_tools.py
@@ -10,12 +10,12 @@
 
 1. For tests that simply run a native process (i.e. no activity is spawned):
 
-Call tool.CopyFiles().
+Call tool.CopyFiles(device).
 Prepend test command line with tool.GetTestWrapper().
 
 2. For tests that spawn an activity:
 
-Call tool.CopyFiles().
+Call tool.CopyFiles(device).
 Call tool.SetupEnvironment().
 Run the test as usual.
 Call tool.CleanUpEnvironment().
@@ -62,7 +62,8 @@
     """
     return ''
 
-  def CopyFiles(self):
+  @classmethod
+  def CopyFiles(cls, device):
     """Copies tool-specific files to the device, create directories, etc."""
     pass
 
@@ -106,21 +107,21 @@
     # This is required because ASan is a compiler-based tool, and md5sum
     # includes instrumented code from base.
     device.old_interface.SetUtilWrapper(self.GetUtilWrapper())
+
+  @classmethod
+  def CopyFiles(cls, device):
+    """Copies ASan tools to the device."""
     libs = glob.glob(os.path.join(DIR_SOURCE_ROOT,
                                   'third_party/llvm-build/Release+Asserts/',
                                   'lib/clang/*/lib/linux/',
                                   'libclang_rt.asan-arm-android.so'))
     assert len(libs) == 1
-    self._lib = libs[0]
-
-  def CopyFiles(self):
-    """Copies ASan tools to the device."""
     subprocess.call([os.path.join(DIR_SOURCE_ROOT,
                                   'tools/android/asan/asan_device_setup.sh'),
-                     '--device', str(self._device),
-                     '--lib', self._lib,
+                     '--device', str(device),
+                     '--lib', libs[0],
                      '--extra-options', AddressSanitizerTool.EXTRA_OPTIONS])
-    self._device.WaitUntilFullyBooted()
+    device.WaitUntilFullyBooted()
 
   def GetTestWrapper(self):
     return AddressSanitizerTool.WRAPPER_NAME
@@ -164,18 +165,19 @@
     self._wrap_properties = ['wrap.com.google.android.apps.ch',
                              'wrap.org.chromium.native_test']
 
-  def CopyFiles(self):
+  @classmethod
+  def CopyFiles(cls, device):
     """Copies Valgrind tools to the device."""
-    self._device.RunShellCommand(
+    device.RunShellCommand(
         'rm -r %s; mkdir %s' % (ValgrindTool.VG_DIR, ValgrindTool.VG_DIR))
-    self._device.RunShellCommand(
+    device.RunShellCommand(
         'rm -r %s; mkdir %s' % (ValgrindTool.VGLOGS_DIR,
                                 ValgrindTool.VGLOGS_DIR))
-    files = self.GetFilesForTool()
-    for f in files:
-      self._device.PushChangedFiles(
-          os.path.join(DIR_SOURCE_ROOT, f),
+    files = cls.GetFilesForTool()
+    device.PushChangedFiles(
+        [((os.path.join(DIR_SOURCE_ROOT, f),
           os.path.join(ValgrindTool.VG_DIR, os.path.basename(f)))
+         for f in files)])
 
   def SetupEnvironment(self):
     """Sets up device environment."""
@@ -192,7 +194,8 @@
       self._device.RunShellCommand('setprop %s ""' % (prop,))
     SetChromeTimeoutScale(self._device, None)
 
-  def GetFilesForTool(self):
+  @staticmethod
+  def GetFilesForTool():
     """Returns a list of file names for the tool."""
     raise NotImplementedError()
 
@@ -211,7 +214,8 @@
   def __init__(self, device):
     super(MemcheckTool, self).__init__(device)
 
-  def GetFilesForTool(self):
+  @staticmethod
+  def GetFilesForTool():
     """Returns a list of file names for the tool."""
     return ['tools/valgrind/android/vg-chrome-wrapper.sh',
             'tools/valgrind/memcheck/suppressions.txt',
@@ -232,7 +236,8 @@
   def __init__(self, device):
     super(TSanTool, self).__init__(device)
 
-  def GetFilesForTool(self):
+  @staticmethod
+  def GetFilesForTool():
     """Returns a list of file names for the tool."""
     return ['tools/valgrind/android/vg-chrome-wrapper-tsan.sh',
             'tools/valgrind/tsan/suppressions.txt',
@@ -276,3 +281,22 @@
     print 'Unknown tool %s, available tools: %s' % (
         tool_name, ', '.join(sorted(TOOL_REGISTRY.keys())))
     sys.exit(1)
+
+def PushFilesForTool(tool_name, device):
+  """Pushes the files required for |tool_name| to |device|.
+
+  Args:
+    tool_name: Name of the tool to create.
+    device: A DeviceUtils instance.
+  """
+  if not tool_name:
+    return
+
+  clazz = TOOL_REGISTRY.get(tool_name)
+  if clazz:
+    clazz.CopyFiles(device)
+  else:
+    print 'Unknown tool %s, available tools: %s' % (
+        tool_name, ', '.join(sorted(TOOL_REGISTRY.keys())))
+    sys.exit(1)
+
diff --git a/build/android/test_runner.py b/build/android/test_runner.py
index 5303463..92c9798 100755
--- a/build/android/test_runner.py
+++ b/build/android/test_runner.py
@@ -57,6 +57,9 @@
                    const='Release', dest='build_type',
                    help=('If set, run test suites under out/Release.'
                          ' Default is env var BUILDTYPE or Debug.'))
+  group.add_option('--build-directory', dest='build_directory',
+                   help=('Path to the directory in which build files are'
+                         ' located (should not include build type)'))
   group.add_option('-c', dest='cleanup_test_files',
                    help='Cleanup test files on the device after run',
                    action='store_true')
@@ -93,6 +96,8 @@
   """Processes and handles all common options."""
   run_tests_helper.SetLogLevel(options.verbose_count)
   constants.SetBuildType(options.build_type)
+  if options.build_directory:
+    constants.SetBuildDirectory(options.build_directory)
 
 
 def AddGTestOptions(option_parser):
diff --git a/build/android/tests/multiple_proguards/src/dummy/NativeLibraries.java b/build/android/tests/multiple_proguards/src/dummy/NativeLibraries.java
index 56cd734..2f7db71 100644
--- a/build/android/tests/multiple_proguards/src/dummy/NativeLibraries.java
+++ b/build/android/tests/multiple_proguards/src/dummy/NativeLibraries.java
@@ -9,9 +9,9 @@
  * NativeLibraries to build, but doesn't include it in its jar file.
  */
 public class NativeLibraries {
-    public static boolean USE_LINKER = false;
-    public static boolean USE_LIBRARY_IN_ZIP_FILE = false;
-    public static boolean ENABLE_LINKER_TESTS = false;
+    public static boolean sUseLinker = false;
+    public static boolean sUseLibraryInZipFile = false;
+    public static boolean sEnableLinkerTests = false;
     static final String[] LIBRARIES = {};
-    static String VERSION_NUMBER = "";
+    static String sVersionNumber = "";
 }