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