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