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