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