Manual partial update from  from https://crrev.com/337502

My goal is to get the changes from https://chromium.googlesource.com/chromium/src.git/+/5ee458982c5706abf29f456b4383aea413766cea.

My strategy is to take the latest version of build/android/test_runner.py and build/android/pylib

R=viettrungluu@chromium.org, vtl

Review URL: https://codereview.chromium.org/1222313015 .
diff --git a/build/android/pylib/base/base_setup.py b/build/android/pylib/base/base_setup.py
index 0b0daf1..a416380 100644
--- a/build/android/pylib/base/base_setup.py
+++ b/build/android/pylib/base/base_setup.py
@@ -59,7 +59,5 @@
 def PushDataDeps(device, device_dir, test_options):
   valgrind_tools.PushFilesForTool(test_options.tool, device)
   if os.path.exists(constants.ISOLATE_DEPS_DIR):
-    device.PushChangedFiles([
-        (os.path.join(constants.ISOLATE_DEPS_DIR, p),
-         '%s/%s' % (device_dir, p))
-        for p in os.listdir(constants.ISOLATE_DEPS_DIR)])
+    device.PushChangedFiles([(constants.ISOLATE_DEPS_DIR, device_dir)],
+                            delete_device_stale=test_options.delete_stale_data)
diff --git a/build/android/pylib/device/battery_utils.py b/build/android/pylib/device/battery_utils.py
index df5f826..d160bbb 100644
--- a/build/android/pylib/device/battery_utils.py
+++ b/build/android/pylib/device/battery_utils.py
@@ -20,15 +20,23 @@
 _DEFAULT_TIMEOUT = 30
 _DEFAULT_RETRIES = 3
 
-_CONTROL_CHARGING_COMMANDS = [
+
+_DEVICE_PROFILES = [
   {
-    # Nexus 4
+    'name': 'Nexus 4',
     'witness_file': '/sys/module/pm8921_charger/parameters/disabled',
-    'enable_command': 'echo 0 > /sys/module/pm8921_charger/parameters/disabled',
-    'disable_command':
-        'echo 1 > /sys/module/pm8921_charger/parameters/disabled',
+    'enable_command': (
+        'echo 0 > /sys/module/pm8921_charger/parameters/disabled && '
+        'dumpsys battery reset'),
+    'disable_command': (
+        'echo 1 > /sys/module/pm8921_charger/parameters/disabled && '
+        'dumpsys battery set ac 0 && dumpsys battery set usb 0'),
+    'charge_counter': None,
+    'voltage': None,
+    'current': None,
   },
   {
+    'name': 'Nexus 5',
     # Nexus 5
     # Setting the HIZ bit of the bq24192 causes the charger to actually ignore
     # energy coming from USB. Setting the power_supply offline just updates the
@@ -36,11 +44,57 @@
     'witness_file': '/sys/kernel/debug/bq24192/INPUT_SRC_CONT',
     'enable_command': (
         'echo 0x4A > /sys/kernel/debug/bq24192/INPUT_SRC_CONT && '
-        'echo 1 > /sys/class/power_supply/usb/online'),
+        'echo 1 > /sys/class/power_supply/usb/online &&'
+        'dumpsys battery reset'),
     'disable_command': (
         'echo 0xCA > /sys/kernel/debug/bq24192/INPUT_SRC_CONT && '
         'chmod 644 /sys/class/power_supply/usb/online && '
-        'echo 0 > /sys/class/power_supply/usb/online'),
+        'echo 0 > /sys/class/power_supply/usb/online && '
+        'dumpsys battery set ac 0 && dumpsys battery set usb 0'),
+    'charge_counter': None,
+    'voltage': None,
+    'current': None,
+  },
+  {
+    'name': 'Nexus 6',
+    'witness_file': None,
+    'enable_command': (
+        'echo 1 > /sys/class/power_supply/battery/charging_enabled && '
+        'dumpsys battery reset'),
+    'disable_command': (
+        'echo 0 > /sys/class/power_supply/battery/charging_enabled && '
+        'dumpsys battery set ac 0 && dumpsys battery set usb 0'),
+    'charge_counter': (
+        '/sys/class/power_supply/max170xx_battery/charge_counter_ext'),
+    'voltage': '/sys/class/power_supply/max170xx_battery/voltage_now',
+    'current': '/sys/class/power_supply/max170xx_battery/current_now',
+  },
+  {
+    'name': 'Nexus 9',
+    'witness_file': None,
+    'enable_command': (
+        'echo Disconnected > '
+        '/sys/bus/i2c/drivers/bq2419x/0-006b/input_cable_state && '
+        'dumpsys battery reset'),
+    'disable_command': (
+        'echo Connected > '
+        '/sys/bus/i2c/drivers/bq2419x/0-006b/input_cable_state && '
+        'dumpsys battery set ac 0 && dumpsys battery set usb 0'),
+    'charge_counter': (
+        '/sys/class/power_supply/max170xx_battery/charge_counter_ext'),
+    'voltage': '/sys/class/power_supply/max170xx_battery/voltage_now',
+    'current': '/sys/class/power_supply/max170xx_battery/current_now',
+  },
+  {
+    'name': 'Nexus 10',
+    'witness_file': None,
+    'enable_command': None,
+    'disable_command': None,
+    'charge_counter': (
+        '/sys/class/power_supply/ds2784-fuelgauge/charge_counter_ext'),
+    'voltage': '/sys/class/power_supply/ds2784-fuelgauge/voltage_now',
+    'current': '/sys/class/power_supply/ds2784-fuelgauge/current_now',
+
   },
 ]
 
@@ -88,8 +142,47 @@
     self._default_retries = default_retries
 
   @decorators.WithTimeoutAndRetriesFromInstance()
+  def SupportsFuelGauge(self, timeout=None, retries=None):
+    """Detect if fuel gauge chip is present.
+
+    Args:
+      timeout: timeout in seconds
+      retries: number of retries
+
+    Returns:
+      True if known fuel gauge files are present.
+      False otherwise.
+    """
+    self._DiscoverDeviceProfile()
+    return (self._cache['profile']['enable_command'] != None
+        and self._cache['profile']['charge_counter'] != None)
+
+  @decorators.WithTimeoutAndRetriesFromInstance()
+  def GetFuelGaugeChargeCounter(self, timeout=None, retries=None):
+    """Get value of charge_counter on fuel gauge chip.
+
+    Device must have charging disabled for this, not just battery updates
+    disabled. The only device that this currently works with is the nexus 5.
+
+    Args:
+      timeout: timeout in seconds
+      retries: number of retries
+
+    Returns:
+      value of charge_counter for fuel gauge chip in units of nAh.
+
+    Raises:
+      device_errors.CommandFailedError: If fuel gauge chip not found.
+    """
+    if self.SupportsFuelGauge():
+       return int(self._device.ReadFile(
+          self._cache['profile']['charge_counter']))
+    raise device_errors.CommandFailedError(
+        'Unable to find fuel gauge.')
+
+  @decorators.WithTimeoutAndRetriesFromInstance()
   def GetNetworkData(self, package, timeout=None, retries=None):
-    """ Get network data for specific package.
+    """Get network data for specific package.
 
     Args:
       package: package name you want network data for.
@@ -127,7 +220,8 @@
 
   @decorators.WithTimeoutAndRetriesFromInstance()
   def GetPowerData(self, timeout=None, retries=None):
-    """ Get power data for device.
+    """Get power data for device.
+
     Args:
       timeout: timeout in seconds
       retries: number of retries
@@ -174,7 +268,7 @@
 
   @decorators.WithTimeoutAndRetriesFromInstance()
   def GetPackagePowerData(self, package, timeout=None, retries=None):
-    """ Get power data for particular package.
+    """Get power data for particular package.
 
     Args:
       package: Package to get power data on.
@@ -246,19 +340,15 @@
       device_errors.CommandFailedError: If method of disabling charging cannot
         be determined.
     """
-    if 'charging_config' not in self._cache:
-      for c in _CONTROL_CHARGING_COMMANDS:
-        if self._device.FileExists(c['witness_file']):
-          self._cache['charging_config'] = c
-          break
-      else:
-        raise device_errors.CommandFailedError(
-            'Unable to find charging commands.')
+    self._DiscoverDeviceProfile()
+    if not self._cache['profile']['enable_command']:
+      raise device_errors.CommandFailedError(
+          'Unable to find charging commands.')
 
     if enabled:
-      command = self._cache['charging_config']['enable_command']
+      command = self._cache['profile']['enable_command']
     else:
-      command = self._cache['charging_config']['disable_command']
+      command = self._cache['profile']['disable_command']
 
     def set_and_verify_charging():
       self._device.RunShellCommand(command, check_return=True)
@@ -269,7 +359,7 @@
   # TODO(rnephew): Make private when all use cases can use the context manager.
   @decorators.WithTimeoutAndRetriesFromInstance()
   def DisableBatteryUpdates(self, timeout=None, retries=None):
-    """ Resets battery data and makes device appear like it is not
+    """Resets battery data and makes device appear like it is not
     charging so that it will collect power data since last charge.
 
     Args:
@@ -284,27 +374,7 @@
     def battery_updates_disabled():
       return self.GetCharging() is False
 
-    if (self._device.build_version_sdk <
-        constants.ANDROID_SDK_VERSION_CODES.LOLLIPOP):
-      raise device_errors.DeviceVersionError('Device must be L or higher.')
-
-    self._device.RunShellCommand(
-        ['dumpsys', 'battery', 'set', 'usb', '1'], check_return=True)
-    self._device.RunShellCommand(
-        ['dumpsys', 'battery', 'set', 'ac', '1'], check_return=True)
-    self._device.RunShellCommand(
-        ['dumpsys', 'batterystats', '--reset'], check_return=True)
-    battery_data = self._device.RunShellCommand(
-        ['dumpsys', 'batterystats', '--charged', '--checkin'],
-        check_return=True, large_output=True)
-    ROW_TYPE_INDEX = 3
-    PWI_POWER_INDEX = 5
-    for line in battery_data:
-      l = line.split(',')
-      if (len(l) > PWI_POWER_INDEX and l[ROW_TYPE_INDEX] == 'pwi'
-          and l[PWI_POWER_INDEX] != 0):
-        raise device_errors.CommandFailedError(
-            'Non-zero pmi value found after reset.')
+    self._ClearPowerData()
     self._device.RunShellCommand(['dumpsys', 'battery', 'set', 'ac', '0'],
                                  check_return=True)
     self._device.RunShellCommand(['dumpsys', 'battery', 'set', 'usb', '0'],
@@ -314,7 +384,7 @@
   # TODO(rnephew): Make private when all use cases can use the context manager.
   @decorators.WithTimeoutAndRetriesFromInstance()
   def EnableBatteryUpdates(self, timeout=None, retries=None):
-    """ Restarts device charging so that dumpsys no longer collects power data.
+    """Restarts device charging so that dumpsys no longer collects power data.
 
     Args:
       timeout: timeout in seconds
@@ -324,11 +394,9 @@
       device_errors.DeviceVersionError: If device is not L or higher.
     """
     def battery_updates_enabled():
-      return self.GetCharging() is True
-
-    if (self._device.build_version_sdk <
-        constants.ANDROID_SDK_VERSION_CODES.LOLLIPOP):
-      raise device_errors.DeviceVersionError('Device must be L or higher.')
+      return (self.GetCharging()
+              or not bool('UPDATES STOPPED' in self._device.RunShellCommand(
+                  ['dumpsys', 'battery'], check_return=True)))
 
     self._device.RunShellCommand(['dumpsys', 'battery', 'reset'],
                                  check_return=True)
@@ -401,7 +469,127 @@
       else:
         logging.info('Current battery temperature: %s', temp)
       return int(temp) <= target_temp
-
-    logging.info('Waiting for the device to cool down to %s degrees.',
+    self.EnableBatteryUpdates()
+    logging.info('Waiting for the device to cool down to %s (0.1 C)',
                  target_temp)
     timeout_retry.WaitFor(cool_device, wait_period=wait_period)
+
+  @decorators.WithTimeoutAndRetriesFromInstance()
+  def TieredSetCharging(self, enabled, timeout=None, retries=None):
+    """Enables or disables charging on the device.
+
+    Args:
+      enabled: A boolean indicating whether charging should be enabled or
+        disabled.
+      timeout: timeout in seconds
+      retries: number of retries
+    """
+    if self.GetCharging() == enabled:
+      logging.warning('Device charging already in expected state: %s', enabled)
+      return
+
+    if enabled:
+      try:
+        self.SetCharging(enabled)
+      except device_errors.CommandFailedError:
+        logging.info('Unable to enable charging via hardware.'
+                     ' Falling back to software enabling.')
+        self.EnableBatteryUpdates()
+    else:
+      try:
+        self._ClearPowerData()
+        self.SetCharging(enabled)
+      except device_errors.CommandFailedError:
+        logging.info('Unable to disable charging via hardware.'
+                     ' Falling back to software disabling.')
+        self.DisableBatteryUpdates()
+
+  @contextlib.contextmanager
+  def PowerMeasurement(self, timeout=None, retries=None):
+    """Context manager that enables battery power collection.
+
+    Once the with block is exited, charging is resumed. Will attempt to disable
+    charging at the hardware level, and if that fails will fall back to software
+    disabling of battery updates.
+
+    Only for devices L and higher.
+
+    Example usage:
+      with PowerMeasurement():
+        browser_actions()
+        get_power_data() # report usage within this block
+      after_measurements() # Anything that runs after power
+                           # measurements are collected
+
+    Args:
+      timeout: timeout in seconds
+      retries: number of retries
+    """
+    try:
+      self.TieredSetCharging(False, timeout=timeout, retries=retries)
+      yield
+    finally:
+      self.TieredSetCharging(True, timeout=timeout, retries=retries)
+
+  def _ClearPowerData(self):
+    """Resets battery data and makes device appear like it is not
+    charging so that it will collect power data since last charge.
+
+    Returns:
+      True if power data cleared.
+      False if power data clearing is not supported (pre-L)
+
+    Raises:
+      device_errors.DeviceVersionError: If power clearing is supported,
+        but fails.
+    """
+    if (self._device.build_version_sdk <
+        constants.ANDROID_SDK_VERSION_CODES.LOLLIPOP):
+      logging.warning('Dumpsys power data only available on 5.0 and above. '
+                      'Cannot clear power data.')
+      return False
+
+    self._device.RunShellCommand(
+        ['dumpsys', 'battery', 'set', 'usb', '1'], check_return=True)
+    self._device.RunShellCommand(
+        ['dumpsys', 'battery', 'set', 'ac', '1'], check_return=True)
+    self._device.RunShellCommand(
+        ['dumpsys', 'batterystats', '--reset'], check_return=True)
+    battery_data = self._device.RunShellCommand(
+        ['dumpsys', 'batterystats', '--charged', '--checkin'],
+        check_return=True, large_output=True)
+    for line in battery_data:
+      l = line.split(',')
+      if (len(l) > _PWI_POWER_CONSUMPTION_INDEX and l[_ROW_TYPE_INDEX] == 'pwi'
+          and l[_PWI_POWER_CONSUMPTION_INDEX] != 0):
+        self._device.RunShellCommand(
+            ['dumpsys', 'battery', 'reset'], check_return=True)
+        raise device_errors.CommandFailedError(
+            'Non-zero pmi value found after reset.')
+    self._device.RunShellCommand(
+        ['dumpsys', 'battery', 'reset'], check_return=True)
+    return True
+
+  def _DiscoverDeviceProfile(self):
+    """Checks and caches device information.
+
+    Returns:
+      True if profile is found, false otherwise.
+    """
+
+    if 'profile' in self._cache:
+      return True
+    for profile in _DEVICE_PROFILES:
+      if self._device.product_model == profile['name']:
+        self._cache['profile'] = profile
+        return True
+    self._cache['profile'] = {
+        'name': None,
+        'witness_file': None,
+        'enable_command': None,
+        'disable_command': None,
+        'charge_counter': None,
+        'voltage': None,
+        'current': None,
+    }
+    return False
diff --git a/build/android/pylib/device/battery_utils_test.py b/build/android/pylib/device/battery_utils_test.py
index 51fae17..b968fa6 100755
--- a/build/android/pylib/device/battery_utils_test.py
+++ b/build/android/pylib/device/battery_utils_test.py
@@ -40,6 +40,43 @@
 
 class BatteryUtilsTest(mock_calls.TestCase):
 
+  _NEXUS_5 = {
+    'name': 'Nexus 5',
+    'witness_file': '/sys/kernel/debug/bq24192/INPUT_SRC_CONT',
+    'enable_command': (
+        'echo 0x4A > /sys/kernel/debug/bq24192/INPUT_SRC_CONT && '
+        'echo 1 > /sys/class/power_supply/usb/online'),
+    'disable_command': (
+        'echo 0xCA > /sys/kernel/debug/bq24192/INPUT_SRC_CONT && '
+        'chmod 644 /sys/class/power_supply/usb/online && '
+        'echo 0 > /sys/class/power_supply/usb/online'),
+    'charge_counter': None,
+    'voltage': None,
+    'current': None,
+  }
+
+  _NEXUS_6 = {
+    'name': 'Nexus 6',
+    'witness_file': None,
+    'enable_command': None,
+    'disable_command': None,
+    'charge_counter': (
+        '/sys/class/power_supply/max170xx_battery/charge_counter_ext'),
+    'voltage': '/sys/class/power_supply/max170xx_battery/voltage_now',
+    'current': '/sys/class/power_supply/max170xx_battery/current_now',
+  }
+
+  _NEXUS_10 = {
+    'name': 'Nexus 10',
+    'witness_file': None,
+    'enable_command': None,
+    'disable_command': None,
+    'charge_counter': (
+        '/sys/class/power_supply/ds2784-fuelgauge/charge_counter_ext'),
+    'voltage': '/sys/class/power_supply/ds2784-fuelgauge/voltage_now',
+    'current': '/sys/class/power_supply/ds2784-fuelgauge/current_now',
+  }
+
   def ShellError(self, output=None, status=1):
     def action(cmd, *args, **kwargs):
       raise device_errors.AdbShellCommandFailedError(
@@ -76,8 +113,8 @@
 
   @mock.patch('time.sleep', mock.Mock())
   def testSetCharging_enabled(self):
+    self.battery._cache['profile'] = self._NEXUS_5
     with self.assertCalls(
-        (self.call.device.FileExists(mock.ANY), True),
         (self.call.device.RunShellCommand(mock.ANY, check_return=True), []),
         (self.call.battery.GetCharging(), False),
         (self.call.device.RunShellCommand(mock.ANY, check_return=True), []),
@@ -85,16 +122,16 @@
       self.battery.SetCharging(True)
 
   def testSetCharging_alreadyEnabled(self):
+    self.battery._cache['profile'] = self._NEXUS_5
     with self.assertCalls(
-        (self.call.device.FileExists(mock.ANY), True),
         (self.call.device.RunShellCommand(mock.ANY, check_return=True), []),
         (self.call.battery.GetCharging(), True)):
       self.battery.SetCharging(True)
 
   @mock.patch('time.sleep', mock.Mock())
   def testSetCharging_disabled(self):
+    self.battery._cache['profile'] = self._NEXUS_5
     with self.assertCalls(
-        (self.call.device.FileExists(mock.ANY), True),
         (self.call.device.RunShellCommand(mock.ANY, check_return=True), []),
         (self.call.battery.GetCharging(), True),
         (self.call.device.RunShellCommand(mock.ANY, check_return=True), []),
@@ -105,20 +142,12 @@
 class BatteryUtilsSetBatteryMeasurementTest(BatteryUtilsTest):
 
   @mock.patch('time.sleep', mock.Mock())
-  def testBatteryMeasurement(self):
+  def testBatteryMeasurementWifi(self):
     with self.assertCalls(
         (self.call.device.RunShellCommand(
             mock.ANY, retries=0, single_line=True,
             timeout=10, check_return=True), '22'),
-        (self.call.device.RunShellCommand(
-            ['dumpsys', 'battery', 'set', 'usb', '1'], check_return=True), []),
-        (self.call.device.RunShellCommand(
-            ['dumpsys', 'battery', 'set', 'ac', '1'], check_return=True), []),
-        (self.call.device.RunShellCommand(
-            ['dumpsys', 'batterystats', '--reset'], check_return=True), []),
-        (self.call.device.RunShellCommand(
-            ['dumpsys', 'batterystats', '--charged', '--checkin'],
-            check_return=True, large_output=True), []),
+        (self.call.battery._ClearPowerData(), True),
         (self.call.device.RunShellCommand(
             ['dumpsys', 'battery', 'set', 'ac', '0'], check_return=True), []),
         (self.call.device.RunShellCommand(
@@ -126,6 +155,32 @@
         (self.call.battery.GetCharging(), False),
         (self.call.device.RunShellCommand(
             ['dumpsys', 'battery', 'reset'], check_return=True), []),
+        (self.call.battery.GetCharging(), False),
+        (self.call.device.RunShellCommand(
+            ['dumpsys', 'battery'], check_return=True), ['UPDATES STOPPED']),
+        (self.call.battery.GetCharging(), False),
+        (self.call.device.RunShellCommand(
+            ['dumpsys', 'battery'], check_return=True), [])):
+      with self.battery.BatteryMeasurement():
+        pass
+
+  @mock.patch('time.sleep', mock.Mock())
+  def testBatteryMeasurementUsb(self):
+    with self.assertCalls(
+        (self.call.device.RunShellCommand(
+            mock.ANY, retries=0, single_line=True,
+            timeout=10, check_return=True), '22'),
+        (self.call.battery._ClearPowerData(), True),
+        (self.call.device.RunShellCommand(
+            ['dumpsys', 'battery', 'set', 'ac', '0'], check_return=True), []),
+        (self.call.device.RunShellCommand(
+            ['dumpsys', 'battery', 'set', 'usb', '0'], check_return=True), []),
+        (self.call.battery.GetCharging(), False),
+        (self.call.device.RunShellCommand(
+            ['dumpsys', 'battery', 'reset'], check_return=True), []),
+        (self.call.battery.GetCharging(), False),
+        (self.call.device.RunShellCommand(
+            ['dumpsys', 'battery'], check_return=True), ['UPDATES STOPPED']),
         (self.call.battery.GetCharging(), True)):
       with self.battery.BatteryMeasurement():
         pass
@@ -311,21 +366,209 @@
       self.battery._cache.clear()
       self.assertEqual(self.battery.GetNetworkData('test_package1'), (1,2))
 
+
 class BatteryUtilsLetBatteryCoolToTemperatureTest(BatteryUtilsTest):
 
   @mock.patch('time.sleep', mock.Mock())
   def testLetBatteryCoolToTemperature_startUnder(self):
     with self.assertCalls(
+        (self.call.battery.EnableBatteryUpdates(), []),
         (self.call.battery.GetBatteryInfo(), {'temperature': '500'})):
       self.battery.LetBatteryCoolToTemperature(600)
 
   @mock.patch('time.sleep', mock.Mock())
   def testLetBatteryCoolToTemperature_startOver(self):
     with self.assertCalls(
+        (self.call.battery.EnableBatteryUpdates(), []),
         (self.call.battery.GetBatteryInfo(), {'temperature': '500'}),
         (self.call.battery.GetBatteryInfo(), {'temperature': '400'})):
       self.battery.LetBatteryCoolToTemperature(400)
 
+class BatteryUtilsSupportsFuelGaugeTest(BatteryUtilsTest):
+
+  def testSupportsFuelGauge_false(self):
+    self.battery._cache['profile'] = self._NEXUS_5
+    self.assertFalse(self.battery.SupportsFuelGauge())
+
+  def testSupportsFuelGauge_trueMax(self):
+    self.battery._cache['profile'] = self._NEXUS_6
+    # TODO(rnephew): Change this to assertTrue when we have support for
+    # disabling hardware charging on nexus 6.
+    self.assertFalse(self.battery.SupportsFuelGauge())
+
+  def testSupportsFuelGauge_trueDS(self):
+    self.battery._cache['profile'] = self._NEXUS_10
+    # TODO(rnephew): Change this to assertTrue when we have support for
+    # disabling hardware charging on nexus 10.
+    self.assertFalse(self.battery.SupportsFuelGauge())
+
+
+class BatteryUtilsGetFuelGaugeChargeCounterTest(BatteryUtilsTest):
+
+  def testGetFuelGaugeChargeCounter_noFuelGauge(self):
+    self.battery._cache['profile'] = self._NEXUS_5
+    with self.assertRaises(device_errors.CommandFailedError):
+        self.battery.GetFuelGaugeChargeCounter()
+
+  def testGetFuelGaugeChargeCounter_fuelGaugePresent(self):
+    self.battery._cache['profile']= self._NEXUS_6
+    with self.assertCalls(
+        (self.call.battery.SupportsFuelGauge(), True),
+        (self.call.device.ReadFile(mock.ANY), '123')):
+      self.assertEqual(self.battery.GetFuelGaugeChargeCounter(), 123)
+
+
+class BatteryUtilsTieredSetCharging(BatteryUtilsTest):
+
+  @mock.patch('time.sleep', mock.Mock())
+  def testTieredSetCharging_softwareSetTrue(self):
+    self.battery._cache['profile'] = self._NEXUS_6
+    with self.assertCalls(
+        (self.call.battery.GetCharging(), False),
+        (self.call.device.RunShellCommand(
+            ['dumpsys', 'battery', 'reset'], check_return=True), []),
+        (self.call.battery.GetCharging(), False),
+        (self.call.device.RunShellCommand(
+            ['dumpsys', 'battery'], check_return=True), ['UPDATES STOPPED']),
+        (self.call.battery.GetCharging(), True)):
+      self.battery.TieredSetCharging(True)
+
+  @mock.patch('time.sleep', mock.Mock())
+  def testTieredSetCharging_softwareSetFalse(self):
+    self.battery._cache['profile'] = self._NEXUS_6
+    with self.assertCalls(
+        (self.call.battery.GetCharging(), True),
+        (self.call.battery._ClearPowerData(), True),
+        (self.call.battery._ClearPowerData(), True),
+        (self.call.device.RunShellCommand(
+            ['dumpsys', 'battery', 'set', 'ac', '0'], check_return=True), []),
+        (self.call.device.RunShellCommand(
+            ['dumpsys', 'battery', 'set', 'usb', '0'], check_return=True), []),
+        (self.call.battery.GetCharging(), False)):
+      self.battery.TieredSetCharging(False)
+
+  @mock.patch('time.sleep', mock.Mock())
+  def testTieredSetCharging_hardwareSetTrue(self):
+    self.battery._cache['profile'] = self._NEXUS_5
+    with self.assertCalls(
+        (self.call.battery.GetCharging(), False),
+        (self.call.battery.SetCharging(True))):
+      self.battery.TieredSetCharging(True)
+
+  @mock.patch('time.sleep', mock.Mock())
+  def testTieredSetCharging_hardwareSetFalse(self):
+    self.battery._cache['profile'] = self._NEXUS_5
+    with self.assertCalls(
+        (self.call.battery.GetCharging(), True),
+        (self.call.battery._ClearPowerData(), True),
+        (self.call.battery.SetCharging(False))):
+      self.battery.TieredSetCharging(False)
+
+  def testTieredSetCharging_expectedStateAlreadyTrue(self):
+    with self.assertCalls((self.call.battery.GetCharging(), True)):
+      self.battery.TieredSetCharging(True)
+
+  def testTieredSetCharging_expectedStateAlreadyFalse(self):
+    with self.assertCalls((self.call.battery.GetCharging(), False)):
+      self.battery.TieredSetCharging(False)
+
+
+class BatteryUtilsPowerMeasurement(BatteryUtilsTest):
+
+  def testPowerMeasurement_hardware(self):
+    self.battery._cache['profile'] = self._NEXUS_5
+    with self.assertCalls(
+        (self.call.battery.GetCharging(), True),
+        (self.call.battery._ClearPowerData(), True),
+        (self.call.battery.SetCharging(False)),
+        (self.call.battery.GetCharging(), False),
+        (self.call.battery.SetCharging(True))):
+      with self.battery.PowerMeasurement():
+        pass
+
+  @mock.patch('time.sleep', mock.Mock())
+  def testPowerMeasurement_software(self):
+    self.battery._cache['profile'] = self._NEXUS_6
+    with self.assertCalls(
+        (self.call.battery.GetCharging(), True),
+        (self.call.battery._ClearPowerData(), True),
+        (self.call.battery._ClearPowerData(), True),
+        (self.call.device.RunShellCommand(
+            ['dumpsys', 'battery', 'set', 'ac', '0'], check_return=True), []),
+        (self.call.device.RunShellCommand(
+            ['dumpsys', 'battery', 'set', 'usb', '0'], check_return=True), []),
+        (self.call.battery.GetCharging(), False),
+        (self.call.battery.GetCharging(), False),
+        (self.call.device.RunShellCommand(
+            ['dumpsys', 'battery', 'reset'], check_return=True), []),
+        (self.call.battery.GetCharging(), False),
+        (self.call.device.RunShellCommand(
+            ['dumpsys', 'battery'], check_return=True), ['UPDATES STOPPED']),
+        (self.call.battery.GetCharging(), True)):
+      with self.battery.PowerMeasurement():
+        pass
+
+
+class BatteryUtilsDiscoverDeviceProfile(BatteryUtilsTest):
+
+  def testDiscoverDeviceProfile_known(self):
+    with self.assertCalls(
+        (self.call.adb.Shell('getprop ro.product.model'), "Nexus 4")):
+      self.battery._DiscoverDeviceProfile()
+      self.assertEqual(self.battery._cache['profile']['name'], "Nexus 4")
+
+  def testDiscoverDeviceProfile_unknown(self):
+    with self.assertCalls(
+        (self.call.adb.Shell('getprop ro.product.model'), "Other")):
+      self.battery._DiscoverDeviceProfile()
+      self.assertEqual(self.battery._cache['profile']['name'], None)
+
+
+class BatteryUtilsClearPowerData(BatteryUtilsTest):
+
+  def testClearPowerData_preL(self):
+    with self.assertCalls(
+        (self.call.device.RunShellCommand(mock.ANY, retries=0,
+            single_line=True, timeout=10, check_return=True), '20')):
+      self.assertFalse(self.battery._ClearPowerData())
+
+  def testClearPowerData_clearedL(self):
+    with self.assertCalls(
+        (self.call.device.RunShellCommand(mock.ANY, retries=0,
+            single_line=True, timeout=10, check_return=True), '22'),
+        (self.call.device.RunShellCommand(
+            ['dumpsys', 'battery', 'set', 'usb', '1'], check_return=True), []),
+        (self.call.device.RunShellCommand(
+            ['dumpsys', 'battery', 'set', 'ac', '1'], check_return=True), []),
+        (self.call.device.RunShellCommand(
+            ['dumpsys', 'batterystats', '--reset'], check_return=True), []),
+        (self.call.device.RunShellCommand(
+            ['dumpsys', 'batterystats', '--charged', '--checkin'],
+            check_return=True, large_output=True), []),
+        (self.call.device.RunShellCommand(
+            ['dumpsys', 'battery', 'reset'], check_return=True), [])):
+      self.assertTrue(self.battery._ClearPowerData())
+
+  def testClearPowerData_notClearedL(self):
+    with self.assertCalls(
+        (self.call.device.RunShellCommand(mock.ANY, retries=0,
+            single_line=True, timeout=10, check_return=True), '22'),
+        (self.call.device.RunShellCommand(
+            ['dumpsys', 'battery', 'set', 'usb', '1'], check_return=True), []),
+        (self.call.device.RunShellCommand(
+            ['dumpsys', 'battery', 'set', 'ac', '1'], check_return=True), []),
+        (self.call.device.RunShellCommand(
+            ['dumpsys', 'batterystats', '--reset'], check_return=True), []),
+        (self.call.device.RunShellCommand(
+            ['dumpsys', 'batterystats', '--charged', '--checkin'],
+            check_return=True, large_output=True),
+            ['9,1000,l,pwi,uid,0.0327']),
+        (self.call.device.RunShellCommand(
+            ['dumpsys', 'battery', 'reset'], check_return=True), [])):
+      with self.assertRaises(device_errors.CommandFailedError):
+        self.battery._ClearPowerData()
+
+
 if __name__ == '__main__':
   logging.getLogger().setLevel(logging.DEBUG)
   unittest.main(verbosity=2)
diff --git a/build/android/pylib/device/device_utils.py b/build/android/pylib/device/device_utils.py
index c8b4718..3a6563e 100644
--- a/build/android/pylib/device/device_utils.py
+++ b/build/android/pylib/device/device_utils.py
@@ -26,6 +26,7 @@
 from pylib import cmd_helper
 from pylib import constants
 from pylib import device_signal
+from pylib.constants import keyevent
 from pylib.device import adb_wrapper
 from pylib.device import decorators
 from pylib.device import device_blacklist
@@ -33,6 +34,7 @@
 from pylib.device import intent
 from pylib.device import logcat_monitor
 from pylib.device.commands import install_commands
+from pylib.sdk import split_select
 from pylib.utils import apk_helper
 from pylib.utils import base_error
 from pylib.utils import device_temp_file
@@ -135,6 +137,8 @@
 
   _MAX_ADB_COMMAND_LENGTH = 512
   _MAX_ADB_OUTPUT_LENGTH = 32768
+  _LAUNCHER_FOCUSED_RE = re.compile(
+      '\s*mCurrentFocus.*(Launcher|launcher).*')
   _VALID_SHELL_VARIABLE = re.compile('^[a-zA-Z_][a-zA-Z0-9_]*$')
 
   # Property in /data/local.prop that controls Java assertions.
@@ -339,14 +343,14 @@
     return value
 
   @decorators.WithTimeoutAndRetriesFromInstance()
-  def GetApplicationPath(self, package, timeout=None, retries=None):
-    """Get the path of the installed apk on the device for the given package.
+  def GetApplicationPaths(self, package, timeout=None, retries=None):
+    """Get the paths of the installed apks on the device for the given package.
 
     Args:
       package: Name of the package.
 
     Returns:
-      Path to the apk on the device if it exists, None otherwise.
+      List of paths to the apks on the device for the given package.
     """
     # 'pm path' is liable to incorrectly exit with a nonzero number starting
     # in Lollipop.
@@ -354,14 +358,37 @@
     # released to put an upper bound on this.
     should_check_return = (self.build_version_sdk <
                            constants.ANDROID_SDK_VERSION_CODES.LOLLIPOP)
-    output = self.RunShellCommand(['pm', 'path', package], single_line=True,
-                                  check_return=should_check_return)
-    if not output:
-      return None
-    if not output.startswith('package:'):
-      raise device_errors.CommandFailedError('pm path returned: %r' % output,
-                                             str(self))
-    return output[len('package:'):]
+    output = self.RunShellCommand(
+        ['pm', 'path', package], check_return=should_check_return)
+    apks = []
+    for line in output:
+      if not line.startswith('package:'):
+        raise device_errors.CommandFailedError(
+            'pm path returned: %r' % '\n'.join(output), str(self))
+      apks.append(line[len('package:'):])
+    return apks
+
+  @decorators.WithTimeoutAndRetriesFromInstance()
+  def GetApplicationDataDirectory(self, package, timeout=None, retries=None):
+    """Get the data directory on the device for the given package.
+
+    Args:
+      package: Name of the package.
+
+    Returns:
+      The package's data directory, or None if the package doesn't exist on the
+      device.
+    """
+    try:
+      output = self._RunPipedShellCommand(
+          'pm dump %s | grep dataDir=' % cmd_helper.SingleQuote(package))
+      for line in output:
+        _, _, dataDir = line.partition('dataDir=')
+        if dataDir:
+          return dataDir
+    except device_errors.CommandFailedError:
+      logging.exception('Could not find data directory for %s', package)
+    return None
 
   @decorators.WithTimeoutAndRetriesFromInstance()
   def WaitUntilFullyBooted(self, wifi=False, timeout=None, retries=None):
@@ -391,7 +418,7 @@
 
     def pm_ready():
       try:
-        return self.GetApplicationPath('android')
+        return self.GetApplicationPaths('android')
       except device_errors.CommandFailedError:
         return False
 
@@ -461,9 +488,15 @@
       DeviceUnreachableError on missing device.
     """
     package_name = apk_helper.GetPackageName(apk_path)
-    device_path = self.GetApplicationPath(package_name)
-    if device_path is not None:
-      should_install = bool(self._GetChangedFilesImpl(apk_path, device_path))
+    device_paths = self.GetApplicationPaths(package_name)
+    if device_paths:
+      if len(device_paths) > 1:
+        logging.warning(
+            'Installing single APK (%s) when split APKs (%s) are currently '
+            'installed.', apk_path, ' '.join(device_paths))
+      (files_to_push, _) = self._GetChangedAndStaleFiles(
+          apk_path, device_paths[0])
+      should_install = bool(files_to_push)
       if should_install and not reinstall:
         self.adb.Uninstall(package_name)
     else:
@@ -471,6 +504,62 @@
     if should_install:
       self.adb.Install(apk_path, reinstall=reinstall)
 
+  @decorators.WithTimeoutAndRetriesDefaults(
+      INSTALL_DEFAULT_TIMEOUT,
+      INSTALL_DEFAULT_RETRIES)
+  def InstallSplitApk(self, base_apk, split_apks, reinstall=False,
+                      timeout=None, retries=None):
+    """Install a split APK.
+
+    Noop if all of the APK splits are already installed.
+
+    Args:
+      base_apk: A string of the path to the base APK.
+      split_apks: A list of strings of paths of all of the APK splits.
+      reinstall: A boolean indicating if we should keep any existing app data.
+      timeout: timeout in seconds
+      retries: number of retries
+
+    Raises:
+      CommandFailedError if the installation fails.
+      CommandTimeoutError if the installation times out.
+      DeviceUnreachableError on missing device.
+      DeviceVersionError if device SDK is less than Android L.
+    """
+    self._CheckSdkLevel(constants.ANDROID_SDK_VERSION_CODES.LOLLIPOP)
+
+    all_apks = [base_apk] + split_select.SelectSplits(
+        self, base_apk, split_apks)
+    package_name = apk_helper.GetPackageName(base_apk)
+    device_apk_paths = self.GetApplicationPaths(package_name)
+
+    if device_apk_paths:
+      partial_install_package = package_name
+      device_checksums = md5sum.CalculateDeviceMd5Sums(device_apk_paths, self)
+      host_checksums = md5sum.CalculateHostMd5Sums(all_apks)
+      apks_to_install = [k for (k, v) in host_checksums.iteritems()
+                         if v not in device_checksums.values()]
+      if apks_to_install and not reinstall:
+        self.adb.Uninstall(package_name)
+        partial_install_package = None
+        apks_to_install = all_apks
+    else:
+      partial_install_package = None
+      apks_to_install = all_apks
+    if apks_to_install:
+      self.adb.InstallMultiple(
+          apks_to_install, partial=partial_install_package, reinstall=reinstall)
+
+  def _CheckSdkLevel(self, required_sdk_level):
+    """Raises an exception if the device does not have the required SDK level.
+    """
+    if self.build_version_sdk < required_sdk_level:
+      raise device_errors.DeviceVersionError(
+          ('Requires SDK level %s, device is SDK level %s' %
+           (required_sdk_level, self.build_version_sdk)),
+           device_serial=self.adb.GetDeviceSerial())
+
+
   @decorators.WithTimeoutAndRetriesFromInstance()
   def RunShellCommand(self, cmd, check_return=False, cwd=None, env=None,
                       as_root=False, single_line=False, large_output=False,
@@ -557,8 +646,8 @@
       if large_output_mode:
         with device_temp_file.DeviceTempFile(self.adb) as large_output_file:
           cmd = '%s > %s' % (cmd, large_output_file.name)
-          logging.info('Large output mode enabled. Will write output to device '
-                       'and read results from file.')
+          logging.debug('Large output mode enabled. Will write output to '
+                        'device and read results from file.')
           handle_large_command(cmd)
           return self.ReadFile(large_output_file.name, force_pull=True)
       else:
@@ -714,7 +803,7 @@
     for k, v in extras.iteritems():
       cmd.extend(['-e', str(k), str(v)])
     cmd.append(component)
-    return self.RunShellCommand(cmd, check_return=True)
+    return self.RunShellCommand(cmd, check_return=True, large_output=True)
 
   @decorators.WithTimeoutAndRetriesFromInstance()
   def BroadcastIntent(self, intent_obj, timeout=None, retries=None):
@@ -734,7 +823,10 @@
 
   @decorators.WithTimeoutAndRetriesFromInstance()
   def GoHome(self, timeout=None, retries=None):
-    """Return to the home screen.
+    """Return to the home screen and obtain launcher focus.
+
+    This command launches the home screen and attempts to obtain
+    launcher focus until the timeout is reached.
 
     Args:
       timeout: timeout in seconds
@@ -744,11 +836,30 @@
       CommandTimeoutError on timeout.
       DeviceUnreachableError on missing device.
     """
+    def is_launcher_focused():
+      output = self.RunShellCommand(['dumpsys', 'window', 'windows'],
+                                    check_return=True, large_output=True)
+      return any(self._LAUNCHER_FOCUSED_RE.match(l) for l in output)
+
+    def dismiss_popups():
+      # There is a dialog present; attempt to get rid of it.
+      # Not all dialogs can be dismissed with back.
+      self.SendKeyEvent(keyevent.KEYCODE_ENTER)
+      self.SendKeyEvent(keyevent.KEYCODE_BACK)
+      return is_launcher_focused()
+
+    # If Home is already focused, return early to avoid unnecessary work.
+    if is_launcher_focused():
+      return
+
     self.StartActivity(
         intent.Intent(action='android.intent.action.MAIN',
                       category='android.intent.category.HOME'),
         blocking=True)
 
+    if not is_launcher_focused():
+      timeout_retry.WaitFor(dismiss_popups, wait_period=1)
+
   @decorators.WithTimeoutAndRetriesFromInstance()
   def ForceStop(self, package, timeout=None, retries=None):
     """Close the application.
@@ -782,7 +893,7 @@
     # may never return.
     if ((self.build_version_sdk >=
          constants.ANDROID_SDK_VERSION_CODES.JELLY_BEAN_MR2)
-        or self.GetApplicationPath(package)):
+        or self.GetApplicationPaths(package)):
       self.RunShellCommand(['pm', 'clear', package], check_return=True)
 
   @decorators.WithTimeoutAndRetriesFromInstance()
@@ -810,9 +921,14 @@
       PUSH_CHANGED_FILES_DEFAULT_TIMEOUT,
       PUSH_CHANGED_FILES_DEFAULT_RETRIES)
   def PushChangedFiles(self, host_device_tuples, timeout=None,
-                       retries=None):
+                       retries=None, delete_device_stale=False):
     """Push files to the device, skipping files that don't need updating.
 
+    When a directory is pushed, it is traversed recursively on the host and
+    all files in it are pushed to the device as needed.
+    Additionally, if delete_device_stale option is True,
+    files that exist on the device but don't exist on the host are deleted.
+
     Args:
       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
@@ -820,6 +936,7 @@
         an absolute path of the destination on the device.
       timeout: timeout in seconds
       retries: number of retries
+      delete_device_stale: option to delete stale files on device
 
     Raises:
       CommandFailedError on failure.
@@ -827,15 +944,72 @@
       DeviceUnreachableError on missing device.
     """
 
-    files = []
+    all_changed_files = []
+    all_stale_files = []
     for h, d in host_device_tuples:
       if os.path.isdir(h):
         self.RunShellCommand(['mkdir', '-p', d], check_return=True)
-      files += self._GetChangedFilesImpl(h, d)
+      (changed_files, stale_files) = self._GetChangedAndStaleFiles(h, d)
+      all_changed_files += changed_files
+      all_stale_files += stale_files
 
-    if not files:
+    if delete_device_stale:
+      self.RunShellCommand(['rm', '-f'] + all_stale_files,
+                             check_return=True)
+
+    if not all_changed_files:
       return
 
+    self._PushFilesImpl(host_device_tuples, all_changed_files)
+
+  def _GetChangedAndStaleFiles(self, host_path, device_path):
+    """Get files to push and delete
+
+    Args:
+      host_path: an absolute path of a file or directory on the host
+      device_path: an absolute path of a file or directory on the device
+
+    Returns:
+      a two-element tuple
+      1st element: a list of (host_files_path, device_files_path) tuples to push
+      2nd element: a list of stale files under device_path
+    """
+    real_host_path = os.path.realpath(host_path)
+    try:
+      real_device_path = self.RunShellCommand(
+          ['realpath', device_path], single_line=True, check_return=True)
+    except device_errors.CommandFailedError:
+      real_device_path = None
+    if not real_device_path:
+      return ([(host_path, device_path)], [])
+
+    try:
+      host_checksums = md5sum.CalculateHostMd5Sums([real_host_path])
+      device_checksums = md5sum.CalculateDeviceMd5Sums(
+        [real_device_path], self)
+    except EnvironmentError as e:
+      logging.warning('Error calculating md5: %s', e)
+      return ([(host_path, device_path)], [])
+
+    if os.path.isfile(host_path):
+      host_checksum = host_checksums.get(real_host_path)
+      device_checksum = device_checksums.get(real_device_path)
+      if host_checksum != device_checksum:
+        return ([(host_path, device_path)], [])
+      else:
+        return ([], [])
+    else:
+      to_push = []
+      for host_abs_path, host_checksum in host_checksums.iteritems():
+        device_abs_path = '%s/%s' % (
+            real_device_path, os.path.relpath(host_abs_path, real_host_path))
+        device_checksum = device_checksums.pop(device_abs_path, None)
+        if device_checksum != host_checksum:
+          to_push.append((host_abs_path, device_abs_path))
+      to_delete = device_checksums.keys()
+      return (to_push, to_delete)
+
+  def _PushFilesImpl(self, host_device_tuples, files):
     size = sum(host_utils.GetRecursiveDiskUsage(h) for h, _ in files)
     file_count = len(files)
     dir_size = sum(host_utils.GetRecursiveDiskUsage(h)
@@ -866,44 +1040,6 @@
           ['chmod', '-R', '777'] + [d for _, d in host_device_tuples],
           as_root=True, check_return=True)
 
-  def _GetChangedFilesImpl(self, host_path, device_path):
-    real_host_path = os.path.realpath(host_path)
-    try:
-      real_device_path = self.RunShellCommand(
-          ['realpath', device_path], single_line=True, check_return=True)
-    except device_errors.CommandFailedError:
-      real_device_path = None
-    if not real_device_path:
-      return [(host_path, device_path)]
-
-    try:
-      host_checksums = md5sum.CalculateHostMd5Sums([real_host_path])
-      device_paths_to_md5 = (
-          real_device_path if os.path.isfile(real_host_path)
-          else ('%s/%s' % (real_device_path, os.path.relpath(p, real_host_path))
-                for p in host_checksums.iterkeys()))
-      device_checksums = md5sum.CalculateDeviceMd5Sums(
-          device_paths_to_md5, self)
-    except EnvironmentError as e:
-      logging.warning('Error calculating md5: %s', e)
-      return [(host_path, device_path)]
-
-    if os.path.isfile(host_path):
-      host_checksum = host_checksums.get(real_host_path)
-      device_checksum = device_checksums.get(real_device_path)
-      if host_checksum != device_checksum:
-        return [(host_path, device_path)]
-      else:
-        return []
-    else:
-      to_push = []
-      for host_abs_path, host_checksum in host_checksums.iteritems():
-        device_abs_path = '%s/%s' % (
-            real_device_path, os.path.relpath(host_abs_path, real_host_path))
-        if (device_checksums.get(device_abs_path) != host_checksum):
-          to_push.append((host_abs_path, device_abs_path))
-      return to_push
-
   def _InstallCommands(self):
     if self._commands_installed is None:
       try:
@@ -1244,6 +1380,29 @@
     else:
       return False
 
+  @property
+  def language(self):
+    """Returns the language setting on the device."""
+    return self.GetProp('persist.sys.language', cache=False)
+
+  @property
+  def country(self):
+    """Returns the country setting on the device."""
+    return self.GetProp('persist.sys.country', cache=False)
+
+  @property
+  def screen_density(self):
+    """Returns the screen density of the device."""
+    DPI_TO_DENSITY = {
+      120: 'ldpi',
+      160: 'mdpi',
+      240: 'hdpi',
+      320: 'xhdpi',
+      480: 'xxhdpi',
+      640: 'xxxhdpi',
+    }
+    dpi = int(self.GetProp('ro.sf.lcd_density', cache=True))
+    return DPI_TO_DENSITY.get(dpi, 'tvdpi')
 
   @property
   def build_description(self):
@@ -1585,4 +1744,3 @@
 
     return [cls(adb) for adb in adb_wrapper.AdbWrapper.Devices()
             if not blacklisted(adb)]
-
diff --git a/build/android/pylib/device/device_utils_device_test.py b/build/android/pylib/device/device_utils_device_test.py
new file mode 100755
index 0000000..daae2b6
--- /dev/null
+++ b/build/android/pylib/device/device_utils_device_test.py
@@ -0,0 +1,211 @@
+#!/usr/bin/env python
+# Copyright 2015 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.
+
+"""
+Unit tests for the contents of device_utils.py (mostly DeviceUtils).
+The test will invoke real devices
+"""
+
+import os
+import tempfile
+import unittest
+
+from pylib import cmd_helper
+from pylib import constants
+from pylib.device import adb_wrapper
+from pylib.device import device_utils
+from pylib.utils import md5sum
+
+_OLD_CONTENTS = "foo"
+_NEW_CONTENTS = "bar"
+_DEVICE_DIR = "/data/local/tmp/device_utils_test"
+_SUB_DIR = "sub"
+_SUB_DIR1 = "sub1"
+_SUB_DIR2 = "sub2"
+
+class DeviceUtilsPushDeleteFilesTest(unittest.TestCase):
+
+  def setUp(self):
+    devices = adb_wrapper.AdbWrapper.Devices()
+    assert devices, 'A device must be attached'
+    self.adb = devices[0]
+    self.adb.WaitForDevice()
+    self.device = device_utils.DeviceUtils(
+        self.adb, default_timeout=10, default_retries=0)
+    default_build_type = os.environ.get('BUILDTYPE', 'Debug')
+    constants.SetBuildType(default_build_type)
+
+  @staticmethod
+  def _MakeTempFile(contents):
+    """Make a temporary file with the given contents.
+
+    Args:
+      contents: string to write to the temporary file.
+
+    Returns:
+      the tuple contains the absolute path to the file and the file name
+    """
+    fi, path = tempfile.mkstemp(text=True)
+    with os.fdopen(fi, 'w') as f:
+      f.write(contents)
+    file_name = os.path.basename(path)
+    return (path, file_name)
+
+  @staticmethod
+  def _MakeTempFileGivenDir(directory, contents):
+    """Make a temporary file under the given directory
+    with the given contents
+
+    Args:
+      directory: the temp directory to create the file
+      contents: string to write to the temp file
+
+    Returns:
+      the list contains the absolute path to the file and the file name
+    """
+    fi, path = tempfile.mkstemp(dir=directory, text=True)
+    with os.fdopen(fi, 'w') as f:
+      f.write(contents)
+    file_name = os.path.basename(path)
+    return (path, file_name)
+
+  @staticmethod
+  def _ChangeTempFile(path, contents):
+    with os.open(path, 'w') as f:
+      f.write(contents)
+
+  @staticmethod
+  def _DeleteTempFile(path):
+    os.remove(path)
+
+  def testPushChangedFiles_noFileChange(self):
+    (host_file_path, file_name) = self._MakeTempFile(_OLD_CONTENTS)
+    device_file_path = "%s/%s" % (_DEVICE_DIR, file_name)
+    self.adb.Push(host_file_path, device_file_path)
+    self.device.PushChangedFiles([(host_file_path, device_file_path)])
+    result = self.device.RunShellCommand(['cat', device_file_path],
+                                         single_line=True)
+    self.assertEqual(_OLD_CONTENTS, result)
+
+    cmd_helper.RunCmd(['rm', host_file_path])
+    self.device.RunShellCommand(['rm', '-rf',  _DEVICE_DIR])
+
+  def testPushChangedFiles_singleFileChange(self):
+    (host_file_path, file_name) = self._MakeTempFile(_OLD_CONTENTS)
+    device_file_path = "%s/%s" % (_DEVICE_DIR, file_name)
+    self.adb.Push(host_file_path, device_file_path)
+
+    with open(host_file_path, 'w') as f:
+      f.write(_NEW_CONTENTS)
+    self.device.PushChangedFiles([(host_file_path, device_file_path)])
+    result = self.device.RunShellCommand(['cat', device_file_path],
+                                         single_line=True)
+    self.assertEqual(_NEW_CONTENTS, result)
+
+    cmd_helper.RunCmd(['rm', host_file_path])
+    self.device.RunShellCommand(['rm', '-rf',  _DEVICE_DIR])
+
+  def testDeleteFiles(self):
+    host_tmp_dir = tempfile.mkdtemp()
+    (host_file_path, file_name) = self._MakeTempFileGivenDir(
+        host_tmp_dir, _OLD_CONTENTS)
+
+    device_file_path = "%s/%s" % (_DEVICE_DIR, file_name)
+    self.adb.Push(host_file_path, device_file_path)
+
+    cmd_helper.RunCmd(['rm', host_file_path])
+    self.device.PushChangedFiles([(host_tmp_dir, _DEVICE_DIR)],
+                                 delete_device_stale=True)
+    result = self.device.RunShellCommand(['ls', _DEVICE_DIR], single_line=True)
+    self.assertEqual('', result)
+
+    cmd_helper.RunCmd(['rm', '-rf', host_tmp_dir])
+    self.device.RunShellCommand(['rm', '-rf',  _DEVICE_DIR])
+
+  def testPushAndDeleteFiles_noSubDir(self):
+    host_tmp_dir = tempfile.mkdtemp()
+    (host_file_path1, file_name1) = self._MakeTempFileGivenDir(
+        host_tmp_dir, _OLD_CONTENTS)
+    (host_file_path2, file_name2) = self._MakeTempFileGivenDir(
+        host_tmp_dir, _OLD_CONTENTS)
+
+    device_file_path1 = "%s/%s" % (_DEVICE_DIR, file_name1)
+    device_file_path2 = "%s/%s" % (_DEVICE_DIR, file_name2)
+    self.adb.Push(host_file_path1, device_file_path1)
+    self.adb.Push(host_file_path2, device_file_path2)
+
+    with open(host_file_path1, 'w') as f:
+      f.write(_NEW_CONTENTS)
+    cmd_helper.RunCmd(['rm', host_file_path2])
+
+    self.device.PushChangedFiles([(host_tmp_dir, _DEVICE_DIR)],
+                                   delete_device_stale=True)
+    result = self.device.RunShellCommand(['cat', device_file_path1],
+                                         single_line=True)
+    self.assertEqual(_NEW_CONTENTS, result)
+    result = self.device.RunShellCommand(['ls', _DEVICE_DIR], single_line=True)
+    self.assertEqual(file_name1, result)
+
+    self.device.RunShellCommand(['rm', '-rf',  _DEVICE_DIR])
+    cmd_helper.RunCmd(['rm', '-rf', host_tmp_dir])
+
+  def testPushAndDeleteFiles_SubDir(self):
+    host_tmp_dir = tempfile.mkdtemp()
+    host_sub_dir1 = "%s/%s" % (host_tmp_dir, _SUB_DIR1)
+    host_sub_dir2 = "%s/%s/%s" % (host_tmp_dir, _SUB_DIR, _SUB_DIR2)
+    cmd_helper.RunCmd(['mkdir', '-p', host_sub_dir1])
+    cmd_helper.RunCmd(['mkdir', '-p', host_sub_dir2])
+
+    (host_file_path1, file_name1) = self._MakeTempFileGivenDir(
+        host_tmp_dir, _OLD_CONTENTS)
+    (host_file_path2, file_name2) = self._MakeTempFileGivenDir(
+        host_tmp_dir, _OLD_CONTENTS)
+    (host_file_path3, file_name3) = self._MakeTempFileGivenDir(
+        host_sub_dir1, _OLD_CONTENTS)
+    (host_file_path4, file_name4) = self._MakeTempFileGivenDir(
+        host_sub_dir2, _OLD_CONTENTS)
+
+    device_file_path1 = "%s/%s" % (_DEVICE_DIR, file_name1)
+    device_file_path2 = "%s/%s" % (_DEVICE_DIR, file_name2)
+    device_file_path3 = "%s/%s/%s" % (_DEVICE_DIR, _SUB_DIR1, file_name3)
+    device_file_path4 = "%s/%s/%s/%s" % (_DEVICE_DIR, _SUB_DIR,
+                                         _SUB_DIR2, file_name4)
+
+    self.adb.Push(host_file_path1, device_file_path1)
+    self.adb.Push(host_file_path2, device_file_path2)
+    self.adb.Push(host_file_path3, device_file_path3)
+    self.adb.Push(host_file_path4, device_file_path4)
+
+    with open(host_file_path1, 'w') as f:
+      f.write(_NEW_CONTENTS)
+    cmd_helper.RunCmd(['rm', host_file_path2])
+    cmd_helper.RunCmd(['rm', host_file_path4])
+
+    self.device.PushChangedFiles([(host_tmp_dir, _DEVICE_DIR)],
+                                   delete_device_stale=True)
+    result = self.device.RunShellCommand(['cat', device_file_path1],
+                                         single_line=True)
+    self.assertEqual(_NEW_CONTENTS, result)
+
+    result = self.device.RunShellCommand(['ls', _DEVICE_DIR])
+    self.assertIn(file_name1, result)
+    self.assertIn(_SUB_DIR1, result)
+    self.assertIn(_SUB_DIR, result)
+    self.assertEqual(3, len(result))
+
+    result = self.device.RunShellCommand(['cat', device_file_path3],
+                                      single_line=True)
+    self.assertEqual(_OLD_CONTENTS, result)
+
+    result = self.device.RunShellCommand(["ls", "%s/%s/%s"
+                                          % (_DEVICE_DIR, _SUB_DIR, _SUB_DIR2)],
+                                         single_line=True)
+    self.assertEqual('', result)
+
+    self.device.RunShellCommand(['rm', '-rf',  _DEVICE_DIR])
+    cmd_helper.RunCmd(['rm', '-rf', host_tmp_dir])
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/build/android/pylib/device/device_utils_test.py b/build/android/pylib/device/device_utils_test.py
index 317e81e..6699673 100755
--- a/build/android/pylib/device/device_utils_test.py
+++ b/build/android/pylib/device/device_utils_test.py
@@ -27,6 +27,7 @@
 from pylib.device import device_errors
 from pylib.device import device_utils
 from pylib.device import intent
+from pylib.sdk import split_select
 from pylib.utils import mock_calls
 
 # RunCommand from third_party/android_testrunner/run_command.py is mocked
@@ -310,30 +311,49 @@
         self.device.GetExternalStoragePath()
 
 
-class DeviceUtilsGetApplicationPathTest(DeviceUtilsTest):
+class DeviceUtilsGetApplicationPathsTest(DeviceUtilsTest):
 
-  def testGetApplicationPath_exists(self):
+  def testGetApplicationPaths_exists(self):
     with self.assertCalls(
         (self.call.adb.Shell('getprop ro.build.version.sdk'), '19\n'),
         (self.call.adb.Shell('pm path android'),
          'package:/path/to/android.apk\n')):
-      self.assertEquals('/path/to/android.apk',
-                        self.device.GetApplicationPath('android'))
+      self.assertEquals(['/path/to/android.apk'],
+                        self.device.GetApplicationPaths('android'))
 
-  def testGetApplicationPath_notExists(self):
+  def testGetApplicationPaths_notExists(self):
     with self.assertCalls(
         (self.call.adb.Shell('getprop ro.build.version.sdk'), '19\n'),
         (self.call.adb.Shell('pm path not.installed.app'), '')):
-      self.assertEquals(None,
-                        self.device.GetApplicationPath('not.installed.app'))
+      self.assertEquals([],
+                        self.device.GetApplicationPaths('not.installed.app'))
 
-  def testGetApplicationPath_fails(self):
+  def testGetApplicationPaths_fails(self):
     with self.assertCalls(
         (self.call.adb.Shell('getprop ro.build.version.sdk'), '19\n'),
         (self.call.adb.Shell('pm path android'),
          self.CommandError('ERROR. Is package manager running?\n'))):
       with self.assertRaises(device_errors.CommandFailedError):
-        self.device.GetApplicationPath('android')
+        self.device.GetApplicationPaths('android')
+
+
+class DeviceUtilsGetApplicationDataDirectoryTest(DeviceUtilsTest):
+
+  def testGetApplicationDataDirectory_exists(self):
+    with self.assertCall(
+        self.call.device._RunPipedShellCommand(
+            'pm dump foo.bar.baz | grep dataDir='),
+        ['dataDir=/data/data/foo.bar.baz']):
+      self.assertEquals(
+          '/data/data/foo.bar.baz',
+          self.device.GetApplicationDataDirectory('foo.bar.baz'))
+
+  def testGetApplicationDataDirectory_notExists(self):
+    with self.assertCall(
+        self.call.device._RunPipedShellCommand(
+            'pm dump foo.bar.baz | grep dataDir='),
+        self.ShellError()):
+      self.assertIsNone(self.device.GetApplicationDataDirectory('foo.bar.baz'))
 
 
 @mock.patch('time.sleep', mock.Mock())
@@ -346,8 +366,8 @@
         (self.call.device.GetExternalStoragePath(), '/fake/storage/path'),
         (self.call.adb.Shell('test -d /fake/storage/path'), ''),
         # pm_ready
-        (self.call.device.GetApplicationPath('android'),
-         'package:/some/fake/path'),
+        (self.call.device.GetApplicationPaths('android'),
+         ['package:/some/fake/path']),
         # boot_completed
         (self.call.device.GetProp('sys.boot_completed'), '1')):
       self.device.WaitUntilFullyBooted(wifi=False)
@@ -359,8 +379,8 @@
         (self.call.device.GetExternalStoragePath(), '/fake/storage/path'),
         (self.call.adb.Shell('test -d /fake/storage/path'), ''),
         # pm_ready
-        (self.call.device.GetApplicationPath('android'),
-         'package:/some/fake/path'),
+        (self.call.device.GetApplicationPaths('android'),
+         ['package:/some/fake/path']),
         # boot_completed
         (self.call.device.GetProp('sys.boot_completed'), '1'),
         # wifi_enabled
@@ -383,8 +403,8 @@
         (self.call.device.GetExternalStoragePath(), '/fake/storage/path'),
         (self.call.adb.Shell('test -d /fake/storage/path'), ''),
         # pm_ready
-        (self.call.device.GetApplicationPath('android'),
-         'package:/some/fake/path'),
+        (self.call.device.GetApplicationPaths('android'),
+         ['package:/some/fake/path']),
         # boot_completed
         (self.call.device.GetProp('sys.boot_completed'), '1')):
       self.device.WaitUntilFullyBooted(wifi=False)
@@ -420,11 +440,11 @@
         (self.call.device.GetExternalStoragePath(), '/fake/storage/path'),
         (self.call.adb.Shell('test -d /fake/storage/path'), ''),
         # pm_ready
-        (self.call.device.GetApplicationPath('android'), self.CommandError()),
+        (self.call.device.GetApplicationPaths('android'), self.CommandError()),
         # pm_ready
-        (self.call.device.GetApplicationPath('android'), self.CommandError()),
+        (self.call.device.GetApplicationPaths('android'), self.CommandError()),
         # pm_ready
-        (self.call.device.GetApplicationPath('android'), self.TimeoutError())):
+        (self.call.device.GetApplicationPaths('android'), self.TimeoutError())):
       with self.assertRaises(device_errors.CommandTimeoutError):
         self.device.WaitUntilFullyBooted(wifi=False)
 
@@ -435,8 +455,8 @@
         (self.call.device.GetExternalStoragePath(), '/fake/storage/path'),
         (self.call.adb.Shell('test -d /fake/storage/path'), ''),
         # pm_ready
-        (self.call.device.GetApplicationPath('android'),
-         'package:/some/fake/path'),
+        (self.call.device.GetApplicationPaths('android'),
+         ['package:/some/fake/path']),
         # boot_completed
         (self.call.device.GetProp('sys.boot_completed'), '0'),
         # boot_completed
@@ -453,8 +473,8 @@
         (self.call.device.GetExternalStoragePath(), '/fake/storage/path'),
         (self.call.adb.Shell('test -d /fake/storage/path'), ''),
         # pm_ready
-        (self.call.device.GetApplicationPath('android'),
-         'package:/some/fake/path'),
+        (self.call.device.GetApplicationPaths('android'),
+         ['package:/some/fake/path']),
         # boot_completed
         (self.call.device.GetProp('sys.boot_completed'), '1'),
         # wifi_enabled
@@ -500,7 +520,7 @@
     with self.assertCalls(
         (mock.call.pylib.utils.apk_helper.GetPackageName('/fake/test/app.apk'),
          'this.is.a.test.package'),
-        (self.call.device.GetApplicationPath('this.is.a.test.package'), None),
+        (self.call.device.GetApplicationPaths('this.is.a.test.package'), []),
         self.call.adb.Install('/fake/test/app.apk', reinstall=False)):
       self.device.Install('/fake/test/app.apk', retries=0)
 
@@ -508,11 +528,12 @@
     with self.assertCalls(
         (mock.call.pylib.utils.apk_helper.GetPackageName('/fake/test/app.apk'),
          'this.is.a.test.package'),
-        (self.call.device.GetApplicationPath('this.is.a.test.package'),
-         '/fake/data/app/this.is.a.test.package.apk'),
-        (self.call.device._GetChangedFilesImpl(
+        (self.call.device.GetApplicationPaths('this.is.a.test.package'),
+         ['/fake/data/app/this.is.a.test.package.apk']),
+        (self.call.device._GetChangedAndStaleFiles(
             '/fake/test/app.apk', '/fake/data/app/this.is.a.test.package.apk'),
-         [('/fake/test/app.apk', '/fake/data/app/this.is.a.test.package.apk')]),
+         ([('/fake/test/app.apk', '/fake/data/app/this.is.a.test.package.apk')],
+          [])),
         self.call.adb.Uninstall('this.is.a.test.package'),
         self.call.adb.Install('/fake/test/app.apk', reinstall=False)):
       self.device.Install('/fake/test/app.apk', retries=0)
@@ -521,11 +542,12 @@
     with self.assertCalls(
         (mock.call.pylib.utils.apk_helper.GetPackageName('/fake/test/app.apk'),
          'this.is.a.test.package'),
-        (self.call.device.GetApplicationPath('this.is.a.test.package'),
-         '/fake/data/app/this.is.a.test.package.apk'),
-        (self.call.device._GetChangedFilesImpl(
+        (self.call.device.GetApplicationPaths('this.is.a.test.package'),
+         ['/fake/data/app/this.is.a.test.package.apk']),
+        (self.call.device._GetChangedAndStaleFiles(
             '/fake/test/app.apk', '/fake/data/app/this.is.a.test.package.apk'),
-         [('/fake/test/app.apk', '/fake/data/app/this.is.a.test.package.apk')]),
+         ([('/fake/test/app.apk', '/fake/data/app/this.is.a.test.package.apk')],
+          [])),
         self.call.adb.Install('/fake/test/app.apk', reinstall=True)):
       self.device.Install('/fake/test/app.apk', reinstall=True, retries=0)
 
@@ -533,23 +555,62 @@
     with self.assertCalls(
         (mock.call.pylib.utils.apk_helper.GetPackageName('/fake/test/app.apk'),
          'this.is.a.test.package'),
-        (self.call.device.GetApplicationPath('this.is.a.test.package'),
-         '/fake/data/app/this.is.a.test.package.apk'),
-        (self.call.device._GetChangedFilesImpl(
+        (self.call.device.GetApplicationPaths('this.is.a.test.package'),
+         ['/fake/data/app/this.is.a.test.package.apk']),
+        (self.call.device._GetChangedAndStaleFiles(
             '/fake/test/app.apk', '/fake/data/app/this.is.a.test.package.apk'),
-         [])):
+         ([], []))):
       self.device.Install('/fake/test/app.apk', retries=0)
 
   def testInstall_fails(self):
     with self.assertCalls(
         (mock.call.pylib.utils.apk_helper.GetPackageName('/fake/test/app.apk'),
          'this.is.a.test.package'),
-        (self.call.device.GetApplicationPath('this.is.a.test.package'), None),
+        (self.call.device.GetApplicationPaths('this.is.a.test.package'), []),
         (self.call.adb.Install('/fake/test/app.apk', reinstall=False),
          self.CommandError('Failure\r\n'))):
       with self.assertRaises(device_errors.CommandFailedError):
         self.device.Install('/fake/test/app.apk', retries=0)
 
+class DeviceUtilsInstallSplitApkTest(DeviceUtilsTest):
+
+  def testInstallSplitApk_noPriorInstall(self):
+    with self.assertCalls(
+        (self.call.device._CheckSdkLevel(21)),
+        (mock.call.pylib.sdk.split_select.SelectSplits(
+            self.device, 'base.apk',
+            ['split1.apk', 'split2.apk', 'split3.apk']),
+         ['split2.apk']),
+        (mock.call.pylib.utils.apk_helper.GetPackageName('base.apk'),
+         'this.is.a.test.package'),
+        (self.call.device.GetApplicationPaths('this.is.a.test.package'), []),
+        (self.call.adb.InstallMultiple(
+            ['base.apk', 'split2.apk'], partial=None, reinstall=False))):
+      self.device.InstallSplitApk('base.apk',
+          ['split1.apk', 'split2.apk', 'split3.apk'], retries=0)
+
+  def testInstallSplitApk_partialInstall(self):
+    with self.assertCalls(
+        (self.call.device._CheckSdkLevel(21)),
+        (mock.call.pylib.sdk.split_select.SelectSplits(
+            self.device, 'base.apk',
+            ['split1.apk', 'split2.apk', 'split3.apk']),
+         ['split2.apk']),
+        (mock.call.pylib.utils.apk_helper.GetPackageName('base.apk'),
+         'test.package'),
+        (self.call.device.GetApplicationPaths('test.package'),
+         ['base-on-device.apk', 'split2-on-device.apk']),
+        (mock.call.pylib.utils.md5sum.CalculateDeviceMd5Sums(
+            ['base-on-device.apk', 'split2-on-device.apk'], self.device),
+         {'base-on-device.apk': 'AAA', 'split2-on-device.apk': 'BBB'}),
+        (mock.call.pylib.utils.md5sum.CalculateHostMd5Sums(
+            ['base.apk', 'split2.apk']),
+         {'base.apk': 'AAA', 'split2.apk': 'CCC'}),
+        (self.call.adb.InstallMultiple(
+            ['split2.apk'], partial='test.package', reinstall=True))):
+      self.device.InstallSplitApk('base.apk',
+          ['split1.apk', 'split2.apk', 'split3.apk'], reinstall=True, retries=0)
+
 
 class DeviceUtilsRunShellCommandTest(DeviceUtilsTest):
 
@@ -972,7 +1033,7 @@
     with self.assertCalls(
         self.call.device.RunShellCommand(
             ['am', 'instrument', 'test.package/.TestInstrumentation'],
-            check_return=True)):
+            check_return=True, large_output=True)):
       self.device.StartInstrumentation(
           'test.package/.TestInstrumentation',
           finish=False, raw=False, extras=None)
@@ -981,7 +1042,7 @@
     with self.assertCalls(
         (self.call.device.RunShellCommand(
             ['am', 'instrument', '-w', 'test.package/.TestInstrumentation'],
-            check_return=True),
+            check_return=True, large_output=True),
          ['OK (1 test)'])):
       output = self.device.StartInstrumentation(
           'test.package/.TestInstrumentation',
@@ -992,7 +1053,7 @@
     with self.assertCalls(
         self.call.device.RunShellCommand(
             ['am', 'instrument', '-r', 'test.package/.TestInstrumentation'],
-            check_return=True)):
+            check_return=True, large_output=True)):
       self.device.StartInstrumentation(
           'test.package/.TestInstrumentation',
           finish=False, raw=True, extras=None)
@@ -1002,7 +1063,7 @@
         self.call.device.RunShellCommand(
             ['am', 'instrument', '-e', 'foo', 'Foo', '-e', 'bar', 'Bar',
              'test.package/.TestInstrumentation'],
-            check_return=True)):
+            check_return=True, large_output=True)):
       self.device.StartInstrumentation(
           'test.package/.TestInstrumentation',
           finish=False, raw=False, extras={'foo': 'Foo', 'bar': 'Bar'})
@@ -1038,13 +1099,88 @@
 
 class DeviceUtilsGoHomeTest(DeviceUtilsTest):
 
-  def testGoHome(self):
-    with self.assertCall(
-        self.call.adb.Shell('am start -W -a android.intent.action.MAIN '
-                            '-c android.intent.category.HOME'),
-        'Starting: Intent { act=android.intent.action.MAIN }\r\n'):
+  def testGoHome_popupsExist(self):
+    with self.assertCalls(
+        (self.call.device.RunShellCommand(
+            ['dumpsys', 'window', 'windows'], check_return=True,
+            large_output=True), []),
+        (self.call.device.RunShellCommand(
+            ['am', 'start', '-W', '-a', 'android.intent.action.MAIN',
+            '-c', 'android.intent.category.HOME'], check_return=True),
+         'Starting: Intent { act=android.intent.action.MAIN }\r\n'''),
+        (self.call.device.RunShellCommand(
+            ['dumpsys', 'window', 'windows'], check_return=True,
+            large_output=True), []),
+        (self.call.device.RunShellCommand(
+            ['input', 'keyevent', '66'], check_return=True)),
+        (self.call.device.RunShellCommand(
+            ['input', 'keyevent', '4'], check_return=True)),
+        (self.call.device.RunShellCommand(
+            ['dumpsys', 'window', 'windows'], check_return=True,
+            large_output=True),
+         ['mCurrentFocus Launcher'])):
       self.device.GoHome()
 
+  def testGoHome_willRetry(self):
+    with self.assertCalls(
+        (self.call.device.RunShellCommand(
+            ['dumpsys', 'window', 'windows'], check_return=True,
+            large_output=True), []),
+        (self.call.device.RunShellCommand(
+            ['am', 'start', '-W', '-a', 'android.intent.action.MAIN',
+            '-c', 'android.intent.category.HOME'], check_return=True),
+         'Starting: Intent { act=android.intent.action.MAIN }\r\n'''),
+        (self.call.device.RunShellCommand(
+            ['dumpsys', 'window', 'windows'], check_return=True,
+            large_output=True), []),
+        (self.call.device.RunShellCommand(
+            ['input', 'keyevent', '66'], check_return=True,)),
+        (self.call.device.RunShellCommand(
+            ['input', 'keyevent', '4'], check_return=True)),
+        (self.call.device.RunShellCommand(
+            ['dumpsys', 'window', 'windows'], check_return=True,
+            large_output=True), []),
+        (self.call.device.RunShellCommand(
+            ['input', 'keyevent', '66'], check_return=True)),
+        (self.call.device.RunShellCommand(
+            ['input', 'keyevent', '4'], check_return=True)),
+        (self.call.device.RunShellCommand(
+            ['dumpsys', 'window', 'windows'], check_return=True,
+            large_output=True),
+         self.TimeoutError())):
+      with self.assertRaises(device_errors.CommandTimeoutError):
+        self.device.GoHome()
+
+  def testGoHome_alreadyFocused(self):
+    with self.assertCall(
+        self.call.device.RunShellCommand(
+            ['dumpsys', 'window', 'windows'], check_return=True,
+            large_output=True),
+        ['mCurrentFocus Launcher']):
+      self.device.GoHome()
+
+  def testGoHome_alreadyFocusedAlternateCase(self):
+    with self.assertCall(
+        self.call.device.RunShellCommand(
+            ['dumpsys', 'window', 'windows'], check_return=True,
+            large_output=True),
+        [' mCurrentFocus .launcher/.']):
+      self.device.GoHome()
+
+  def testGoHome_obtainsFocusAfterGoingHome(self):
+    with self.assertCalls(
+        (self.call.device.RunShellCommand(
+            ['dumpsys', 'window', 'windows'], check_return=True,
+            large_output=True), []),
+        (self.call.device.RunShellCommand(
+            ['am', 'start', '-W', '-a', 'android.intent.action.MAIN',
+            '-c', 'android.intent.category.HOME'], check_return=True),
+         'Starting: Intent { act=android.intent.action.MAIN }\r\n'''),
+        (self.call.device.RunShellCommand(
+            ['dumpsys', 'window', 'windows'], check_return=True,
+            large_output=True),
+         ['mCurrentFocus Launcher'])):
+      self.device.GoHome()
 
 class DeviceUtilsForceStopTest(DeviceUtilsTest):
 
@@ -1060,8 +1196,8 @@
   def testClearApplicationState_packageDoesntExist(self):
     with self.assertCalls(
         (self.call.adb.Shell('getprop ro.build.version.sdk'), '17\n'),
-        (self.call.device.GetApplicationPath('this.package.does.not.exist'),
-         None)):
+        (self.call.device.GetApplicationPaths('this.package.does.not.exist'),
+         [])):
       self.device.ClearApplicationState('this.package.does.not.exist')
 
   def testClearApplicationState_packageDoesntExistOnAndroidJBMR2OrAbove(self):
@@ -1074,8 +1210,8 @@
   def testClearApplicationState_packageExists(self):
     with self.assertCalls(
         (self.call.adb.Shell('getprop ro.build.version.sdk'), '17\n'),
-        (self.call.device.GetApplicationPath('this.package.exists'),
-         '/data/app/this.package.exists.apk'),
+        (self.call.device.GetApplicationPaths('this.package.exists'),
+         ['/data/app/this.package.exists.apk']),
         (self.call.adb.Shell('pm clear this.package.exists'),
          'Success\r\n')):
       self.device.ClearApplicationState('this.package.exists')
diff --git a/build/android/pylib/gtest/filter/cc_unittests_disabled b/build/android/pylib/gtest/filter/cc_unittests_disabled
index feab5ac..b49d2c6 100644
--- a/build/android/pylib/gtest/filter/cc_unittests_disabled
+++ b/build/android/pylib/gtest/filter/cc_unittests_disabled
@@ -1,5 +1,5 @@
 # Death tests are not supported with apks.
-BeginFrameObserverMixInTest.OnBeginFrameImplementation
-BeginFrameSourceMixInTest.ObserverManipulation
+BeginFrameObserverBaseTest.OnBeginFrameImplementation
+BeginFrameSourceBaseTest.ObserverManipulation
 BeginFrameSourceMultiplexerTest.SourcesManipulation
 BeginFrameSourceMultiplexerTest.MinimumIntervalNegativeFails
diff --git a/build/android/pylib/gtest/gtest_test_instance.py b/build/android/pylib/gtest/gtest_test_instance.py
index cea5016..3285e0b 100644
--- a/build/android/pylib/gtest/gtest_test_instance.py
+++ b/build/android/pylib/gtest/gtest_test_instance.py
@@ -7,10 +7,12 @@
 import re
 import shutil
 import sys
+import tempfile
 
 from pylib import constants
 from pylib.base import base_test_result
 from pylib.base import test_instance
+from pylib.utils import apk_helper
 
 sys.path.append(os.path.join(
     constants.DIR_SOURCE_ROOT, 'build', 'util', 'lib', 'common'))
@@ -23,6 +25,29 @@
 ]
 
 
+_DEFAULT_ISOLATE_FILE_PATHS = {
+    'base_unittests': 'base/base_unittests.isolate',
+    'blink_heap_unittests':
+      'third_party/WebKit/Source/platform/heap/BlinkHeapUnitTests.isolate',
+    'breakpad_unittests': 'breakpad/breakpad_unittests.isolate',
+    'cc_perftests': 'cc/cc_perftests.isolate',
+    'components_browsertests': 'components/components_browsertests.isolate',
+    'components_unittests': 'components/components_unittests.isolate',
+    'content_browsertests': 'content/content_browsertests.isolate',
+    'content_unittests': 'content/content_unittests.isolate',
+    'media_perftests': 'media/media_perftests.isolate',
+    'media_unittests': 'media/media_unittests.isolate',
+    'midi_unittests': 'media/midi/midi_unittests.isolate',
+    'net_unittests': 'net/net_unittests.isolate',
+    'sql_unittests': 'sql/sql_unittests.isolate',
+    'sync_unit_tests': 'sync/sync_unit_tests.isolate',
+    'ui_base_unittests': 'ui/base/ui_base_tests.isolate',
+    'unit_tests': 'chrome/unit_tests.isolate',
+    'webkit_unit_tests':
+      'third_party/WebKit/Source/web/WebKitUnitTests.isolate',
+}
+
+
 # Used for filtering large data deps at a finer grain than what's allowed in
 # isolate files since pushing deps to devices is expensive.
 # Wildcards are allowed.
@@ -46,6 +71,13 @@
 ]
 
 
+_EXTRA_NATIVE_TEST_ACTIVITY = (
+    'org.chromium.native_test.NativeTestInstrumentationTestRunner.'
+        'NativeTestActivity')
+_EXTRA_SHARD_SIZE_LIMIT =(
+    'org.chromium.native_test.NativeTestInstrumentationTestRunner.'
+        'ShardSizeLimit')
+
 # TODO(jbudorick): Remove these once we're no longer parsing stdout to generate
 # results.
 _RE_TEST_STATUS = re.compile(
@@ -105,6 +137,20 @@
                                   self._suite)
     if not os.path.exists(self._apk_path):
       self._apk_path = None
+      self._activity = None
+      self._package = None
+      self._runner = None
+    else:
+      helper = apk_helper.ApkHelper(self._apk_path)
+      self._activity = helper.GetActivityName()
+      self._package = helper.GetPackageName()
+      self._runner = helper.GetInstrumentationName()
+      self._extras = {
+        _EXTRA_NATIVE_TEST_ACTIVITY: self._activity,
+      }
+      if self._suite in BROWSER_TEST_SUITES:
+        self._extras[_EXTRA_SHARD_SIZE_LIMIT] = 1
+
     if not os.path.exists(self._exe_path):
       self._exe_path = None
     if not self._apk_path and not self._exe_path:
@@ -118,6 +164,13 @@
         self._gtest_filter = ':'.join(l.strip() for l in f)
     else:
       self._gtest_filter = None
+
+    if not args.isolate_file_path:
+      default_isolate_file_path = _DEFAULT_ISOLATE_FILE_PATHS.get(self._suite)
+      if default_isolate_file_path:
+        args.isolate_file_path = os.path.join(
+            constants.DIR_SOURCE_ROOT, default_isolate_file_path)
+
     if args.isolate_file_path:
       self._isolate_abs_path = os.path.abspath(args.isolate_file_path)
       self._isolate_delegate = isolate_delegate
@@ -127,6 +180,17 @@
       logging.warning('No isolate file provided. No data deps will be pushed.');
       self._isolate_delegate = None
 
+    if args.app_data_files:
+      self._app_data_files = args.app_data_files
+      if args.app_data_file_dir:
+        self._app_data_file_dir = args.app_data_file_dir
+      else:
+        self._app_data_file_dir = tempfile.mkdtemp()
+        logging.critical('Saving app files to %s', self._app_data_file_dir)
+    else:
+      self._app_data_files = None
+      self._app_data_file_dir = None
+
   #override
   def TestType(self):
     return 'gtest'
@@ -228,14 +292,38 @@
       self._isolate_delegate.Clear()
 
   @property
+  def activity(self):
+    return self._activity
+
+  @property
   def apk(self):
     return self._apk_path
 
   @property
+  def app_file_dir(self):
+    return self._app_data_file_dir
+
+  @property
+  def app_files(self):
+    return self._app_data_files
+
+  @property
   def exe(self):
     return self._exe_path
 
   @property
+  def extras(self):
+    return self._extras
+
+  @property
+  def package(self):
+    return self._package
+
+  @property
+  def runner(self):
+    return self._runner
+
+  @property
   def suite(self):
     return self._suite
 
diff --git a/build/android/pylib/gtest/local_device_gtest_run.py b/build/android/pylib/gtest/local_device_gtest_run.py
index 15a58a4..f1cea4e 100644
--- a/build/android/pylib/gtest/local_device_gtest_run.py
+++ b/build/android/pylib/gtest/local_device_gtest_run.py
@@ -2,9 +2,10 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-
+import itertools
 import logging
 import os
+import posixpath
 
 from pylib import constants
 from pylib import ports
@@ -15,7 +16,6 @@
 from pylib.local import local_test_server_spawner
 from pylib.local.device import local_device_environment
 from pylib.local.device import local_device_test_run
-from pylib.utils import apk_helper
 from pylib.utils import device_temp_file
 
 _COMMAND_LINE_FLAGS_SUPPORTED = True
@@ -24,9 +24,9 @@
     'org.chromium.native_test.NativeTestActivity.CommandLineFile')
 _EXTRA_COMMAND_LINE_FLAGS = (
     'org.chromium.native_test.NativeTestActivity.CommandLineFlags')
-_EXTRA_NATIVE_TEST_ACTIVITY = (
+_EXTRA_TEST_LIST = (
     'org.chromium.native_test.NativeTestInstrumentationTestRunner'
-        '.NativeTestActivity')
+        '.TestList')
 
 _MAX_SHARD_SIZE = 256
 
@@ -37,38 +37,57 @@
   'net_unittests', 'unit_tests'
 ]
 
-class _ApkDelegate(object):
-  def __init__(self, apk):
-    self._apk = apk
+# TODO(jbudorick): Move this inside _ApkDelegate once TestPackageApk is gone.
+def PullAppFilesImpl(device, package, files, directory):
+  device_dir = device.GetApplicationDataDirectory(package)
+  host_dir = os.path.join(directory, str(device))
+  for f in files:
+    device_file = posixpath.join(device_dir, f)
+    host_file = os.path.join(host_dir, *f.split(posixpath.sep))
+    host_file_base, ext = os.path.splitext(host_file)
+    for i in itertools.count():
+      host_file = '%s_%d%s' % (host_file_base, i, ext)
+      if not os.path.exists(host_file):
+        break
+    device.PullFile(device_file, host_file)
 
-    helper = apk_helper.ApkHelper(self._apk)
-    self._activity = helper.GetActivityName()
-    self._package = helper.GetPackageName()
-    self._runner = helper.GetInstrumentationName()
+class _ApkDelegate(object):
+  def __init__(self, test_instance):
+    self._activity = test_instance.activity
+    self._apk = test_instance.apk
+    self._package = test_instance.package
+    self._runner = test_instance.runner
+
     self._component = '%s/%s' % (self._package, self._runner)
-    self._enable_test_server_spawner = False
+    self._extras = test_instance.extras
 
   def Install(self, device):
     device.Install(self._apk)
 
-  def RunWithFlags(self, device, flags, **kwargs):
+  def Run(self, test, device, flags=None, **kwargs):
+    extras = dict(self._extras)
+
     with device_temp_file.DeviceTempFile(device.adb) as command_line_file:
-      device.WriteFile(command_line_file.name, '_ %s' % flags)
+      device.WriteFile(command_line_file.name, '_ %s' % flags if flags else '_')
+      extras[_EXTRA_COMMAND_LINE_FILE] = command_line_file.name
 
-      extras = {
-        _EXTRA_COMMAND_LINE_FILE: command_line_file.name,
-        _EXTRA_NATIVE_TEST_ACTIVITY: self._activity,
-      }
+      with device_temp_file.DeviceTempFile(device.adb) as test_list_file:
+        if test:
+          device.WriteFile(test_list_file.name, '\n'.join(test))
+          extras[_EXTRA_TEST_LIST] = test_list_file.name
 
-      return device.StartInstrumentation(
-          self._component, extras=extras, raw=False, **kwargs)
+        return device.StartInstrumentation(
+            self._component, extras=extras, raw=False, **kwargs)
+
+  def PullAppFiles(self, device, files, directory):
+    PullAppFilesImpl(device, self._package, files, directory)
 
   def Clear(self, device):
     device.ClearApplicationState(self._package)
 
 
 class _ExeDelegate(object):
-  def __init__(self, exe, tr):
+  def __init__(self, tr, exe):
     self._exe_host_path = exe
     self._exe_file_name = os.path.split(exe)[-1]
     self._exe_device_path = '%s/%s' % (
@@ -89,12 +108,15 @@
       host_device_tuples.append((self._deps_host_path, self._deps_device_path))
     device.PushChangedFiles(host_device_tuples)
 
-  def RunWithFlags(self, device, flags, **kwargs):
+  def Run(self, test, device, flags=None, **kwargs):
     cmd = [
         self._test_run.GetTool(device).GetTestWrapper(),
         self._exe_device_path,
-        flags,
     ]
+    if test:
+      cmd.append('--gtest_filter=%s' % ':'.join(test))
+    if flags:
+      cmd.append(flags)
     cwd = constants.TEST_EXECUTABLE_DIR
 
     env = {
@@ -119,6 +141,9 @@
                                       env=env, **kwargs)
     return output
 
+  def PullAppFiles(self, device, files, directory):
+    pass
+
   def Clear(self, device):
     device.KillAll(self._exe_file_name, blocking=True, timeout=30, quiet=True)
 
@@ -131,7 +156,7 @@
     super(LocalDeviceGtestRun, self).__init__(env, test_instance)
 
     if self._test_instance.apk:
-      self._delegate = _ApkDelegate(self._test_instance.apk)
+      self._delegate = _ApkDelegate(self._test_instance)
     elif self._test_instance.exe:
       self._delegate = _ExeDelegate(self, self._test_instance.exe)
 
@@ -173,21 +198,18 @@
 
   #override
   def _CreateShards(self, tests):
-    if self._test_instance.suite in gtest_test_instance.BROWSER_TEST_SUITES:
-      return tests
-    else:
-      device_count = len(self._env.devices)
-      shards = []
-      for i in xrange(0, device_count):
-        unbounded_shard = tests[i::device_count]
-        shards += [unbounded_shard[j:j+_MAX_SHARD_SIZE]
-                   for j in xrange(0, len(unbounded_shard), _MAX_SHARD_SIZE)]
-      return [':'.join(s) for s in shards]
+    device_count = len(self._env.devices)
+    shards = []
+    for i in xrange(0, device_count):
+      unbounded_shard = tests[i::device_count]
+      shards += [unbounded_shard[j:j+_MAX_SHARD_SIZE]
+                 for j in xrange(0, len(unbounded_shard), _MAX_SHARD_SIZE)]
+    return shards
 
   #override
   def _GetTests(self):
-    tests = self._delegate.RunWithFlags(
-        self._env.devices[0], '--gtest_list_tests')
+    tests = self._delegate.Run(
+        None, self._env.devices[0], flags='--gtest_list_tests')
     tests = gtest_test_instance.ParseGTestListTests(tests)
     tests = self._test_instance.FilterTests(tests)
     return tests
@@ -195,10 +217,13 @@
   #override
   def _RunTest(self, device, test):
     # Run the test.
-    output = self._delegate.RunWithFlags(
-        device, '--gtest_filter=%s' % test, timeout=900, retries=0)
+    output = self._delegate.Run(
+        test, device, timeout=900, retries=0)
     for s in self._servers[str(device)]:
       s.Reset()
+    if self._test_instance.app_files:
+      self._delegate.PullAppFiles(device, self._test_instance.app_files,
+                                  self._test_instance.app_file_dir)
     self._delegate.Clear(device)
 
     # Parse the output.
diff --git a/build/android/pylib/gtest/setup.py b/build/android/pylib/gtest/setup.py
index 8ba9a8d..f563ccf 100644
--- a/build/android/pylib/gtest/setup.py
+++ b/build/android/pylib/gtest/setup.py
@@ -26,27 +26,8 @@
 import unittest_util # pylint: disable=F0401
 
 
-ISOLATE_FILE_PATHS = {
-    'base_unittests': 'base/base_unittests.isolate',
-    'blink_heap_unittests':
-      'third_party/WebKit/Source/platform/heap/BlinkHeapUnitTests.isolate',
-    'breakpad_unittests': 'breakpad/breakpad_unittests.isolate',
-    'cc_perftests': 'cc/cc_perftests.isolate',
-    'components_browsertests': 'components/components_browsertests.isolate',
-    'components_unittests': 'components/components_unittests.isolate',
-    'content_browsertests': 'content/content_browsertests.isolate',
-    'content_unittests': 'content/content_unittests.isolate',
-    'media_perftests': 'media/media_perftests.isolate',
-    'media_unittests': 'media/media_unittests.isolate',
-    'midi_unittests': 'media/midi/midi_unittests.isolate',
-    'net_unittests': 'net/net_unittests.isolate',
-    'sql_unittests': 'sql/sql_unittests.isolate',
-    'sync_unit_tests': 'sync/sync_unit_tests.isolate',
-    'ui_base_unittests': 'ui/base/ui_base_tests.isolate',
-    'unit_tests': 'chrome/unit_tests.isolate',
-    'webkit_unit_tests':
-      'third_party/WebKit/Source/web/WebKitUnitTests.isolate',
-}
+ISOLATE_FILE_PATHS = gtest_test_instance._DEFAULT_ISOLATE_FILE_PATHS
+
 
 # Used for filtering large data deps at a finer grain than what's allowed in
 # isolate files since pushing deps to devices is expensive.
diff --git a/build/android/pylib/gtest/test_options.py b/build/android/pylib/gtest/test_options.py
index 58cd82b..8bc6996 100644
--- a/build/android/pylib/gtest/test_options.py
+++ b/build/android/pylib/gtest/test_options.py
@@ -13,4 +13,7 @@
     'test_arguments',
     'timeout',
     'isolate_file_path',
-    'suite_name'])
+    'suite_name',
+    'app_data_files',
+    'app_data_file_dir',
+    'delete_stale_data'])
diff --git a/build/android/pylib/gtest/test_package.py b/build/android/pylib/gtest/test_package.py
index dbd47bf..4042a98 100644
--- a/build/android/pylib/gtest/test_package.py
+++ b/build/android/pylib/gtest/test_package.py
@@ -22,7 +22,7 @@
     Args:
       device: Instance of DeviceUtils.
     """
-    raise NotImplementedError('Method must be overriden.')
+    raise NotImplementedError('Method must be overridden.')
 
   def CreateCommandLineFileOnDevice(self, device, test_filter, test_arguments):
     """Creates a test runner script and pushes to the device.
@@ -32,7 +32,7 @@
       test_filter: A test_filter flag.
       test_arguments: Additional arguments to pass to the test binary.
     """
-    raise NotImplementedError('Method must be overriden.')
+    raise NotImplementedError('Method must be overridden.')
 
   def GetAllTests(self, device):
     """Returns a list of all tests available in the test suite.
@@ -40,7 +40,7 @@
     Args:
       device: Instance of DeviceUtils.
     """
-    raise NotImplementedError('Method must be overriden.')
+    raise NotImplementedError('Method must be overridden.')
 
   def GetGTestReturnCode(self, _device):
     return None
@@ -54,7 +54,7 @@
     Returns:
       An instance of pexpect spawn class.
     """
-    raise NotImplementedError('Method must be overriden.')
+    raise NotImplementedError('Method must be overridden.')
 
   def Install(self, device):
     """Install the test package to the device.
@@ -62,5 +62,15 @@
     Args:
       device: Instance of DeviceUtils.
     """
-    raise NotImplementedError('Method must be overriden.')
+    raise NotImplementedError('Method must be overridden.')
 
+  def PullAppFiles(self, device, files, directory):
+    """Pull application data from the device.
+
+    Args:
+      device: Instance of DeviceUtils.
+      files: A list of paths relative to the application data directory to
+        retrieve from the device.
+      directory: The host directory to which files should be pulled.
+    """
+    raise NotImplementedError('Method must be overridden.')
diff --git a/build/android/pylib/gtest/test_package_apk.py b/build/android/pylib/gtest/test_package_apk.py
index 6447820..16ef21c 100644
--- a/build/android/pylib/gtest/test_package_apk.py
+++ b/build/android/pylib/gtest/test_package_apk.py
@@ -5,8 +5,10 @@
 """Defines TestPackageApk to help run APK-based native tests."""
 # pylint: disable=W0212
 
+import itertools
 import logging
 import os
+import posixpath
 import shlex
 import sys
 import tempfile
@@ -18,6 +20,7 @@
 from pylib.device import device_errors
 from pylib.device import intent
 from pylib.gtest import gtest_test_instance
+from pylib.gtest import local_device_gtest_run
 from pylib.gtest.test_package import TestPackage
 
 
@@ -141,3 +144,8 @@
   def Install(self, device):
     self.tool.CopyFiles(device)
     device.Install(self.suite_path)
+
+  #override
+  def PullAppFiles(self, device, files, directory):
+    local_device_gtest_run.PullAppFilesImpl(
+        device, self._package_info.package, files, directory)
diff --git a/build/android/pylib/gtest/test_package_exe.py b/build/android/pylib/gtest/test_package_exe.py
index 7cdcb99..87071b5 100644
--- a/build/android/pylib/gtest/test_package_exe.py
+++ b/build/android/pylib/gtest/test_package_exe.py
@@ -157,3 +157,7 @@
     deps_path = self.suite_path + '_deps'
     if os.path.isdir(deps_path):
       device.PushChangedFiles([(deps_path, test_binary_path + '_deps')])
+
+  #override
+  def PullAppFiles(self, device, files, directory):
+    pass
diff --git a/build/android/pylib/gtest/test_runner.py b/build/android/pylib/gtest/test_runner.py
index 6d01990..a48f18a 100644
--- a/build/android/pylib/gtest/test_runner.py
+++ b/build/android/pylib/gtest/test_runner.py
@@ -5,6 +5,7 @@
 import logging
 import os
 import re
+import tempfile
 
 from pylib import pexpect
 from pylib import ports
@@ -80,6 +81,17 @@
     else:
       self._servers = []
 
+    if test_options.app_data_files:
+      self._app_data_files = test_options.app_data_files
+      if test_options.app_data_file_dir:
+        self._app_data_file_dir = test_options.app_data_file_dir
+      else:
+        self._app_data_file_dir = tempfile.mkdtemp()
+        logging.critical('Saving app files to %s', self._app_data_file_dir)
+    else:
+      self._app_data_files = None
+      self._app_data_file_dir = None
+
   #override
   def InstallTestPackage(self):
     self.test_package.Install(self.device)
@@ -167,6 +179,9 @@
           self.device, test, self._test_arguments)
       test_results = self._ParseTestOutput(
           self.test_package.SpawnTestProcess(self.device))
+      if self._app_data_files:
+        self.test_package.PullAppFiles(self.device, self._app_data_files,
+                                       self._app_data_file_dir)
     finally:
       for s in self._servers:
         s.Reset()
diff --git a/build/android/pylib/instrumentation/test_jar.py b/build/android/pylib/instrumentation/test_jar.py
index 9c38510..7ad8997 100644
--- a/build/android/pylib/instrumentation/test_jar.py
+++ b/build/android/pylib/instrumentation/test_jar.py
@@ -65,7 +65,8 @@
       try:
         with open(self._pickled_proguard_name, 'r') as r:
           d = pickle.loads(r.read())
-        jar_md5 = md5sum.CalculateHostMd5Sums(self._jar_path)[self._jar_path]
+        jar_md5 = md5sum.CalculateHostMd5Sums(
+          self._jar_path)[os.path.realpath(self._jar_path)]
         if (d['JAR_MD5SUM'] == jar_md5 and
             d['VERSION'] == PICKLE_FORMAT_VERSION):
           self._test_methods = d['TEST_METHODS']
@@ -106,7 +107,8 @@
     d = {'VERSION': PICKLE_FORMAT_VERSION,
          'TEST_METHODS': self._test_methods,
          'JAR_MD5SUM':
-              md5sum.CalculateHostMd5Sums(self._jar_path)[self._jar_path]}
+              md5sum.CalculateHostMd5Sums(
+                self._jar_path)[os.path.realpath(self._jar_path)]}
     with open(self._pickled_proguard_name, 'w') as f:
       f.write(pickle.dumps(d))
 
diff --git a/build/android/pylib/instrumentation/test_options.py b/build/android/pylib/instrumentation/test_options.py
index 792010b..e7b7a9f 100644
--- a/build/android/pylib/instrumentation/test_options.py
+++ b/build/android/pylib/instrumentation/test_options.py
@@ -23,4 +23,5 @@
     'test_support_apk_path',
     'device_flags',
     'isolate_file_path',
-    'set_asserts'])
+    'set_asserts',
+    'delete_stale_data'])
diff --git a/build/android/pylib/instrumentation/test_runner.py b/build/android/pylib/instrumentation/test_runner.py
index 4ca0742..0f2e53f 100644
--- a/build/android/pylib/instrumentation/test_runner.py
+++ b/build/android/pylib/instrumentation/test_runner.py
@@ -343,6 +343,11 @@
     try:
       self.TestSetup(test)
 
+      try:
+        self.device.GoHome()
+      except device_errors.CommandTimeoutError:
+        logging.exception('Failed to focus the launcher.')
+
       time_ms = lambda: int(time.time() * 1000)
       start_ms = time_ms()
       raw_output = self._RunTest(test, timeout)
diff --git a/build/android/pylib/local/device/local_device_environment.py b/build/android/pylib/local/device/local_device_environment.py
index 0d02ca3..04f9ab7 100644
--- a/build/android/pylib/local/device/local_device_environment.py
+++ b/build/android/pylib/local/device/local_device_environment.py
@@ -25,7 +25,7 @@
       raise device_errors.NoDevicesError
     if self._device_serial:
       self._devices = [d for d in available_devices
-                       if d.adb.GetDeviceSerial == self._device_serial]
+                       if d.adb.GetDeviceSerial() == self._device_serial]
       if not self._devices:
         raise device_errors.DeviceUnreachableError(
             'Could not find device %r' % self._device_serial)
diff --git a/build/android/pylib/perf/test_options.py b/build/android/pylib/perf/test_options.py
index 0a0ace0..e6ef560 100644
--- a/build/android/pylib/perf/test_options.py
+++ b/build/android/pylib/perf/test_options.py
@@ -17,4 +17,5 @@
     'single_step',
     'collect_chartjson_data',
     'output_chartjson_data',
+    'max_battery_temp',
 ])
diff --git a/build/android/pylib/perf/test_runner.py b/build/android/pylib/perf/test_runner.py
index 0f464c3..31eb0e9 100644
--- a/build/android/pylib/perf/test_runner.py
+++ b/build/android/pylib/perf/test_runner.py
@@ -273,6 +273,10 @@
     logging.info(
         'temperature: %s (0.1 C)',
         str(self._device_battery.GetBatteryInfo().get('temperature')))
+    if self._options.max_battery_temp:
+      self._device_battery.LetBatteryCoolToTemperature(
+          self._options.max_battery_temp)
+
     logging.info('%s : %s', test_name, cmd)
     start_time = datetime.datetime.now()
 
diff --git a/build/android/pylib/results/flakiness_dashboard/results_uploader.py b/build/android/pylib/results/flakiness_dashboard/results_uploader.py
index 856fa9c..b86d7ac 100644
--- a/build/android/pylib/results/flakiness_dashboard/results_uploader.py
+++ b/build/android/pylib/results/flakiness_dashboard/results_uploader.py
@@ -106,6 +106,9 @@
       buildbot_branch = os.environ.get('BUILDBOT_BRANCH')
       if not buildbot_branch:
         buildbot_branch = 'master'
+      else:
+        # Ensure there's no leading "origin/"
+        buildbot_branch = buildbot_branch[buildbot_branch.find('/') + 1:]
       self._master_name = '%s-%s' % (self._build_name, buildbot_branch)
 
     self._test_results_map = {}
diff --git a/build/android/pylib/results/report_results.py b/build/android/pylib/results/report_results.py
index 4c9518e..4fc6aa0 100644
--- a/build/android/pylib/results/report_results.py
+++ b/build/android/pylib/results/report_results.py
@@ -43,7 +43,9 @@
     if test_type == 'Instrumentation':
       if flakiness_server == constants.UPSTREAM_FLAKINESS_SERVER:
         assert test_package in ['ContentShellTest',
+                                'ChromePublicTest',
                                 'ChromeShellTest',
+                                'ChromeSyncShellTest',
                                 'AndroidWebViewTest']
         dashboard_test_type = ('%s_instrumentation_tests' %
                                test_package.lower().rstrip('test'))
diff --git a/build/android/pylib/sdk/__init__.py b/build/android/pylib/sdk/__init__.py
new file mode 100644
index 0000000..50b23df
--- /dev/null
+++ b/build/android/pylib/sdk/__init__.py
@@ -0,0 +1,3 @@
+# Copyright 2015 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/sdk/dexdump.py b/build/android/pylib/sdk/dexdump.py
new file mode 100644
index 0000000..ec10aba
--- /dev/null
+++ b/build/android/pylib/sdk/dexdump.py
@@ -0,0 +1,30 @@
+# Copyright 2015 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 cmd_helper
+from pylib import constants
+
+_DEXDUMP_PATH = os.path.join(constants.ANDROID_SDK_TOOLS, 'dexdump')
+
+def DexDump(dexfiles, file_summary=False):
+  """A wrapper around the Android SDK's dexdump tool.
+
+  Args:
+    dexfiles: The dexfile or list of dex files to dump.
+    file_summary: Display summary information from the file header. (-f)
+
+  Returns:
+    An iterable over the output lines.
+  """
+  # TODO(jbudorick): Add support for more options as necessary.
+  if isinstance(dexfiles, basestring):
+    dexfiles = [dexfiles]
+  args = [_DEXDUMP_PATH] + dexfiles
+  if file_summary:
+    args.append('-f')
+
+  return cmd_helper.IterCmdOutputLines(args)
+
diff --git a/build/android/pylib/sdk/split_select.py b/build/android/pylib/sdk/split_select.py
new file mode 100644
index 0000000..8752129
--- /dev/null
+++ b/build/android/pylib/sdk/split_select.py
@@ -0,0 +1,66 @@
+# Copyright 2015 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.
+
+"""This module wraps Android's split-select tool."""
+# pylint: disable=unused-argument
+
+import os
+
+from pylib import cmd_helper
+from pylib import constants
+from pylib.utils import timeout_retry
+
+_SPLIT_SELECT_PATH = os.path.join(constants.ANDROID_SDK_TOOLS, 'split-select')
+_DEFAULT_TIMEOUT = 30
+_DEFAULT_RETRIES = 2
+
+def _RunSplitSelectCmd(args, timeout=None, retries=None):
+  """Runs a split-select command.
+
+  Args:
+    args: A list of arguments for split-select.
+    timeout: Timeout in seconds.
+    retries: Number of retries.
+
+  Returns:
+    The output of the command.
+  """
+  cmd = [_SPLIT_SELECT_PATH] + args
+  status, output = cmd_helper.GetCmdStatusAndOutputWithTimeout(
+      cmd, timeout_retry.CurrentTimeoutThread().GetRemainingTime())
+  if status != 0:
+    raise Exception('Failed running command %s' % str(cmd))
+  return output
+
+def _SplitConfig(device):
+  """Returns a config specifying which APK splits are required by the device.
+
+  Args:
+    device: A DeviceUtils object.
+  """
+  return ('%s-r%s-%s:%s' %
+          (device.language,
+           device.country,
+           device.screen_density,
+           device.product_cpu_abi))
+
+def SelectSplits(device, base_apk, split_apks,
+                 timeout=_DEFAULT_TIMEOUT, retries=_DEFAULT_RETRIES):
+  """Determines which APK splits the device requires.
+
+  Args:
+    device: A DeviceUtils object.
+    base_apk: The path of the base APK.
+    split_apks: A list of paths of APK splits.
+    timeout: Timeout in seconds.
+    retries: Number of retries.
+
+  Returns:
+    The list of APK splits that the device requires.
+  """
+  config = _SplitConfig(device)
+  args = ['--target', config, '--base', base_apk]
+  for split in split_apks:
+    args.extend(['--split', split])
+  return _RunSplitSelectCmd(args, timeout=timeout, retries=retries).splitlines()
\ No newline at end of file
diff --git a/build/android/pylib/uiautomator/test_runner.py b/build/android/pylib/uiautomator/test_runner.py
index 296bd47..bda6687 100644
--- a/build/android/pylib/uiautomator/test_runner.py
+++ b/build/android/pylib/uiautomator/test_runner.py
@@ -41,7 +41,8 @@
         test_support_apk_path=None,
         device_flags=None,
         isolate_file_path=None,
-        set_asserts=test_options.set_asserts)
+        set_asserts=test_options.set_asserts,
+        delete_stale_data=False)
     super(TestRunner, self).__init__(instrumentation_options, device,
                                      shard_index, test_pkg)
 
diff --git a/build/android/pylib/utils/emulator.py b/build/android/pylib/utils/emulator.py
index 635462f..cc07e61 100644
--- a/build/android/pylib/utils/emulator.py
+++ b/build/android/pylib/utils/emulator.py
@@ -86,17 +86,19 @@
   """Kill all running emulators that look like ones we started.
 
   There are odd 'sticky' cases where there can be no emulator process
-  running but a device slot is taken.  A little bot trouble and and
-  we're out of room forever.
+  running but a device slot is taken.  A little bot trouble and we're out of
+  room forever.
   """
-  emulators = [d for d in device_utils.HealthyDevices() if d.adb.is_emulator]
+  emulators = [d for d in device_utils.DeviceUtils.HealthyDevices()
+               if d.adb.is_emulator]
   if not emulators:
     return
   for e in emulators:
     e.adb.Emu(['kill'])
   logging.info('Emulator killing is async; give a few seconds for all to die.')
   for _ in range(5):
-    if not any(d.adb.is_emulator for d in device_utils.HealthyDevices()):
+    if not any(d.adb.is_emulator for d
+               in device_utils.DeviceUtils.HealthyDevices()):
       return
     time.sleep(1)
 
@@ -140,7 +142,8 @@
 def _GetAvailablePort():
   """Returns an available TCP port for the console."""
   used_ports = []
-  emulators = [d for d in device_utils.HealthyDevices() if d.adb.is_emulator]
+  emulators = [d for d in device_utils.DeviceUtils.HealthyDevices()
+               if d.adb.is_emulator]
   for emulator in emulators:
     used_ports.append(emulator.adb.GetDeviceSerial().split('-')[1])
   for port in PortPool.port_range():
diff --git a/build/android/test_runner.py b/build/android/test_runner.py
index 52de8fd..d508ef8 100755
--- a/build/android/test_runner.py
+++ b/build/android/test_runner.py
@@ -27,6 +27,8 @@
 from pylib.device import device_errors
 from pylib.device import device_utils
 from pylib.gtest import gtest_config
+# TODO(jbudorick): Remove this once we stop selectively enabling platform mode.
+from pylib.gtest import gtest_test_instance
 from pylib.gtest import setup as gtest_setup
 from pylib.gtest import test_options as gtest_test_options
 from pylib.linker import setup as linker_setup
@@ -215,6 +217,15 @@
                      dest='isolate_file_path',
                      help='.isolate file path to override the default '
                           'path')
+  group.add_argument('--app-data-file', action='append', dest='app_data_files',
+                     help='A file path relative to the app data directory '
+                          'that should be saved to the host.')
+  group.add_argument('--app-data-file-dir',
+                     help='Host directory to which app data files will be'
+                          ' saved. Used with --app-data-file.')
+  group.add_argument('--delete-stale-data', dest='delete_stale_data',
+                     action='store_true',
+                     help='Delete stale test data on the device.')
 
   filter_group = group.add_mutually_exclusive_group()
   filter_group.add_argument('-f', '--gtest_filter', '--gtest-filter',
@@ -335,6 +346,9 @@
                      dest='isolate_file_path',
                      help='.isolate file path to override the default '
                           'path')
+  group.add_argument('--delete-stale-data', dest='delete_stale_data',
+                     action='store_true',
+                     help='Delete stale test data on the device.')
 
   AddCommonOptions(parser)
   AddDeviceOptions(parser)
@@ -388,7 +402,8 @@
       args.test_support_apk_path,
       args.device_flags,
       args.isolate_file_path,
-      args.set_asserts
+      args.set_asserts,
+      args.delete_stale_data
       )
 
 
@@ -591,6 +606,11 @@
   group.add_argument(
       '--dry-run', action='store_true',
       help='Just print the steps without executing.')
+  # Uses 0.1 degrees C because that's what Android does.
+  group.add_argument(
+      '--max-battery-temp', type=int,
+      help='Only start tests when the battery is at or below the given '
+           'temperature (0.1 C)')
   group.add_argument('single_step_command', nargs='*', action=SingleStepAction,
                      help='If --single-step is specified, the command to run.')
   AddCommonOptions(parser)
@@ -615,7 +635,7 @@
       args.steps, args.flaky_steps, args.output_json_list,
       args.print_step, args.no_timeout, args.test_filter,
       args.dry_run, args.single_step, args.collect_chartjson_data,
-      args.output_chartjson_data)
+      args.output_chartjson_data, args.max_battery_temp)
 
 
 def AddPythonTestOptions(parser):
@@ -640,7 +660,10 @@
         args.test_arguments,
         args.timeout,
         args.isolate_file_path,
-        suite_name)
+        suite_name,
+        args.app_data_files,
+        args.app_data_file_dir,
+        args.delete_stale_data)
     runner_factory, tests = gtest_setup.Setup(gtest_options, devices)
 
     results, test_exit_code = test_dispatcher.RunTests(
@@ -911,6 +934,8 @@
     raise Exception('Failed to reset test server port.')
 
   if command == 'gtest':
+    if args.suite_name[0] in gtest_test_instance.BROWSER_TEST_SUITES:
+      return RunTestsInPlatformMode(args, parser)
     return _RunGTests(args, devices)
   elif command == 'linker':
     return _RunLinkerTests(args, devices)
diff --git a/build/gyp_chromium_test.py b/build/gyp_chromium_test.py.remove
similarity index 100%
rename from build/gyp_chromium_test.py
rename to build/gyp_chromium_test.py.remove