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