| Index: build/android/pylib/device/battery_utils.py
 | 
| diff --git a/build/android/pylib/device/battery_utils.py b/build/android/pylib/device/battery_utils.py
 | 
| new file mode 100644
 | 
| index 0000000000000000000000000000000000000000..cca2cf92cfdcc1f83b923ef4f0a331189b61ba39
 | 
| --- /dev/null
 | 
| +++ b/build/android/pylib/device/battery_utils.py
 | 
| @@ -0,0 +1,386 @@
 | 
| +# 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.
 | 
| +
 | 
| +"""Provides a variety of device interactions with power.
 | 
| +"""
 | 
| +# pylint: disable=unused-argument
 | 
| +
 | 
| +import collections
 | 
| +import contextlib
 | 
| +import csv
 | 
| +import logging
 | 
| +
 | 
| +from pylib import constants
 | 
| +from pylib.device import decorators
 | 
| +from pylib.device import device_errors
 | 
| +from pylib.device import device_utils
 | 
| +from pylib.utils import timeout_retry
 | 
| +
 | 
| +_DEFAULT_TIMEOUT = 30
 | 
| +_DEFAULT_RETRIES = 3
 | 
| +
 | 
| +_CONTROL_CHARGING_COMMANDS = [
 | 
| +  {
 | 
| +    # 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',
 | 
| +  },
 | 
| +  {
 | 
| +    # 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
 | 
| +    # Android system to reflect that.
 | 
| +    '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'),
 | 
| +  },
 | 
| +]
 | 
| +
 | 
| +# The list of useful dumpsys columns.
 | 
| +# Index of the column containing the format version.
 | 
| +_DUMP_VERSION_INDEX = 0
 | 
| +# Index of the column containing the type of the row.
 | 
| +_ROW_TYPE_INDEX = 3
 | 
| +# Index of the column containing the uid.
 | 
| +_PACKAGE_UID_INDEX = 4
 | 
| +# Index of the column containing the application package.
 | 
| +_PACKAGE_NAME_INDEX = 5
 | 
| +# The column containing the uid of the power data.
 | 
| +_PWI_UID_INDEX = 1
 | 
| +# The column containing the type of consumption. Only consumtion since last
 | 
| +# charge are of interest here.
 | 
| +_PWI_AGGREGATION_INDEX = 2
 | 
| +# The column containing the amount of power used, in mah.
 | 
| +_PWI_POWER_CONSUMPTION_INDEX = 5
 | 
| +
 | 
| +
 | 
| +class BatteryUtils(object):
 | 
| +
 | 
| +  def __init__(self, device, default_timeout=_DEFAULT_TIMEOUT,
 | 
| +               default_retries=_DEFAULT_RETRIES):
 | 
| +    """BatteryUtils constructor.
 | 
| +
 | 
| +      Args:
 | 
| +        device: A DeviceUtils instance.
 | 
| +        default_timeout: An integer containing the default number of seconds to
 | 
| +                         wait for an operation to complete if no explicit value
 | 
| +                         is provided.
 | 
| +        default_retries: An integer containing the default number or times an
 | 
| +                         operation should be retried on failure if no explicit
 | 
| +                         value is provided.
 | 
| +
 | 
| +      Raises:
 | 
| +        TypeError: If it is not passed a DeviceUtils instance.
 | 
| +    """
 | 
| +    if not isinstance(device, device_utils.DeviceUtils):
 | 
| +      raise TypeError('Must be initialized with DeviceUtils object.')
 | 
| +    self._device = device
 | 
| +    self._cache = device.GetClientCache(self.__class__.__name__)
 | 
| +    self._default_timeout = default_timeout
 | 
| +    self._default_retries = default_retries
 | 
| +
 | 
| +  @decorators.WithTimeoutAndRetriesFromInstance()
 | 
| +  def GetNetworkData(self, package, timeout=None, retries=None):
 | 
| +    """ Get network data for specific package.
 | 
| +
 | 
| +    Args:
 | 
| +      package: package name you want network data for.
 | 
| +      timeout: timeout in seconds
 | 
| +      retries: number of retries
 | 
| +
 | 
| +    Returns:
 | 
| +      Tuple of (sent_data, recieved_data)
 | 
| +      None if no network data found
 | 
| +    """
 | 
| +    # If device_utils clears cache, cache['uids'] doesn't exist
 | 
| +    if 'uids' not in self._cache:
 | 
| +      self._cache['uids'] = {}
 | 
| +    if package not in self._cache['uids']:
 | 
| +      self.GetPowerData()
 | 
| +      if package not in self._cache['uids']:
 | 
| +        logging.warning('No UID found for %s. Can\'t get network data.',
 | 
| +                        package)
 | 
| +        return None
 | 
| +
 | 
| +    network_data_path = '/proc/uid_stat/%s/' % self._cache['uids'][package]
 | 
| +    try:
 | 
| +      send_data = int(self._device.ReadFile(network_data_path + 'tcp_snd'))
 | 
| +    # If ReadFile throws exception, it means no network data usage file for
 | 
| +    # package has been recorded. Return 0 sent and 0 received.
 | 
| +    except device_errors.AdbShellCommandFailedError:
 | 
| +      logging.warning('No sent data found for package %s', package)
 | 
| +      send_data = 0
 | 
| +    try:
 | 
| +      recv_data = int(self._device.ReadFile(network_data_path + 'tcp_rcv'))
 | 
| +    except device_errors.AdbShellCommandFailedError:
 | 
| +      logging.warning('No received data found for package %s', package)
 | 
| +      recv_data = 0
 | 
| +    return (send_data, recv_data)
 | 
| +
 | 
| +  @decorators.WithTimeoutAndRetriesFromInstance()
 | 
| +  def GetPowerData(self, timeout=None, retries=None):
 | 
| +    """ Get power data for device.
 | 
| +    Args:
 | 
| +      timeout: timeout in seconds
 | 
| +      retries: number of retries
 | 
| +
 | 
| +    Returns:
 | 
| +      Dict of power data, keyed on package names.
 | 
| +      {
 | 
| +        package_name: {
 | 
| +          'uid': uid,
 | 
| +          'data': [1,2,3]
 | 
| +        },
 | 
| +      }
 | 
| +    """
 | 
| +    if 'uids' not in self._cache:
 | 
| +      self._cache['uids'] = {}
 | 
| +    dumpsys_output = self._device.RunShellCommand(
 | 
| +        ['dumpsys', 'batterystats', '-c'], check_return=True)
 | 
| +    csvreader = csv.reader(dumpsys_output)
 | 
| +    pwi_entries = collections.defaultdict(list)
 | 
| +    for entry in csvreader:
 | 
| +      if entry[_DUMP_VERSION_INDEX] not in ['8', '9']:
 | 
| +        # Wrong dumpsys version.
 | 
| +        raise device_errors.DeviceVersionError(
 | 
| +            'Dumpsys version must be 8 or 9. %s found.'
 | 
| +            % entry[_DUMP_VERSION_INDEX])
 | 
| +      if _ROW_TYPE_INDEX < len(entry) and entry[_ROW_TYPE_INDEX] == 'uid':
 | 
| +        current_package = entry[_PACKAGE_NAME_INDEX]
 | 
| +        if (self._cache['uids'].get(current_package)
 | 
| +            and self._cache['uids'].get(current_package)
 | 
| +            != entry[_PACKAGE_UID_INDEX]):
 | 
| +          raise device_errors.CommandFailedError(
 | 
| +              'Package %s found multiple times with differnt UIDs %s and %s'
 | 
| +               % (current_package, self._cache['uids'][current_package],
 | 
| +               entry[_PACKAGE_UID_INDEX]))
 | 
| +        self._cache['uids'][current_package] = entry[_PACKAGE_UID_INDEX]
 | 
| +      elif (_PWI_POWER_CONSUMPTION_INDEX < len(entry)
 | 
| +          and entry[_ROW_TYPE_INDEX] == 'pwi'
 | 
| +          and entry[_PWI_AGGREGATION_INDEX] == 'l'):
 | 
| +        pwi_entries[entry[_PWI_UID_INDEX]].append(
 | 
| +            float(entry[_PWI_POWER_CONSUMPTION_INDEX]))
 | 
| +
 | 
| +    return {p: {'uid': uid, 'data': pwi_entries[uid]}
 | 
| +            for p, uid in self._cache['uids'].iteritems()}
 | 
| +
 | 
| +  @decorators.WithTimeoutAndRetriesFromInstance()
 | 
| +  def GetPackagePowerData(self, package, timeout=None, retries=None):
 | 
| +    """ Get power data for particular package.
 | 
| +
 | 
| +    Args:
 | 
| +      package: Package to get power data on.
 | 
| +
 | 
| +    returns:
 | 
| +      Dict of UID and power data.
 | 
| +      {
 | 
| +        'uid': uid,
 | 
| +        'data': [1,2,3]
 | 
| +      }
 | 
| +      None if the package is not found in the power data.
 | 
| +    """
 | 
| +    return self.GetPowerData().get(package)
 | 
| +
 | 
| +  @decorators.WithTimeoutAndRetriesFromInstance()
 | 
| +  def GetBatteryInfo(self, timeout=None, retries=None):
 | 
| +    """Gets battery info for the device.
 | 
| +
 | 
| +    Args:
 | 
| +      timeout: timeout in seconds
 | 
| +      retries: number of retries
 | 
| +    Returns:
 | 
| +      A dict containing various battery information as reported by dumpsys
 | 
| +      battery.
 | 
| +    """
 | 
| +    result = {}
 | 
| +    # Skip the first line, which is just a header.
 | 
| +    for line in self._device.RunShellCommand(
 | 
| +        ['dumpsys', 'battery'], check_return=True)[1:]:
 | 
| +      # If usb charging has been disabled, an extra line of header exists.
 | 
| +      if 'UPDATES STOPPED' in line:
 | 
| +        logging.warning('Dumpsys battery not receiving updates. '
 | 
| +                        'Run dumpsys battery reset if this is in error.')
 | 
| +      elif ':' not in line:
 | 
| +        logging.warning('Unknown line found in dumpsys battery: "%s"', line)
 | 
| +      else:
 | 
| +        k, v = line.split(':', 1)
 | 
| +        result[k.strip()] = v.strip()
 | 
| +    return result
 | 
| +
 | 
| +  @decorators.WithTimeoutAndRetriesFromInstance()
 | 
| +  def GetCharging(self, timeout=None, retries=None):
 | 
| +    """Gets the charging state of the device.
 | 
| +
 | 
| +    Args:
 | 
| +      timeout: timeout in seconds
 | 
| +      retries: number of retries
 | 
| +    Returns:
 | 
| +      True if the device is charging, false otherwise.
 | 
| +    """
 | 
| +    battery_info = self.GetBatteryInfo()
 | 
| +    for k in ('AC powered', 'USB powered', 'Wireless powered'):
 | 
| +      if (k in battery_info and
 | 
| +          battery_info[k].lower() in ('true', '1', 'yes')):
 | 
| +        return True
 | 
| +    return False
 | 
| +
 | 
| +  @decorators.WithTimeoutAndRetriesFromInstance()
 | 
| +  def SetCharging(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
 | 
| +
 | 
| +    Raises:
 | 
| +      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.')
 | 
| +
 | 
| +    if enabled:
 | 
| +      command = self._cache['charging_config']['enable_command']
 | 
| +    else:
 | 
| +      command = self._cache['charging_config']['disable_command']
 | 
| +
 | 
| +    def set_and_verify_charging():
 | 
| +      self._device.RunShellCommand(command, check_return=True)
 | 
| +      return self.GetCharging() == enabled
 | 
| +
 | 
| +    timeout_retry.WaitFor(set_and_verify_charging, wait_period=1)
 | 
| +
 | 
| +  # 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
 | 
| +    charging so that it will collect power data since last charge.
 | 
| +
 | 
| +    Args:
 | 
| +      timeout: timeout in seconds
 | 
| +      retries: number of retries
 | 
| +
 | 
| +    Raises:
 | 
| +      device_errors.CommandFailedError: When resetting batterystats fails to
 | 
| +        reset power values.
 | 
| +      device_errors.DeviceVersionError: If device is not L or higher.
 | 
| +    """
 | 
| +    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', 'reset'], check_return=True)
 | 
| +    self._device.RunShellCommand(
 | 
| +        ['dumpsys', 'batterystats', '--reset'], check_return=True)
 | 
| +    battery_data = self._device.RunShellCommand(
 | 
| +        ['dumpsys', 'batterystats', '--charged', '--checkin'],
 | 
| +        check_return=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._device.RunShellCommand(['dumpsys', 'battery', 'set', 'ac', '0'],
 | 
| +                                 check_return=True)
 | 
| +    self._device.RunShellCommand(['dumpsys', 'battery', 'set', 'usb', '0'],
 | 
| +                                 check_return=True)
 | 
| +    timeout_retry.WaitFor(battery_updates_disabled, wait_period=1)
 | 
| +
 | 
| +  # 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.
 | 
| +
 | 
| +    Args:
 | 
| +      timeout: timeout in seconds
 | 
| +      retries: number of retries
 | 
| +
 | 
| +    Raises:
 | 
| +      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.')
 | 
| +
 | 
| +    self._device.RunShellCommand(['dumpsys', 'battery', 'reset'],
 | 
| +                                 check_return=True)
 | 
| +    timeout_retry.WaitFor(battery_updates_enabled, wait_period=1)
 | 
| +
 | 
| +  @contextlib.contextmanager
 | 
| +  def BatteryMeasurement(self, timeout=None, retries=None):
 | 
| +    """Context manager that enables battery data collection. It makes
 | 
| +    the device appear to stop charging so that dumpsys will start collecting
 | 
| +    power data since last charge. Once the with block is exited, charging is
 | 
| +    resumed and power data since last charge is no longer collected.
 | 
| +
 | 
| +    Only for devices L and higher.
 | 
| +
 | 
| +    Example usage:
 | 
| +      with BatteryMeasurement():
 | 
| +        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
 | 
| +
 | 
| +    Raises:
 | 
| +      device_errors.DeviceVersionError: If device is not L or higher.
 | 
| +    """
 | 
| +    if (self._device.build_version_sdk <
 | 
| +        constants.ANDROID_SDK_VERSION_CODES.LOLLIPOP):
 | 
| +      raise device_errors.DeviceVersionError('Device must be L or higher.')
 | 
| +    try:
 | 
| +      self.DisableBatteryUpdates(timeout=timeout, retries=retries)
 | 
| +      yield
 | 
| +    finally:
 | 
| +      self.EnableBatteryUpdates(timeout=timeout, retries=retries)
 | 
| +
 | 
| +  def ChargeDeviceToLevel(self, level, wait_period=60):
 | 
| +    """Enables charging and waits for device to be charged to given level.
 | 
| +
 | 
| +    Args:
 | 
| +      level: level of charge to wait for.
 | 
| +      wait_period: time in seconds to wait between checking.
 | 
| +    """
 | 
| +    self.SetCharging(True)
 | 
| +
 | 
| +    def device_charged():
 | 
| +      battery_level = self.GetBatteryInfo().get('level')
 | 
| +      if battery_level is None:
 | 
| +        logging.warning('Unable to find current battery level.')
 | 
| +        battery_level = 100
 | 
| +      else:
 | 
| +        logging.info('current battery level: %s', battery_level)
 | 
| +        battery_level = int(battery_level)
 | 
| +      return battery_level >= level
 | 
| +
 | 
| +    timeout_retry.WaitFor(device_charged, wait_period=wait_period)
 | 
| 
 |