• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2016 Google Inc.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15import contextlib
16import enum
17import functools
18import logging
19import os
20import re
21import shutil
22import time
23
24from mobly import logger as mobly_logger
25from mobly import runtime_test_info
26from mobly import utils
27from mobly.controllers.android_device_lib import adb
28from mobly.controllers.android_device_lib import errors
29from mobly.controllers.android_device_lib import fastboot
30from mobly.controllers.android_device_lib import service_manager
31from mobly.controllers.android_device_lib.services import logcat
32from mobly.controllers.android_device_lib.services import snippet_management_service
33
34# Convenience constant for the package of Mobly Bundled Snippets
35# (http://github.com/google/mobly-bundled-snippets).
36MBS_PACKAGE = 'com.google.android.mobly.snippet.bundled'
37
38MOBLY_CONTROLLER_CONFIG_NAME = 'AndroidDevice'
39
40ANDROID_DEVICE_PICK_ALL_TOKEN = '*'
41_DEBUG_PREFIX_TEMPLATE = '[AndroidDevice|%s] %s'
42
43# Key name for adb logcat extra params in config file.
44ANDROID_DEVICE_ADB_LOGCAT_PARAM_KEY = 'adb_logcat_param'
45ANDROID_DEVICE_EMPTY_CONFIG_MSG = 'Configuration is empty, abort!'
46ANDROID_DEVICE_NOT_LIST_CONFIG_MSG = 'Configuration should be a list, abort!'
47
48# System properties that are cached by the `AndroidDevice.build_info` property.
49# The only properties on this list should be read-only system properties.
50CACHED_SYSTEM_PROPS = [
51    'ro.build.id',
52    'ro.build.type',
53    'ro.build.fingerprint',
54    'ro.build.version.codename',
55    'ro.build.version.incremental',
56    'ro.build.version.sdk',
57    'ro.build.product',
58    'ro.build.characteristics',
59    'ro.debuggable',
60    'ro.product.name',
61    'ro.hardware',
62]
63
64# Keys for attributes in configs that alternate the controller module behavior.
65# If this is False for a device, errors from that device will be ignored
66# during `create`. Default is True.
67KEY_DEVICE_REQUIRED = 'required'
68DEFAULT_VALUE_DEVICE_REQUIRED = True
69# If True, logcat collection will not be started during `create`.
70# Default is False.
71KEY_SKIP_LOGCAT = 'skip_logcat'
72DEFAULT_VALUE_SKIP_LOGCAT = False
73SERVICE_NAME_LOGCAT = 'logcat'
74
75# Default name for bug reports taken without a specified test name.
76DEFAULT_BUG_REPORT_NAME = 'bugreport'
77
78# Default Timeout to wait for boot completion
79DEFAULT_TIMEOUT_BOOT_COMPLETION_SECOND = 15 * 60
80
81# Timeout for the adb command for taking a screenshot
82TAKE_SCREENSHOT_TIMEOUT_SECOND = 10
83
84# Aliases of error types for backward compatibility.
85Error = errors.Error
86DeviceError = errors.DeviceError
87SnippetError = snippet_management_service.Error
88
89# Regex to heuristically determine if the device is an emulator.
90EMULATOR_SERIAL_REGEX = re.compile(r'emulator-\d+')
91
92
93def create(configs):
94  """Creates AndroidDevice controller objects.
95
96  Args:
97    configs: A list of dicts, each representing a configuration for an
98      Android device.
99
100  Returns:
101    A list of AndroidDevice objects.
102  """
103  if not configs:
104    raise Error(ANDROID_DEVICE_EMPTY_CONFIG_MSG)
105  elif configs == ANDROID_DEVICE_PICK_ALL_TOKEN:
106    ads = get_all_instances()
107  elif not isinstance(configs, list):
108    raise Error(ANDROID_DEVICE_NOT_LIST_CONFIG_MSG)
109  elif isinstance(configs[0], dict):
110    # Configs is a list of dicts.
111    ads = get_instances_with_configs(configs)
112  elif isinstance(configs[0], str):
113    # Configs is a list of strings representing serials.
114    ads = get_instances(configs)
115  else:
116    raise Error('No valid config found in: %s' % configs)
117  _start_services_on_ads(ads)
118  return ads
119
120
121def destroy(ads):
122  """Cleans up AndroidDevice objects.
123
124  Args:
125    ads: A list of AndroidDevice objects.
126  """
127  for ad in ads:
128    try:
129      ad.services.stop_all()
130    except Exception:
131      ad.log.exception('Failed to clean up properly.')
132
133
134def get_info(ads):
135  """Get information on a list of AndroidDevice objects.
136
137  Args:
138    ads: A list of AndroidDevice objects.
139
140  Returns:
141    A list of dict, each representing info for an AndroidDevice objects.
142    Everything in this dict should be yaml serializable.
143  """
144  infos = []
145  # The values of user_added_info can be arbitrary types, so we shall sanitize
146  # them here to ensure they are yaml serializable.
147  for ad in ads:
148    device_info = ad.device_info
149    user_added_info = {
150        k: str(v) for (k, v) in device_info['user_added_info'].items()
151    }
152    device_info['user_added_info'] = user_added_info
153    infos.append(device_info)
154  return infos
155
156
157def _validate_device_existence(serials):
158  """Validate that all the devices specified by the configs can be reached.
159
160  Args:
161    serials: list of strings, the serials of all the devices that are expected
162      to exist.
163  """
164  valid_ad_identifiers = (
165      list_adb_devices()
166      + list_adb_devices_by_usb_id()
167      + list_fastboot_devices()
168  )
169  for serial in serials:
170    if serial not in valid_ad_identifiers:
171      raise Error(
172          f'Android device serial "{serial}" is specified in '
173          'config but is not reachable.'
174      )
175
176
177def _start_services_on_ads(ads):
178  """Starts long running services on multiple AndroidDevice objects.
179
180  If any one AndroidDevice object fails to start services, cleans up all
181  AndroidDevice objects and their services.
182
183  Args:
184    ads: A list of AndroidDevice objects whose services to start.
185  """
186  for ad in ads:
187    start_logcat = not getattr(ad, KEY_SKIP_LOGCAT, DEFAULT_VALUE_SKIP_LOGCAT)
188    try:
189      if start_logcat:
190        ad.services.logcat.start()
191    except Exception:
192      is_required = getattr(
193          ad, KEY_DEVICE_REQUIRED, DEFAULT_VALUE_DEVICE_REQUIRED
194      )
195      if is_required:
196        ad.log.exception('Failed to start some services, abort!')
197        destroy(ads)
198        raise
199      else:
200        ad.log.exception(
201            'Skipping this optional device because some '
202            'services failed to start.'
203        )
204
205
206def parse_device_list(device_list_str, key=None):
207  """Parses a byte string representing a list of devices.
208
209  The string is generated by calling either adb or fastboot. The tokens in
210  each string is tab-separated.
211
212  Args:
213    device_list_str: Output of adb or fastboot.
214    key: The token that signifies a device in device_list_str. Only devices
215      with the specified key in device_list_str are parsed, such as 'device' or
216      'fastbootd'. If not specified, all devices listed are parsed.
217
218  Returns:
219    A list of android device serial numbers.
220  """
221  try:
222    clean_lines = str(device_list_str, 'utf-8').strip().split('\n')
223  except UnicodeDecodeError:
224    logging.warning('unicode decode error, origin str: %s', device_list_str)
225    raise
226  results = []
227  for line in clean_lines:
228    tokens = line.strip().split('\t')
229    if len(tokens) == 2 and (key is None or tokens[1] == key):
230      results.append(tokens[0])
231  return results
232
233
234def list_adb_devices():
235  """List all android devices connected to the computer that are detected by
236  adb.
237
238  Returns:
239    A list of android device serials. Empty if there's none.
240  """
241  out = adb.AdbProxy().devices()
242  return parse_device_list(out, 'device')
243
244
245def list_adb_devices_by_usb_id():
246  """List the usb id of all android devices connected to the computer that
247  are detected by adb.
248
249  Returns:
250    A list of strings that are android device usb ids. Empty if there's
251    none.
252  """
253  out = adb.AdbProxy().devices(['-l'])
254  clean_lines = str(out, 'utf-8').strip().split('\n')
255  results = []
256  for line in clean_lines:
257    tokens = line.strip().split()
258    if len(tokens) > 2 and tokens[1] == 'device':
259      results.append(tokens[2])
260  return results
261
262
263def list_fastboot_devices():
264  """List all android devices connected to the computer that are in in
265  fastboot mode. These are detected by fastboot.
266
267  This function doesn't raise any error if `fastboot` binary doesn't exist,
268  because `FastbootProxy` itself doesn't raise any error.
269
270  Returns:
271    A list of android device serials. Empty if there's none.
272  """
273  out = fastboot.FastbootProxy().devices()
274  return parse_device_list(out)
275
276
277def get_instances(serials):
278  """Create AndroidDevice instances from a list of serials.
279
280  Args:
281    serials: A list of android device serials.
282
283  Returns:
284    A list of AndroidDevice objects.
285  """
286  _validate_device_existence(serials)
287
288  results = []
289  for s in serials:
290    results.append(AndroidDevice(s))
291  return results
292
293
294def get_instances_with_configs(configs):
295  """Create AndroidDevice instances from a list of dict configs.
296
297  Each config should have the required key-value pair 'serial'.
298
299  Args:
300    configs: A list of dicts each representing the configuration of one
301      android device.
302
303  Returns:
304    A list of AndroidDevice objects.
305  """
306  # First make sure each config contains a serial, and all the serials'
307  # corresponding devices exist.
308  serials = []
309  for c in configs:
310    try:
311      serials.append(c['serial'])
312    except KeyError:
313      raise Error(
314          'Required value "serial" is missing in AndroidDevice config %s.' % c
315      )
316  _validate_device_existence(serials)
317  results = []
318  for c in configs:
319    serial = c.pop('serial')
320    is_required = c.get(KEY_DEVICE_REQUIRED, True)
321    try:
322      ad = AndroidDevice(serial)
323      ad.load_config(c)
324    except Exception:
325      if is_required:
326        raise
327      ad.log.exception('Skipping this optional device due to error.')
328      continue
329    results.append(ad)
330  return results
331
332
333def get_all_instances(include_fastboot=False):
334  """Create AndroidDevice instances for all attached android devices.
335
336  Args:
337    include_fastboot: Whether to include devices in bootloader mode or not.
338
339  Returns:
340    A list of AndroidDevice objects each representing an android device
341    attached to the computer.
342  """
343  if include_fastboot:
344    serial_list = list_adb_devices() + list_fastboot_devices()
345    return get_instances(serial_list)
346  return get_instances(list_adb_devices())
347
348
349def filter_devices(ads, func):
350  """Finds the AndroidDevice instances from a list that match certain
351  conditions.
352
353  Args:
354    ads: A list of AndroidDevice instances.
355    func: A function that takes an AndroidDevice object and returns True
356      if the device satisfies the filter condition.
357
358  Returns:
359    A list of AndroidDevice instances that satisfy the filter condition.
360  """
361  results = []
362  for ad in ads:
363    if func(ad):
364      results.append(ad)
365  return results
366
367
368def get_devices(ads, **kwargs):
369  """Finds a list of AndroidDevice instance from a list that has specific
370  attributes of certain values.
371
372  Example:
373    get_devices(android_devices, label='foo', phone_number='1234567890')
374    get_devices(android_devices, model='angler')
375
376  Args:
377    ads: A list of AndroidDevice instances.
378    kwargs: keyword arguments used to filter AndroidDevice instances.
379
380  Returns:
381    A list of target AndroidDevice instances.
382
383  Raises:
384    Error: No devices are matched.
385  """
386
387  def _get_device_filter(ad):
388    for k, v in kwargs.items():
389      if not hasattr(ad, k):
390        return False
391      elif getattr(ad, k) != v:
392        return False
393    return True
394
395  filtered = filter_devices(ads, _get_device_filter)
396  if not filtered:
397    raise Error(
398        'Could not find a target device that matches condition: %s.' % kwargs
399    )
400  else:
401    return filtered
402
403
404def get_device(ads, **kwargs):
405  """Finds a unique AndroidDevice instance from a list that has specific
406  attributes of certain values.
407
408  Example:
409    get_device(android_devices, label='foo', phone_number='1234567890')
410    get_device(android_devices, model='angler')
411
412  Args:
413    ads: A list of AndroidDevice instances.
414    kwargs: keyword arguments used to filter AndroidDevice instances.
415
416  Returns:
417    The target AndroidDevice instance.
418
419  Raises:
420    Error: None or more than one device is matched.
421  """
422
423  filtered = get_devices(ads, **kwargs)
424  if len(filtered) == 1:
425    return filtered[0]
426  else:
427    serials = [ad.serial for ad in filtered]
428    raise Error('More than one device matched: %s' % serials)
429
430
431def take_bug_reports(ads, test_name=None, begin_time=None, destination=None):
432  """Takes bug reports on a list of android devices.
433
434  If you want to take a bug report, call this function with a list of
435  android_device objects in on_fail. But reports will be taken on all the
436  devices in the list concurrently. Bug report takes a relative long
437  time to take, so use this cautiously.
438
439  Args:
440    ads: A list of AndroidDevice instances.
441    test_name: Name of the test method that triggered this bug report.
442      If None, the default name "bugreport" will be used.
443    begin_time: timestamp taken when the test started, can be either
444      string or int. If None, the current time will be used.
445    destination: string, path to the directory where the bugreport
446      should be saved.
447  """
448  if begin_time is None:
449    begin_time = mobly_logger.get_log_file_timestamp()
450  else:
451    begin_time = mobly_logger.sanitize_filename(str(begin_time))
452
453  def take_br(test_name, begin_time, ad, destination):
454    ad.take_bug_report(
455        test_name=test_name, begin_time=begin_time, destination=destination
456    )
457
458  args = [(test_name, begin_time, ad, destination) for ad in ads]
459  utils.concurrent_exec(take_br, args)
460
461
462class BuildInfoConstants(enum.Enum):
463  """Enums for build info constants used for AndroidDevice build info.
464
465  Attributes:
466    build_info_key: The key used for the build_info dictionary in AndroidDevice.
467    system_prop_key: The key used for getting the build info from system
468      properties.
469  """
470
471  BUILD_ID = 'build_id', 'ro.build.id'
472  BUILD_TYPE = 'build_type', 'ro.build.type'
473  BUILD_FINGERPRINT = 'build_fingerprint', 'ro.build.fingerprint'
474  BUILD_VERSION_CODENAME = 'build_version_codename', 'ro.build.version.codename'
475  BUILD_VERSION_INCREMENTAL = (
476      'build_version_incremental',
477      'ro.build.version.incremental',
478  )
479  BUILD_VERSION_SDK = 'build_version_sdk', 'ro.build.version.sdk'
480  BUILD_PRODUCT = 'build_product', 'ro.build.product'
481  BUILD_CHARACTERISTICS = 'build_characteristics', 'ro.build.characteristics'
482  DEBUGGABLE = 'debuggable', 'ro.debuggable'
483  PRODUCT_NAME = 'product_name', 'ro.product.name'
484  HARDWARE = 'hardware', 'ro.hardware'
485
486  def __init__(self, build_info_key, system_prop_key):
487    self.build_info_key = build_info_key
488    self.system_prop_key = system_prop_key
489
490
491class AndroidDevice:
492  """Class representing an android device.
493
494  Each object of this class represents one Android device in Mobly. This class
495  provides various ways, like adb, fastboot, and Mobly snippets, to control
496  an Android device, whether it's a real device or an emulator instance.
497
498  You can also register your own services to the device's service manager.
499  See the docs of `service_manager` and `base_service` for details.
500
501  Attributes:
502    serial: A string that's the serial number of the Android device.
503    log_path: A string that is the path where all logs collected on this
504      android device should be stored.
505    log: A logger adapted from root logger with an added prefix specific
506      to an AndroidDevice instance. The default prefix is
507      [AndroidDevice|<serial>]. Use self.debug_tag = 'tag' to use a
508      different tag in the prefix.
509    adb_logcat_file_path: A string that's the full path to the adb logcat
510      file collected, if any.
511    adb: An AdbProxy object used for interacting with the device via adb.
512    fastboot: A FastbootProxy object used for interacting with the device
513      via fastboot.
514    services: ServiceManager, the manager of long-running services on the
515      device.
516  """
517
518  def __init__(self, serial=''):
519    self._serial = str(serial)
520    # logging.log_path only exists when this is used in an Mobly test run.
521    _log_path_base = utils.abs_path(getattr(logging, 'log_path', '/tmp/logs'))
522    self._log_path = os.path.join(
523        _log_path_base, 'AndroidDevice%s' % self._normalized_serial
524    )
525    self._debug_tag = self._serial
526    self.log = AndroidDeviceLoggerAdapter(
527        logging.getLogger(), {'tag': self.debug_tag}
528    )
529    self._build_info = None
530    self._is_rebooting = False
531    self.adb = adb.AdbProxy(serial)
532    self.fastboot = fastboot.FastbootProxy(serial)
533    if self.is_rootable:
534      self.root_adb()
535    self.services = service_manager.ServiceManager(self)
536    self.services.register(
537        SERVICE_NAME_LOGCAT, logcat.Logcat, start_service=False
538    )
539    self.services.register(
540        'snippets', snippet_management_service.SnippetManagementService
541    )
542    # Device info cache.
543    self._user_added_device_info = {}
544
545  def __repr__(self):
546    return '<AndroidDevice|%s>' % self.debug_tag
547
548  @property
549  def adb_logcat_file_path(self):
550    if self.services.has_service_by_name(SERVICE_NAME_LOGCAT):
551      return self.services.logcat.adb_logcat_file_path
552
553  @property
554  def _normalized_serial(self):
555    """Normalized serial name for usage in log filename.
556
557    Some Android emulators use ip:port as their serial names, while on
558    Windows `:` is not valid in filename, it should be sanitized first.
559    """
560    if self._serial is None:
561      return None
562    return mobly_logger.sanitize_filename(self._serial)
563
564  @property
565  def device_info(self):
566    """Information to be pulled into controller info.
567
568    The latest serial, model, and build_info are included. Additional info
569    can be added via `add_device_info`.
570    """
571    info = {
572        'serial': self.serial,
573        'model': self.model,
574        'build_info': self.build_info,
575        'user_added_info': self._user_added_device_info,
576    }
577    return info
578
579  def add_device_info(self, name, info):
580    """Add information of the device to be pulled into controller info.
581
582    Adding the same info name the second time will override existing info.
583
584    Args:
585      name: string, name of this info.
586      info: serializable, content of the info.
587    """
588    self._user_added_device_info.update({name: info})
589
590  @property
591  def debug_tag(self):
592    """A string that represents a device object in debug info. Default value
593    is the device serial.
594
595    This will be used as part of the prefix of debugging messages emitted by
596    this device object, like log lines and the message of DeviceError.
597    """
598    return self._debug_tag
599
600  @debug_tag.setter
601  def debug_tag(self, tag):
602    """Setter for the debug tag.
603
604    By default, the tag is the serial of the device, but sometimes it may
605    be more descriptive to use a different tag of the user's choice.
606
607    Changing debug tag changes part of the prefix of debug info emitted by
608    this object, like log lines and the message of DeviceError.
609
610    Example:
611      By default, the device's serial number is used:
612        'INFO [AndroidDevice|abcdefg12345] One pending call ringing.'
613      The tag can be customized with `ad.debug_tag = 'Caller'`:
614        'INFO [AndroidDevice|Caller] One pending call ringing.'
615    """
616    self.log.info('Logging debug tag set to "%s"', tag)
617    self._debug_tag = tag
618    self.log.extra['tag'] = tag
619
620  @property
621  def has_active_service(self):
622    """True if any service is running on the device.
623
624    A service can be a snippet or logcat collection.
625    """
626    return self.services.is_any_alive
627
628  @property
629  def log_path(self):
630    """A string that is the path for all logs collected from this device."""
631    if not os.path.exists(self._log_path):
632      utils.create_dir(self._log_path)
633    return self._log_path
634
635  @log_path.setter
636  def log_path(self, new_path):
637    """Setter for `log_path`, use with caution."""
638    if self.has_active_service:
639      raise DeviceError(
640          self, 'Cannot change `log_path` when there is service running.'
641      )
642    old_path = self._log_path
643    if new_path == old_path:
644      return
645    if os.listdir(new_path):
646      raise DeviceError(
647          self, 'Logs already exist at %s, cannot override.' % new_path
648      )
649    if os.path.exists(old_path):
650      # Remove new path so copytree doesn't complain.
651      shutil.rmtree(new_path, ignore_errors=True)
652      shutil.copytree(old_path, new_path)
653      shutil.rmtree(old_path, ignore_errors=True)
654    self._log_path = new_path
655
656  @property
657  def serial(self):
658    """The serial number used to identify a device.
659
660    This is essentially the value used for adb's `-s` arg, which means it
661    can be a network address or USB bus number.
662    """
663    return self._serial
664
665  def update_serial(self, new_serial):
666    """Updates the serial number of a device.
667
668    The "serial number" used with adb's `-s` arg is not necessarily the
669    actual serial number. For remote devices, it could be a combination of
670    host names and port numbers.
671
672    This is used for when such identifier of remote devices changes during
673    a test. For example, when a remote device reboots, it may come back
674    with a different serial number.
675
676    This is NOT meant for switching the object to represent another device.
677
678    We intentionally did not make it a regular setter of the serial
679    property so people don't accidentally call this without understanding
680    the consequences.
681
682    Args:
683      new_serial: string, the new serial number for the same device.
684
685    Raises:
686      DeviceError: tries to update serial when any service is running.
687    """
688    new_serial = str(new_serial)
689    if self.has_active_service:
690      raise DeviceError(
691          self,
692          'Cannot change device serial number when there is service running.',
693      )
694    if self._debug_tag == self.serial:
695      self._debug_tag = new_serial
696    self._serial = new_serial
697    self.adb.serial = new_serial
698    self.fastboot.serial = new_serial
699
700  @contextlib.contextmanager
701  def handle_reboot(self):
702    """Properly manage the service life cycle when the device needs to
703    temporarily disconnect.
704
705    The device can temporarily lose adb connection due to user-triggered
706    reboot. Use this function to make sure the services
707    started by Mobly are properly stopped and restored afterwards.
708
709    For sample usage, see self.reboot().
710    """
711    live_service_names = self.services.list_live_services()
712    self.services.stop_all()
713    # On rooted devices, system properties may change on reboot, so disable
714    # the `build_info` cache by setting `_is_rebooting` to True and
715    # repopulate it after reboot.
716    # Note, this logic assumes that instance variable assignment in Python
717    # is atomic; otherwise, `threading` data structures would be necessary.
718    # Additionally, nesting calls to `handle_reboot` while changing the
719    # read-only property values during reboot will result in stale values.
720    self._is_rebooting = True
721    try:
722      yield
723    finally:
724      self.wait_for_boot_completion()
725      # On boot completion, invalidate the `build_info` cache since any
726      # value it had from before boot completion is potentially invalid.
727      # If the value gets set after the final invalidation and before
728      # setting`_is_rebooting` to True, then that's okay because the
729      # device has finished rebooting at that point, and values at that
730      # point should be valid.
731      # If the reboot fails for some reason, then `_is_rebooting` is never
732      # set to False, which means the `build_info` cache remains disabled
733      # until the next reboot. This is relatively okay because the
734      # `build_info` cache is only minimizes adb commands.
735      self._build_info = None
736      self._is_rebooting = False
737      if self.is_rootable:
738        self.root_adb()
739    self.services.start_services(live_service_names)
740
741  @contextlib.contextmanager
742  def handle_usb_disconnect(self):
743    """Properly manage the service life cycle when USB is disconnected.
744
745    The device can temporarily lose adb connection due to user-triggered
746    USB disconnection, e.g. the following cases can be handled by this
747    method:
748
749    * Power measurement: Using Monsoon device to measure battery consumption
750      would potentially disconnect USB.
751    * Unplug USB so device loses connection.
752    * ADB connection over WiFi and WiFi got disconnected.
753    * Any other type of USB disconnection, as long as snippet session can
754      be kept alive while USB disconnected (reboot caused USB
755      disconnection is not one of these cases because snippet session
756      cannot survive reboot.
757      Use handle_reboot() instead).
758
759    Use this function to make sure the services started by Mobly are
760    properly reconnected afterwards.
761
762    Just like the usage of self.handle_reboot(), this method does not
763    automatically detect if the disconnection is because of a reboot or USB
764    disconnect. Users of this function should make sure the right handle_*
765    function is used to handle the correct type of disconnection.
766
767    This method also reconnects snippet event client. Therefore, the
768    callback objects created (by calling Async RPC methods) before
769    disconnection would still be valid and can be used to retrieve RPC
770    execution result after device got reconnected.
771
772    Example Usage:
773
774    .. code-block:: python
775
776      with ad.handle_usb_disconnect():
777        try:
778          # User action that triggers USB disconnect, could throw
779          # exceptions.
780          do_something()
781        finally:
782          # User action that triggers USB reconnect
783          action_that_reconnects_usb()
784          # Make sure device is reconnected before returning from this
785          # context
786          ad.adb.wait_for_device(timeout=SOME_TIMEOUT)
787    """
788    live_service_names = self.services.list_live_services()
789    self.services.pause_all()
790    try:
791      yield
792    finally:
793      self.services.resume_services(live_service_names)
794
795  @property
796  def build_info(self):
797    """Gets the build info of this Android device, including build id and type.
798
799    This is not available if the device is in bootloader mode.
800
801    Returns:
802      A dict with the build info of this Android device, or None if the
803      device is in bootloader mode.
804    """
805    if self.is_bootloader:
806      self.log.error('Device is in fastboot mode, could not get build info.')
807      return
808    if self._build_info is None or self._is_rebooting:
809      info = {}
810      build_info = self.adb.getprops(CACHED_SYSTEM_PROPS)
811      for build_info_constant in BuildInfoConstants:
812        info[build_info_constant.build_info_key] = build_info.get(
813            build_info_constant.system_prop_key, ''
814        )
815      self._build_info = info
816      return info
817    return self._build_info
818
819  @property
820  def is_bootloader(self):
821    """True if the device is in bootloader mode."""
822    return self.serial in list_fastboot_devices()
823
824  @property
825  def is_adb_root(self):
826    """True if adb is running as root for this device."""
827    try:
828      return '0' == self.adb.shell('id -u').decode('utf-8').strip()
829    except adb.AdbError:
830      # Wait a bit and retry to work around adb flakiness for this cmd.
831      time.sleep(0.2)
832      return '0' == self.adb.shell('id -u').decode('utf-8').strip()
833
834  @property
835  def is_rootable(self):
836    return self.is_adb_detectable() and self.build_info['debuggable'] == '1'
837
838  @functools.cached_property
839  def model(self):
840    """The Android code name for the device."""
841    # If device is in bootloader mode, get mode name from fastboot.
842    if self.is_bootloader:
843      out = self.fastboot.getvar('product').strip()
844      # 'out' is never empty because of the 'total time' message fastboot
845      # writes to stderr.
846      lines = out.decode('utf-8').split('\n', 1)
847      if lines:
848        tokens = lines[0].split(' ')
849        if len(tokens) > 1:
850          return tokens[1].lower()
851      return None
852    model = self.build_info['build_product'].lower()
853    if model == 'sprout':
854      return model
855    return self.build_info['product_name'].lower()
856
857  @property
858  def is_emulator(self):
859    """Whether this device is probably an emulator.
860
861    Returns:
862      True if this is probably an emulator.
863    """
864    if EMULATOR_SERIAL_REGEX.match(self.serial):
865      # If the device's serial follows 'emulator-dddd', then it's almost
866      # certainly an emulator.
867      return True
868    elif self.build_info['build_characteristics'] == 'emulator':
869      # If the device says that it's an emulator, then it's probably an
870      # emulator although some real devices apparently report themselves
871      # as emulators in addition to other things, so only return True on
872      # an exact match.
873      return True
874    elif self.build_info['hardware'] in ['ranchu', 'goldfish', 'cutf_cvm']:
875      # Ranchu and Goldfish are the hardware properties that the AOSP
876      # emulators report, so if the device says it's an AOSP emulator, it
877      # probably is one. Cuttlefish emulators report 'cutf_cvm` as the
878      # hardware property.
879      return True
880    else:
881      return False
882
883  def load_config(self, config):
884    """Add attributes to the AndroidDevice object based on config.
885
886    Args:
887      config: A dictionary representing the configs.
888
889    Raises:
890      Error: The config is trying to overwrite an existing attribute.
891    """
892    for k, v in config.items():
893      if hasattr(self, k) and k not in _ANDROID_DEVICE_SETTABLE_PROPS:
894        raise DeviceError(
895            self,
896            'Attribute %s already exists with value %s, cannot set again.'
897            % (k, getattr(self, k)),
898        )
899      setattr(self, k, v)
900      self.add_device_info(k, v)
901
902  def root_adb(self):
903    """Change adb to root mode for this device if allowed.
904
905    If executed on a production build, adb will not be switched to root
906    mode per security restrictions.
907    """
908    self.adb.root()
909    # `root` causes the device to temporarily disappear from adb.
910    # So we need to wait for the device to come back before proceeding.
911    self.adb.wait_for_device(timeout=DEFAULT_TIMEOUT_BOOT_COMPLETION_SECOND)
912
913  def load_snippet(self, name, package, config=None):
914    """Starts the snippet apk with the given package name and connects.
915
916    Examples:
917
918    .. code-block:: python
919
920      ad.load_snippet(
921          name='maps', package='com.google.maps.snippets')
922      ad.maps.activateZoom('3')
923
924    Args:
925      name: string, the attribute name to which to attach the snippet
926        client. E.g. `name='maps'` attaches the snippet client to
927        `ad.maps`.
928      package: string, the package name of the snippet apk to connect to.
929      config: snippet_client_v2.Config, the configuration object for
930        controlling the snippet behaviors. See the docstring of the `Config`
931        class for supported configurations.
932
933    Raises:
934      SnippetError: Illegal load operations are attempted.
935    """
936    # Should not load snippet with an existing attribute.
937    if hasattr(self, name):
938      raise SnippetError(
939          self,
940          'Attribute "%s" already exists, please use a different name.' % name,
941      )
942    self.services.snippets.add_snippet_client(name, package, config=config)
943
944  def unload_snippet(self, name):
945    """Stops a snippet apk.
946
947    Args:
948      name: The attribute name the snippet server is attached with.
949
950    Raises:
951      SnippetError: The given snippet name is not registered.
952    """
953    self.services.snippets.remove_snippet_client(name)
954
955  def generate_filename(
956      self, file_type, time_identifier=None, extension_name=None
957  ):
958    """Generates a name for an output file related to this device.
959
960    The name follows the pattern:
961
962      {file type},{debug_tag},{serial},{model},{time identifier}.{ext}
963
964    "debug_tag" is only added if it's different from the serial. "ext" is
965    added if specified by user.
966
967    Args:
968      file_type: string, type of this file, like "logcat" etc.
969      time_identifier: string or RuntimeTestInfo. If a `RuntimeTestInfo`
970        is passed in, the `signature` of the test case will be used. If
971        a string is passed in, the string itself will be used.
972        Otherwise the current timestamp will be used.
973      extension_name: string, the extension name of the file.
974
975    Returns:
976      String, the filename generated.
977    """
978    time_str = time_identifier
979    if time_identifier is None:
980      time_str = mobly_logger.get_log_file_timestamp()
981    elif isinstance(time_identifier, runtime_test_info.RuntimeTestInfo):
982      time_str = time_identifier.signature
983    filename_tokens = [file_type]
984    if self.debug_tag != self.serial:
985      filename_tokens.append(self.debug_tag)
986    filename_tokens.extend([self.serial, self.model, time_str])
987    filename_str = ','.join(filename_tokens)
988    if extension_name is not None:
989      filename_str = '%s.%s' % (filename_str, extension_name)
990    filename_str = mobly_logger.sanitize_filename(filename_str)
991    self.log.debug('Generated filename: %s', filename_str)
992    return filename_str
993
994  def take_bug_report(
995      self, test_name=None, begin_time=None, timeout=300, destination=None
996  ):
997    """Takes a bug report on the device and stores it in a file.
998
999    Args:
1000      test_name: Name of the test method that triggered this bug report.
1001      begin_time: Timestamp of when the test started. If not set, then
1002        this will default to the current time.
1003      timeout: float, the number of seconds to wait for bugreport to
1004        complete, default is 5min.
1005      destination: string, path to the directory where the bugreport
1006        should be saved.
1007
1008    Returns:
1009      A string that is the absolute path to the bug report on the host.
1010    """
1011    prefix = DEFAULT_BUG_REPORT_NAME
1012    if test_name:
1013      prefix = '%s,%s' % (DEFAULT_BUG_REPORT_NAME, test_name)
1014    if begin_time is None:
1015      begin_time = mobly_logger.get_log_file_timestamp()
1016
1017    new_br = True
1018    try:
1019      stdout = self.adb.shell('bugreportz -v').decode('utf-8')
1020      # This check is necessary for builds before N, where adb shell's ret
1021      # code and stderr are not propagated properly.
1022      if 'not found' in stdout:
1023        new_br = False
1024    except adb.AdbError:
1025      new_br = False
1026
1027    if destination is None:
1028      destination = os.path.join(self.log_path, 'BugReports')
1029    br_path = utils.abs_path(destination)
1030    utils.create_dir(br_path)
1031    filename = self.generate_filename(prefix, str(begin_time), 'txt')
1032    if new_br:
1033      filename = filename.replace('.txt', '.zip')
1034    full_out_path = os.path.join(br_path, filename)
1035    # in case device restarted, wait for adb interface to return
1036    self.wait_for_boot_completion()
1037    self.log.debug('Start taking bugreport.')
1038    if new_br:
1039      out = self.adb.shell('bugreportz', timeout=timeout).decode('utf-8')
1040      if not out.startswith('OK'):
1041        raise DeviceError(self, 'Failed to take bugreport: %s' % out)
1042      br_out_path = out.split(':')[1].strip()
1043      self.adb.pull([br_out_path, full_out_path])
1044      self.adb.shell(['rm', br_out_path])
1045    else:
1046      # shell=True as this command redirects the stdout to a local file
1047      # using shell redirection.
1048      self.adb.bugreport(' > "%s"' % full_out_path, shell=True, timeout=timeout)
1049    self.log.debug('Bugreport taken at %s.', full_out_path)
1050    return full_out_path
1051
1052  def take_screenshot(self, destination, prefix='screenshot'):
1053    """Takes a screenshot of the device.
1054
1055    Args:
1056      destination: string, full path to the directory to save in.
1057      prefix: string, prefix file name of the screenshot.
1058
1059    Returns:
1060      string, full path to the screenshot file on the host.
1061    """
1062    filename = self.generate_filename(prefix, extension_name='png')
1063    device_path = os.path.join('/storage/emulated/0/', filename)
1064    self.adb.shell(
1065        ['screencap', '-p', device_path], timeout=TAKE_SCREENSHOT_TIMEOUT_SECOND
1066    )
1067    utils.create_dir(destination)
1068    self.adb.pull([device_path, destination])
1069    pic_path = os.path.join(destination, filename)
1070    self.log.debug('Screenshot taken, saved on the host: %s', pic_path)
1071    self.adb.shell(['rm', device_path])
1072    return pic_path
1073
1074  def run_iperf_client(self, server_host, extra_args=''):
1075    """Start iperf client on the device.
1076
1077    Return status as true if iperf client start successfully.
1078    And data flow information as results.
1079
1080    Args:
1081      server_host: Address of the iperf server.
1082      extra_args: A string representing extra arguments for iperf client,
1083        e.g. '-i 1 -t 30'.
1084
1085    Returns:
1086      status: true if iperf client start successfully.
1087      results: results have data flow information
1088    """
1089    out = self.adb.shell('iperf3 -c %s %s' % (server_host, extra_args))
1090    clean_out = str(out, 'utf-8').strip().split('\n')
1091    if 'error' in clean_out[0].lower():
1092      return False, clean_out
1093    return True, clean_out
1094
1095  def wait_for_boot_completion(
1096      self, timeout=DEFAULT_TIMEOUT_BOOT_COMPLETION_SECOND
1097  ):
1098    """Waits for Android framework to broadcast ACTION_BOOT_COMPLETED.
1099
1100    This function times out after 15 minutes.
1101
1102    Args:
1103      timeout: float, the number of seconds to wait before timing out.
1104        If not specified, no timeout takes effect.
1105    """
1106    deadline = time.perf_counter() + timeout
1107
1108    self.adb.wait_for_device(timeout=timeout)
1109    while time.perf_counter() < deadline:
1110      try:
1111        if self.is_boot_completed():
1112          return
1113      except (adb.AdbError, adb.AdbTimeoutError):
1114        # adb shell calls may fail during certain period of booting
1115        # process, which is normal. Ignoring these errors.
1116        pass
1117      time.sleep(5)
1118    raise DeviceError(self, 'Booting process timed out')
1119
1120  def is_boot_completed(self):
1121    """Checks if device boot is completed by verifying system property."""
1122    completed = self.adb.getprop('sys.boot_completed')
1123    if completed == '1':
1124      self.log.debug('Device boot completed.')
1125      return True
1126    return False
1127
1128  def is_adb_detectable(self):
1129    """Checks if USB is on and device is ready by verifying adb devices."""
1130    serials = list_adb_devices()
1131    if self.serial in serials:
1132      self.log.debug('Is now adb detectable.')
1133      return True
1134    return False
1135
1136  def reboot(self):
1137    """Reboots the device.
1138
1139    Generally one should use this method to reboot the device instead of
1140    directly calling `adb.reboot`. Because this method gracefully handles
1141    the teardown and restoration of running services.
1142
1143    This method is blocking and only returns when the reboot has completed
1144    and the services restored.
1145
1146    Raises:
1147      Error: Waiting for completion timed out.
1148    """
1149    if self.is_bootloader:
1150      self.fastboot.reboot()
1151      return
1152    with self.handle_reboot():
1153      self.adb.reboot()
1154
1155  def __getattr__(self, name):
1156    """Tries to return a snippet client registered with `name`.
1157
1158    This is for backward compatibility of direct accessing snippet clients.
1159    """
1160    client = self.services.snippets.get_snippet_client(name)
1161    if client:
1162      return client
1163    return self.__getattribute__(name)
1164
1165
1166# Properties in AndroidDevice that have setters.
1167# This line has to live below the AndroidDevice code.
1168_ANDROID_DEVICE_SETTABLE_PROPS = utils.get_settable_properties(AndroidDevice)
1169
1170
1171class AndroidDeviceLoggerAdapter(logging.LoggerAdapter):
1172  """A wrapper class that adds a prefix to each log line.
1173
1174  Usage:
1175
1176  .. code-block:: python
1177
1178    my_log = AndroidDeviceLoggerAdapter(logging.getLogger(), {
1179      'tag': <custom tag>
1180    })
1181
1182  Then each log line added by my_log will have a prefix
1183  '[AndroidDevice|<tag>]'
1184  """
1185
1186  def process(self, msg, kwargs):
1187    msg = _DEBUG_PREFIX_TEMPLATE % (self.extra['tag'], msg)
1188    return (msg, kwargs)
1189