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