Home | History | Annotate | Download | only in android
      1 # Copyright 2015 The Chromium Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 """Provides a variety of device interactions with power.
      6 """
      7 # pylint: disable=unused-argument
      8 
      9 import collections
     10 import contextlib
     11 import csv
     12 import logging
     13 
     14 from devil.android import decorators
     15 from devil.android import device_errors
     16 from devil.android import device_utils
     17 from devil.android.sdk import version_codes
     18 from devil.utils import timeout_retry
     19 
     20 logger = logging.getLogger(__name__)
     21 
     22 _DEFAULT_TIMEOUT = 30
     23 _DEFAULT_RETRIES = 3
     24 
     25 
     26 _DEVICE_PROFILES = [
     27   {
     28     'name': ['Nexus 4'],
     29     'enable_command': (
     30         'echo 0 > /sys/module/pm8921_charger/parameters/disabled && '
     31         'dumpsys battery reset'),
     32     'disable_command': (
     33         'echo 1 > /sys/module/pm8921_charger/parameters/disabled && '
     34         'dumpsys battery set ac 0 && dumpsys battery set usb 0'),
     35     'charge_counter': None,
     36     'voltage': None,
     37     'current': None,
     38   },
     39   {
     40     'name': ['Nexus 5'],
     41     # Nexus 5
     42     # Setting the HIZ bit of the bq24192 causes the charger to actually ignore
     43     # energy coming from USB. Setting the power_supply offline just updates the
     44     # Android system to reflect that.
     45     'enable_command': (
     46         'echo 0x4A > /sys/kernel/debug/bq24192/INPUT_SRC_CONT && '
     47         'chmod 644 /sys/class/power_supply/usb/online && '
     48         'echo 1 > /sys/class/power_supply/usb/online && '
     49         'dumpsys battery reset'),
     50     'disable_command': (
     51         'echo 0xCA > /sys/kernel/debug/bq24192/INPUT_SRC_CONT && '
     52         'chmod 644 /sys/class/power_supply/usb/online && '
     53         'echo 0 > /sys/class/power_supply/usb/online && '
     54         'dumpsys battery set ac 0 && dumpsys battery set usb 0'),
     55     'charge_counter': None,
     56     'voltage': None,
     57     'current': None,
     58   },
     59   {
     60     'name': ['Nexus 6'],
     61     'enable_command': (
     62         'echo 1 > /sys/class/power_supply/battery/charging_enabled && '
     63         'dumpsys battery reset'),
     64     'disable_command': (
     65         'echo 0 > /sys/class/power_supply/battery/charging_enabled && '
     66         'dumpsys battery set ac 0 && dumpsys battery set usb 0'),
     67     'charge_counter': (
     68         '/sys/class/power_supply/max170xx_battery/charge_counter_ext'),
     69     'voltage': '/sys/class/power_supply/max170xx_battery/voltage_now',
     70     'current': '/sys/class/power_supply/max170xx_battery/current_now',
     71   },
     72   {
     73     'name': ['Nexus 9'],
     74     'enable_command': (
     75         'echo Disconnected > '
     76         '/sys/bus/i2c/drivers/bq2419x/0-006b/input_cable_state && '
     77         'dumpsys battery reset'),
     78     'disable_command': (
     79         'echo Connected > '
     80         '/sys/bus/i2c/drivers/bq2419x/0-006b/input_cable_state && '
     81         'dumpsys battery set ac 0 && dumpsys battery set usb 0'),
     82     'charge_counter': '/sys/class/power_supply/battery/charge_counter_ext',
     83     'voltage': '/sys/class/power_supply/battery/voltage_now',
     84     'current': '/sys/class/power_supply/battery/current_now',
     85   },
     86   {
     87     'name': ['Nexus 10'],
     88     'enable_command': None,
     89     'disable_command': None,
     90     'charge_counter': None,
     91     'voltage': '/sys/class/power_supply/ds2784-fuelgauge/voltage_now',
     92     'current': '/sys/class/power_supply/ds2784-fuelgauge/current_now',
     93 
     94   },
     95   {
     96     'name': ['Nexus 5X'],
     97     'enable_command': (
     98         'echo 1 > /sys/class/power_supply/battery/charging_enabled && '
     99         'dumpsys battery reset'),
    100     'disable_command': (
    101         'echo 0 > /sys/class/power_supply/battery/charging_enabled && '
    102         'dumpsys battery set ac 0 && dumpsys battery set usb 0'),
    103     'charge_counter': None,
    104     'voltage': None,
    105     'current': None,
    106   },
    107   { # Galaxy s5
    108     'name': ['SM-G900H'],
    109     'enable_command': (
    110         'chmod 644 /sys/class/power_supply/battery/test_mode && '
    111         'chmod 644 /sys/class/power_supply/sec-charger/current_now && '
    112         'echo 0 > /sys/class/power_supply/battery/test_mode && '
    113         'echo 9999 > /sys/class/power_supply/sec-charger/current_now &&'
    114         'dumpsys battery reset'),
    115     'disable_command': (
    116         'chmod 644 /sys/class/power_supply/battery/test_mode && '
    117         'chmod 644 /sys/class/power_supply/sec-charger/current_now && '
    118         'echo 1 > /sys/class/power_supply/battery/test_mode && '
    119         'echo 0 > /sys/class/power_supply/sec-charger/current_now && '
    120         'dumpsys battery set ac 0 && dumpsys battery set usb 0'),
    121     'charge_counter': None,
    122     'voltage': '/sys/class/power_supply/sec-fuelgauge/voltage_now',
    123     'current': '/sys/class/power_supply/sec-charger/current_now',
    124   },
    125   { # Galaxy s6, Galaxy s6, Galaxy s6 edge
    126     'name': ['SM-G920F', 'SM-G920V', 'SM-G925V'],
    127     'enable_command': (
    128         'chmod 644 /sys/class/power_supply/battery/test_mode && '
    129         'chmod 644 /sys/class/power_supply/max77843-charger/current_now && '
    130         'echo 0 > /sys/class/power_supply/battery/test_mode && '
    131         'echo 9999 > /sys/class/power_supply/max77843-charger/current_now &&'
    132         'dumpsys battery reset'),
    133     'disable_command': (
    134         'chmod 644 /sys/class/power_supply/battery/test_mode && '
    135         'chmod 644 /sys/class/power_supply/max77843-charger/current_now && '
    136         'echo 1 > /sys/class/power_supply/battery/test_mode && '
    137         'echo 0 > /sys/class/power_supply/max77843-charger/current_now && '
    138         'dumpsys battery set ac 0 && dumpsys battery set usb 0'),
    139     'charge_counter': None,
    140     'voltage': '/sys/class/power_supply/max77843-fuelgauge/voltage_now',
    141     'current': '/sys/class/power_supply/max77843-charger/current_now',
    142   },
    143   { # Cherry Mobile One
    144     'name': ['W6210 (4560MMX_b fingerprint)'],
    145     'enable_command': (
    146         'echo "0 0" > /proc/mtk_battery_cmd/current_cmd && '
    147         'dumpsys battery reset'),
    148     'disable_command': (
    149         'echo "0 1" > /proc/mtk_battery_cmd/current_cmd && '
    150         'dumpsys battery set ac 0 && dumpsys battery set usb 0'),
    151     'charge_counter': None,
    152     'voltage': None,
    153     'current': None,
    154 },
    155 ]
    156 
    157 # The list of useful dumpsys columns.
    158 # Index of the column containing the format version.
    159 _DUMP_VERSION_INDEX = 0
    160 # Index of the column containing the type of the row.
    161 _ROW_TYPE_INDEX = 3
    162 # Index of the column containing the uid.
    163 _PACKAGE_UID_INDEX = 4
    164 # Index of the column containing the application package.
    165 _PACKAGE_NAME_INDEX = 5
    166 # The column containing the uid of the power data.
    167 _PWI_UID_INDEX = 1
    168 # The column containing the type of consumption. Only consumption since last
    169 # charge are of interest here.
    170 _PWI_AGGREGATION_INDEX = 2
    171 _PWS_AGGREGATION_INDEX = _PWI_AGGREGATION_INDEX
    172 # The column containing the amount of power used, in mah.
    173 _PWI_POWER_CONSUMPTION_INDEX = 5
    174 _PWS_POWER_CONSUMPTION_INDEX = _PWI_POWER_CONSUMPTION_INDEX
    175 
    176 _MAX_CHARGE_ERROR = 20
    177 
    178 
    179 class BatteryUtils(object):
    180 
    181   def __init__(self, device, default_timeout=_DEFAULT_TIMEOUT,
    182                default_retries=_DEFAULT_RETRIES):
    183     """BatteryUtils constructor.
    184 
    185       Args:
    186         device: A DeviceUtils instance.
    187         default_timeout: An integer containing the default number of seconds to
    188                          wait for an operation to complete if no explicit value
    189                          is provided.
    190         default_retries: An integer containing the default number or times an
    191                          operation should be retried on failure if no explicit
    192                          value is provided.
    193       Raises:
    194         TypeError: If it is not passed a DeviceUtils instance.
    195     """
    196     if not isinstance(device, device_utils.DeviceUtils):
    197       raise TypeError('Must be initialized with DeviceUtils object.')
    198     self._device = device
    199     self._cache = device.GetClientCache(self.__class__.__name__)
    200     self._default_timeout = default_timeout
    201     self._default_retries = default_retries
    202 
    203   @decorators.WithTimeoutAndRetriesFromInstance()
    204   def SupportsFuelGauge(self, timeout=None, retries=None):
    205     """Detect if fuel gauge chip is present.
    206 
    207     Args:
    208       timeout: timeout in seconds
    209       retries: number of retries
    210 
    211     Returns:
    212       True if known fuel gauge files are present.
    213       False otherwise.
    214     """
    215     self._DiscoverDeviceProfile()
    216     return (self._cache['profile']['enable_command'] != None
    217         and self._cache['profile']['charge_counter'] != None)
    218 
    219   @decorators.WithTimeoutAndRetriesFromInstance()
    220   def GetFuelGaugeChargeCounter(self, timeout=None, retries=None):
    221     """Get value of charge_counter on fuel gauge chip.
    222 
    223     Device must have charging disabled for this, not just battery updates
    224     disabled. The only device that this currently works with is the nexus 5.
    225 
    226     Args:
    227       timeout: timeout in seconds
    228       retries: number of retries
    229 
    230     Returns:
    231       value of charge_counter for fuel gauge chip in units of nAh.
    232 
    233     Raises:
    234       device_errors.CommandFailedError: If fuel gauge chip not found.
    235     """
    236     if self.SupportsFuelGauge():
    237       return int(self._device.ReadFile(
    238           self._cache['profile']['charge_counter']))
    239     raise device_errors.CommandFailedError(
    240         'Unable to find fuel gauge.')
    241 
    242   @decorators.WithTimeoutAndRetriesFromInstance()
    243   def GetNetworkData(self, package, timeout=None, retries=None):
    244     """Get network data for specific package.
    245 
    246     Args:
    247       package: package name you want network data for.
    248       timeout: timeout in seconds
    249       retries: number of retries
    250 
    251     Returns:
    252       Tuple of (sent_data, recieved_data)
    253       None if no network data found
    254     """
    255     # If device_utils clears cache, cache['uids'] doesn't exist
    256     if 'uids' not in self._cache:
    257       self._cache['uids'] = {}
    258     if package not in self._cache['uids']:
    259       self.GetPowerData()
    260       if package not in self._cache['uids']:
    261         logger.warning('No UID found for %s. Can\'t get network data.',
    262                        package)
    263         return None
    264 
    265     network_data_path = '/proc/uid_stat/%s/' % self._cache['uids'][package]
    266     try:
    267       send_data = int(self._device.ReadFile(network_data_path + 'tcp_snd'))
    268     # If ReadFile throws exception, it means no network data usage file for
    269     # package has been recorded. Return 0 sent and 0 received.
    270     except device_errors.AdbShellCommandFailedError:
    271       logger.warning('No sent data found for package %s', package)
    272       send_data = 0
    273     try:
    274       recv_data = int(self._device.ReadFile(network_data_path + 'tcp_rcv'))
    275     except device_errors.AdbShellCommandFailedError:
    276       logger.warning('No received data found for package %s', package)
    277       recv_data = 0
    278     return (send_data, recv_data)
    279 
    280   @decorators.WithTimeoutAndRetriesFromInstance()
    281   def GetPowerData(self, timeout=None, retries=None):
    282     """Get power data for device.
    283 
    284     Args:
    285       timeout: timeout in seconds
    286       retries: number of retries
    287 
    288     Returns:
    289       Dict containing system power, and a per-package power dict keyed on
    290       package names.
    291       {
    292         'system_total': 23.1,
    293         'per_package' : {
    294           package_name: {
    295             'uid': uid,
    296             'data': [1,2,3]
    297           },
    298         }
    299       }
    300     """
    301     if 'uids' not in self._cache:
    302       self._cache['uids'] = {}
    303     dumpsys_output = self._device.RunShellCommand(
    304         ['dumpsys', 'batterystats', '-c'],
    305         check_return=True, large_output=True)
    306     csvreader = csv.reader(dumpsys_output)
    307     pwi_entries = collections.defaultdict(list)
    308     system_total = None
    309     for entry in csvreader:
    310       if entry[_DUMP_VERSION_INDEX] not in ['8', '9']:
    311         # Wrong dumpsys version.
    312         raise device_errors.DeviceVersionError(
    313             'Dumpsys version must be 8 or 9. "%s" found.'
    314             % entry[_DUMP_VERSION_INDEX])
    315       if _ROW_TYPE_INDEX < len(entry) and entry[_ROW_TYPE_INDEX] == 'uid':
    316         current_package = entry[_PACKAGE_NAME_INDEX]
    317         if (self._cache['uids'].get(current_package)
    318             and self._cache['uids'].get(current_package)
    319             != entry[_PACKAGE_UID_INDEX]):
    320           raise device_errors.CommandFailedError(
    321               'Package %s found multiple times with different UIDs %s and %s'
    322                % (current_package, self._cache['uids'][current_package],
    323                entry[_PACKAGE_UID_INDEX]))
    324         self._cache['uids'][current_package] = entry[_PACKAGE_UID_INDEX]
    325       elif (_PWI_POWER_CONSUMPTION_INDEX < len(entry)
    326           and entry[_ROW_TYPE_INDEX] == 'pwi'
    327           and entry[_PWI_AGGREGATION_INDEX] == 'l'):
    328         pwi_entries[entry[_PWI_UID_INDEX]].append(
    329             float(entry[_PWI_POWER_CONSUMPTION_INDEX]))
    330       elif (_PWS_POWER_CONSUMPTION_INDEX < len(entry)
    331           and entry[_ROW_TYPE_INDEX] == 'pws'
    332           and entry[_PWS_AGGREGATION_INDEX] == 'l'):
    333         # This entry should only appear once.
    334         assert system_total is None
    335         system_total = float(entry[_PWS_POWER_CONSUMPTION_INDEX])
    336 
    337     per_package = {p: {'uid': uid, 'data': pwi_entries[uid]}
    338                    for p, uid in self._cache['uids'].iteritems()}
    339     return {'system_total': system_total, 'per_package': per_package}
    340 
    341   @decorators.WithTimeoutAndRetriesFromInstance()
    342   def GetBatteryInfo(self, timeout=None, retries=None):
    343     """Gets battery info for the device.
    344 
    345     Args:
    346       timeout: timeout in seconds
    347       retries: number of retries
    348     Returns:
    349       A dict containing various battery information as reported by dumpsys
    350       battery.
    351     """
    352     result = {}
    353     # Skip the first line, which is just a header.
    354     for line in self._device.RunShellCommand(
    355         ['dumpsys', 'battery'], check_return=True)[1:]:
    356       # If usb charging has been disabled, an extra line of header exists.
    357       if 'UPDATES STOPPED' in line:
    358         logger.warning('Dumpsys battery not receiving updates. '
    359                        'Run dumpsys battery reset if this is in error.')
    360       elif ':' not in line:
    361         logger.warning('Unknown line found in dumpsys battery: "%s"', line)
    362       else:
    363         k, v = line.split(':', 1)
    364         result[k.strip()] = v.strip()
    365     return result
    366 
    367   @decorators.WithTimeoutAndRetriesFromInstance()
    368   def GetCharging(self, timeout=None, retries=None):
    369     """Gets the charging state of the device.
    370 
    371     Args:
    372       timeout: timeout in seconds
    373       retries: number of retries
    374     Returns:
    375       True if the device is charging, false otherwise.
    376     """
    377     battery_info = self.GetBatteryInfo()
    378     for k in ('AC powered', 'USB powered', 'Wireless powered'):
    379       if (k in battery_info and
    380           battery_info[k].lower() in ('true', '1', 'yes')):
    381         return True
    382     return False
    383 
    384   # TODO(rnephew): Make private when all use cases can use the context manager.
    385   @decorators.WithTimeoutAndRetriesFromInstance()
    386   def DisableBatteryUpdates(self, timeout=None, retries=None):
    387     """Resets battery data and makes device appear like it is not
    388     charging so that it will collect power data since last charge.
    389 
    390     Args:
    391       timeout: timeout in seconds
    392       retries: number of retries
    393 
    394     Raises:
    395       device_errors.CommandFailedError: When resetting batterystats fails to
    396         reset power values.
    397       device_errors.DeviceVersionError: If device is not L or higher.
    398     """
    399     def battery_updates_disabled():
    400       return self.GetCharging() is False
    401 
    402     self._ClearPowerData()
    403     self._device.RunShellCommand(['dumpsys', 'battery', 'set', 'ac', '0'],
    404                                  check_return=True)
    405     self._device.RunShellCommand(['dumpsys', 'battery', 'set', 'usb', '0'],
    406                                  check_return=True)
    407     timeout_retry.WaitFor(battery_updates_disabled, wait_period=1)
    408 
    409   # TODO(rnephew): Make private when all use cases can use the context manager.
    410   @decorators.WithTimeoutAndRetriesFromInstance()
    411   def EnableBatteryUpdates(self, timeout=None, retries=None):
    412     """Restarts device charging so that dumpsys no longer collects power data.
    413 
    414     Args:
    415       timeout: timeout in seconds
    416       retries: number of retries
    417 
    418     Raises:
    419       device_errors.DeviceVersionError: If device is not L or higher.
    420     """
    421     def battery_updates_enabled():
    422       return (self.GetCharging()
    423               or not bool('UPDATES STOPPED' in self._device.RunShellCommand(
    424                   ['dumpsys', 'battery'], check_return=True)))
    425 
    426     self._device.RunShellCommand(['dumpsys', 'battery', 'reset'],
    427                                  check_return=True)
    428     timeout_retry.WaitFor(battery_updates_enabled, wait_period=1)
    429 
    430   @contextlib.contextmanager
    431   def BatteryMeasurement(self, timeout=None, retries=None):
    432     """Context manager that enables battery data collection. It makes
    433     the device appear to stop charging so that dumpsys will start collecting
    434     power data since last charge. Once the with block is exited, charging is
    435     resumed and power data since last charge is no longer collected.
    436 
    437     Only for devices L and higher.
    438 
    439     Example usage:
    440       with BatteryMeasurement():
    441         browser_actions()
    442         get_power_data() # report usage within this block
    443       after_measurements() # Anything that runs after power
    444                            # measurements are collected
    445 
    446     Args:
    447       timeout: timeout in seconds
    448       retries: number of retries
    449 
    450     Raises:
    451       device_errors.DeviceVersionError: If device is not L or higher.
    452     """
    453     if self._device.build_version_sdk < version_codes.LOLLIPOP:
    454       raise device_errors.DeviceVersionError('Device must be L or higher.')
    455     try:
    456       self.DisableBatteryUpdates(timeout=timeout, retries=retries)
    457       yield
    458     finally:
    459       self.EnableBatteryUpdates(timeout=timeout, retries=retries)
    460 
    461   def _DischargeDevice(self, percent, wait_period=120):
    462     """Disables charging and waits for device to discharge given amount
    463 
    464     Args:
    465       percent: level of charge to discharge.
    466 
    467     Raises:
    468       ValueError: If percent is not between 1 and 99.
    469     """
    470     battery_level = int(self.GetBatteryInfo().get('level'))
    471     if not 0 < percent < 100:
    472       raise ValueError('Discharge amount(%s) must be between 1 and 99'
    473                        % percent)
    474     if battery_level is None:
    475       logger.warning('Unable to find current battery level. Cannot discharge.')
    476       return
    477     # Do not discharge if it would make battery level too low.
    478     if percent >= battery_level - 10:
    479       logger.warning('Battery is too low or discharge amount requested is too '
    480                      'high. Cannot discharge phone %s percent.', percent)
    481       return
    482 
    483     self._HardwareSetCharging(False)
    484 
    485     def device_discharged():
    486       self._HardwareSetCharging(True)
    487       current_level = int(self.GetBatteryInfo().get('level'))
    488       logger.info('current battery level: %s', current_level)
    489       if battery_level - current_level >= percent:
    490         return True
    491       self._HardwareSetCharging(False)
    492       return False
    493 
    494     timeout_retry.WaitFor(device_discharged, wait_period=wait_period)
    495 
    496   def ChargeDeviceToLevel(self, level, wait_period=60):
    497     """Enables charging and waits for device to be charged to given level.
    498 
    499     Args:
    500       level: level of charge to wait for.
    501       wait_period: time in seconds to wait between checking.
    502     Raises:
    503       device_errors.DeviceChargingError: If error while charging is detected.
    504     """
    505     self.SetCharging(True)
    506     charge_status = {
    507         'charge_failure_count': 0,
    508         'last_charge_value': 0
    509     }
    510     def device_charged():
    511       battery_level = self.GetBatteryInfo().get('level')
    512       if battery_level is None:
    513         logger.warning('Unable to find current battery level.')
    514         battery_level = 100
    515       else:
    516         logger.info('current battery level: %s', battery_level)
    517         battery_level = int(battery_level)
    518 
    519       # Use > so that it will not reset if charge is going down.
    520       if battery_level > charge_status['last_charge_value']:
    521         charge_status['last_charge_value'] = battery_level
    522         charge_status['charge_failure_count'] = 0
    523       else:
    524         charge_status['charge_failure_count'] += 1
    525 
    526       if (not battery_level >= level
    527           and charge_status['charge_failure_count'] >= _MAX_CHARGE_ERROR):
    528         raise device_errors.DeviceChargingError(
    529             'Device not charging properly. Current level:%s Previous level:%s'
    530              % (battery_level, charge_status['last_charge_value']))
    531       return battery_level >= level
    532 
    533     timeout_retry.WaitFor(device_charged, wait_period=wait_period)
    534 
    535   def LetBatteryCoolToTemperature(self, target_temp, wait_period=180):
    536     """Lets device sit to give battery time to cool down
    537     Args:
    538       temp: maximum temperature to allow in tenths of degrees c.
    539       wait_period: time in seconds to wait between checking.
    540     """
    541     def cool_device():
    542       temp = self.GetBatteryInfo().get('temperature')
    543       if temp is None:
    544         logger.warning('Unable to find current battery temperature.')
    545         temp = 0
    546       else:
    547         logger.info('Current battery temperature: %s', temp)
    548       if int(temp) <= target_temp:
    549         return True
    550       else:
    551         if 'Nexus 5' in self._cache['profile']['name']:
    552           self._DischargeDevice(1)
    553         return False
    554 
    555     self._DiscoverDeviceProfile()
    556     self.EnableBatteryUpdates()
    557     logger.info('Waiting for the device to cool down to %s (0.1 C)',
    558                 target_temp)
    559     timeout_retry.WaitFor(cool_device, wait_period=wait_period)
    560 
    561   @decorators.WithTimeoutAndRetriesFromInstance()
    562   def SetCharging(self, enabled, timeout=None, retries=None):
    563     """Enables or disables charging on the device.
    564 
    565     Args:
    566       enabled: A boolean indicating whether charging should be enabled or
    567         disabled.
    568       timeout: timeout in seconds
    569       retries: number of retries
    570     """
    571     if self.GetCharging() == enabled:
    572       logger.warning('Device charging already in expected state: %s', enabled)
    573       return
    574 
    575     self._DiscoverDeviceProfile()
    576     if enabled:
    577       if self._cache['profile']['enable_command']:
    578         self._HardwareSetCharging(enabled)
    579       else:
    580         logger.info('Unable to enable charging via hardware. '
    581                     'Falling back to software enabling.')
    582         self.EnableBatteryUpdates()
    583     else:
    584       if self._cache['profile']['enable_command']:
    585         self._ClearPowerData()
    586         self._HardwareSetCharging(enabled)
    587       else:
    588         logger.info('Unable to disable charging via hardware. '
    589                      'Falling back to software disabling.')
    590         self.DisableBatteryUpdates()
    591 
    592   def _HardwareSetCharging(self, enabled, timeout=None, retries=None):
    593     """Enables or disables charging on the device.
    594 
    595     Args:
    596       enabled: A boolean indicating whether charging should be enabled or
    597         disabled.
    598       timeout: timeout in seconds
    599       retries: number of retries
    600 
    601     Raises:
    602       device_errors.CommandFailedError: If method of disabling charging cannot
    603         be determined.
    604     """
    605     self._DiscoverDeviceProfile()
    606     if not self._cache['profile']['enable_command']:
    607       raise device_errors.CommandFailedError(
    608           'Unable to find charging commands.')
    609 
    610     command = (self._cache['profile']['enable_command'] if enabled
    611                else self._cache['profile']['disable_command'])
    612 
    613     def verify_charging():
    614       return self.GetCharging() == enabled
    615 
    616     self._device.RunShellCommand(
    617         command, shell=True, check_return=True, as_root=True, large_output=True)
    618     timeout_retry.WaitFor(verify_charging, wait_period=1)
    619 
    620   @contextlib.contextmanager
    621   def PowerMeasurement(self, timeout=None, retries=None):
    622     """Context manager that enables battery power collection.
    623 
    624     Once the with block is exited, charging is resumed. Will attempt to disable
    625     charging at the hardware level, and if that fails will fall back to software
    626     disabling of battery updates.
    627 
    628     Only for devices L and higher.
    629 
    630     Example usage:
    631       with PowerMeasurement():
    632         browser_actions()
    633         get_power_data() # report usage within this block
    634       after_measurements() # Anything that runs after power
    635                            # measurements are collected
    636 
    637     Args:
    638       timeout: timeout in seconds
    639       retries: number of retries
    640     """
    641     try:
    642       self.SetCharging(False, timeout=timeout, retries=retries)
    643       yield
    644     finally:
    645       self.SetCharging(True, timeout=timeout, retries=retries)
    646 
    647   def _ClearPowerData(self):
    648     """Resets battery data and makes device appear like it is not
    649     charging so that it will collect power data since last charge.
    650 
    651     Returns:
    652       True if power data cleared.
    653       False if power data clearing is not supported (pre-L)
    654 
    655     Raises:
    656       device_errors.DeviceVersionError: If power clearing is supported,
    657         but fails.
    658     """
    659     if self._device.build_version_sdk < version_codes.LOLLIPOP:
    660       logger.warning('Dumpsys power data only available on 5.0 and above. '
    661                      'Cannot clear power data.')
    662       return False
    663 
    664     self._device.RunShellCommand(
    665         ['dumpsys', 'battery', 'set', 'usb', '1'], check_return=True)
    666     self._device.RunShellCommand(
    667         ['dumpsys', 'battery', 'set', 'ac', '1'], check_return=True)
    668 
    669     def test_if_clear():
    670       self._device.RunShellCommand(
    671           ['dumpsys', 'batterystats', '--reset'], check_return=True)
    672       battery_data = self._device.RunShellCommand(
    673           ['dumpsys', 'batterystats', '--charged', '-c'],
    674           check_return=True, large_output=True)
    675       for line in battery_data:
    676         l = line.split(',')
    677         if (len(l) > _PWI_POWER_CONSUMPTION_INDEX
    678             and l[_ROW_TYPE_INDEX] == 'pwi'
    679             and float(l[_PWI_POWER_CONSUMPTION_INDEX]) != 0.0):
    680           return False
    681       return True
    682 
    683     try:
    684       timeout_retry.WaitFor(test_if_clear, wait_period=1)
    685       return True
    686     finally:
    687       self._device.RunShellCommand(
    688           ['dumpsys', 'battery', 'reset'], check_return=True)
    689 
    690   def _DiscoverDeviceProfile(self):
    691     """Checks and caches device information.
    692 
    693     Returns:
    694       True if profile is found, false otherwise.
    695     """
    696 
    697     if 'profile' in self._cache:
    698       return True
    699     for profile in _DEVICE_PROFILES:
    700       if self._device.product_model in profile['name']:
    701         self._cache['profile'] = profile
    702         return True
    703     self._cache['profile'] = {
    704         'name': [],
    705         'enable_command': None,
    706         'disable_command': None,
    707         'charge_counter': None,
    708         'voltage': None,
    709         'current': None,
    710     }
    711     return False
    712