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