1#!/usr/bin/env python3.4 2# 3# Copyright 2016 - The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17from builtins import str 18from builtins import open 19 20import os 21import time 22import traceback 23 24from acts import logger as acts_logger 25from acts import signals 26from acts import utils 27from acts.controllers import adb 28from acts.controllers import android 29from acts.controllers import event_dispatcher 30from acts.controllers import fastboot 31 32ACTS_CONTROLLER_CONFIG_NAME = "AndroidDevice" 33ACTS_CONTROLLER_REFERENCE_NAME = "android_devices" 34 35ANDROID_DEVICE_PICK_ALL_TOKEN = "*" 36# Key name for adb logcat extra params in config file. 37ANDROID_DEVICE_ADB_LOGCAT_PARAM_KEY = "adb_logcat_param" 38ANDROID_DEVICE_EMPTY_CONFIG_MSG = "Configuration is empty, abort!" 39ANDROID_DEVICE_NOT_LIST_CONFIG_MSG = "Configuration should be a list, abort!" 40 41class AndroidDeviceError(signals.ControllerError): 42 pass 43 44class DoesNotExistError(AndroidDeviceError): 45 """Raised when something that does not exist is referenced. 46 """ 47 48def create(configs, logger): 49 if not configs: 50 raise AndroidDeviceError(ANDROID_DEVICE_EMPTY_CONFIG_MSG) 51 elif configs == ANDROID_DEVICE_PICK_ALL_TOKEN: 52 ads = get_all_instances(logger=logger) 53 elif not isinstance(configs, list): 54 raise AndroidDeviceError(ANDROID_DEVICE_NOT_LIST_CONFIG_MSG) 55 elif isinstance(configs[0], str): 56 # Configs is a list of serials. 57 ads = get_instances(configs, logger) 58 else: 59 # Configs is a list of dicts. 60 ads = get_instances_with_configs(configs, logger) 61 connected_ads = list_adb_devices() 62 for ad in ads: 63 if ad.serial not in connected_ads: 64 raise DoesNotExistError(("Android device %s is specified in config" 65 " but is not attached.") % ad.serial) 66 ad.start_adb_logcat() 67 try: 68 ad.get_droid() 69 ad.ed.start() 70 except: 71 # This exception is logged here to help with debugging under py2, 72 # because "exception raised while processing another exception" is 73 # only printed under py3. 74 msg = "Failed to start sl4a on %s" % ad.serial 75 logger.exception(msg) 76 raise AndroidDeviceError(msg) 77 return ads 78 79def destroy(ads): 80 for ad in ads: 81 try: 82 ad.terminate_all_sessions() 83 except: 84 pass 85 if ad.adb_logcat_process: 86 ad.stop_adb_logcat() 87 88def _parse_device_list(device_list_str, key): 89 """Parses a byte string representing a list of devices. The string is 90 generated by calling either adb or fastboot. 91 92 Args: 93 device_list_str: Output of adb or fastboot. 94 key: The token that signifies a device in device_list_str. 95 96 Returns: 97 A list of android device serial numbers. 98 """ 99 clean_lines = str(device_list_str, 'utf-8').strip().split('\n') 100 results = [] 101 for line in clean_lines: 102 tokens = line.strip().split('\t') 103 if len(tokens) == 2 and tokens[1] == key: 104 results.append(tokens[0]) 105 return results 106 107def list_adb_devices(): 108 """List all android devices connected to the computer that are detected by 109 adb. 110 111 Returns: 112 A list of android device serials. Empty if there's none. 113 """ 114 out = adb.AdbProxy().devices() 115 return _parse_device_list(out, "device") 116 117def list_fastboot_devices(): 118 """List all android devices connected to the computer that are in in 119 fastboot mode. These are detected by fastboot. 120 121 Returns: 122 A list of android device serials. Empty if there's none. 123 """ 124 out = fastboot.FastbootProxy().devices() 125 return _parse_device_list(out, "fastboot") 126 127def get_instances(serials, logger=None): 128 """Create AndroidDevice instances from a list of serials. 129 130 Args: 131 serials: A list of android device serials. 132 logger: A logger to be passed to each instance. 133 134 Returns: 135 A list of AndroidDevice objects. 136 """ 137 results = [] 138 for s in serials: 139 results.append(AndroidDevice(s, logger=logger)) 140 return results 141 142def get_instances_with_configs(configs, logger=None): 143 """Create AndroidDevice instances from a list of json configs. 144 145 Each config should have the required key-value pair "serial". 146 147 Args: 148 configs: A list of dicts each representing the configuration of one 149 android device. 150 logger: A logger to be passed to each instance. 151 152 Returns: 153 A list of AndroidDevice objects. 154 """ 155 results = [] 156 for c in configs: 157 try: 158 serial = c.pop("serial") 159 except KeyError: 160 raise AndroidDeviceError(('Required value "serial" is missing in ' 161 'AndroidDevice config %s.') % c) 162 ad = AndroidDevice(serial, logger=logger) 163 ad.load_config(c) 164 results.append(ad) 165 return results 166 167def get_all_instances(include_fastboot=False, logger=None): 168 """Create AndroidDevice instances for all attached android devices. 169 170 Args: 171 include_fastboot: Whether to include devices in bootloader mode or not. 172 logger: A logger to be passed to each instance. 173 174 Returns: 175 A list of AndroidDevice objects each representing an android device 176 attached to the computer. 177 """ 178 if include_fastboot: 179 serial_list = list_adb_devices() + list_fastboot_devices() 180 return get_instances(serial_list, logger=logger) 181 return get_instances(list_adb_devices(), logger=logger) 182 183def filter_devices(ads, func): 184 """Finds the AndroidDevice instances from a list that match certain 185 conditions. 186 187 Args: 188 ads: A list of AndroidDevice instances. 189 func: A function that takes an AndroidDevice object and returns True 190 if the device satisfies the filter condition. 191 192 Returns: 193 A list of AndroidDevice instances that satisfy the filter condition. 194 """ 195 results = [] 196 for ad in ads: 197 if func(ad): 198 results.append(ad) 199 return results 200 201def get_device(ads, **kwargs): 202 """Finds a unique AndroidDevice instance from a list that has specific 203 attributes of certain values. 204 205 Example: 206 get_device(android_devices, label="foo", phone_number="1234567890") 207 get_device(android_devices, model="angler") 208 209 Args: 210 ads: A list of AndroidDevice instances. 211 kwargs: keyword arguments used to filter AndroidDevice instances. 212 213 Returns: 214 The target AndroidDevice instance. 215 216 Raises: 217 AndroidDeviceError is raised if none or more than one device is 218 matched. 219 """ 220 def _get_device_filter(ad): 221 for k, v in kwargs.items(): 222 if not hasattr(ad, k): 223 return False 224 elif getattr(ad, k) != v: 225 return False 226 return True 227 filtered = filter_devices(ads, _get_device_filter) 228 if not filtered: 229 raise AndroidDeviceError(("Could not find a target device that matches" 230 " condition: %s.") % kwargs) 231 elif len(filtered) == 1: 232 return filtered[0] 233 else: 234 serials = [ad.serial for ad in filtered] 235 raise AndroidDeviceError("More than one device matched: %s" % serials) 236 237def take_bug_reports(ads, test_name, begin_time): 238 """Takes bug reports on a list of android devices. 239 240 If you want to take a bug report, call this function with a list of 241 android_device objects in on_fail. But reports will be taken on all the 242 devices in the list concurrently. Bug report takes a relative long 243 time to take, so use this cautiously. 244 245 Args: 246 ads: A list of AndroidDevice instances. 247 test_name: Name of the test case that triggered this bug report. 248 begin_time: Logline format timestamp taken when the test started. 249 """ 250 begin_time = acts_logger.normalize_log_line_timestamp(begin_time) 251 def take_br(test_name, begin_time, ad): 252 ad.take_bug_report(test_name, begin_time) 253 args = [(test_name, begin_time, ad) for ad in ads] 254 utils.concurrent_exec(take_br, args) 255 256class AndroidDevice: 257 """Class representing an android device. 258 259 Each object of this class represents one Android device in ACTS, including 260 handles to adb, fastboot, and sl4a clients. In addition to direct adb 261 commands, this object also uses adb port forwarding to talk to the Android 262 device. 263 264 Attributes: 265 serial: A string that's the serial number of the Androi device. 266 h_port: An integer that's the port number for adb port forwarding used 267 on the computer the Android device is connected 268 d_port: An integer that's the port number used on the Android device 269 for adb port forwarding. 270 log: A LoggerProxy object used for the class's internal logging. 271 log_path: A string that is the path where all logs collected on this 272 android device should be stored. 273 adb_logcat_process: A process that collects the adb logcat. 274 adb_logcat_file_path: A string that's the full path to the adb logcat 275 file collected, if any. 276 adb: An AdbProxy object used for interacting with the device via adb. 277 fastboot: A FastbootProxy object used for interacting with the device 278 via fastboot. 279 """ 280 281 def __init__(self, serial="", host_port=None, device_port=8080, 282 logger=None): 283 self.serial = serial 284 self.h_port = host_port 285 self.d_port = device_port 286 self.log = acts_logger.LoggerProxy(logger) 287 lp = self.log.log_path 288 self.log_path = os.path.join(lp, "AndroidDevice%s" % serial) 289 self._droid_sessions = {} 290 self._event_dispatchers = {} 291 self.adb_logcat_process = None 292 self.adb_logcat_file_path = None 293 self.adb = adb.AdbProxy(serial) 294 self.fastboot = fastboot.FastbootProxy(serial) 295 if not self.is_bootloader: 296 self.root_adb() 297 298 def __del__(self): 299 if self.h_port: 300 self.adb.forward("--remove tcp:%d" % self.h_port) 301 if self.adb_logcat_process: 302 self.stop_adb_logcat() 303 304 @property 305 def is_bootloader(self): 306 """True if the device is in bootloader mode. 307 """ 308 return self.serial in list_fastboot_devices() 309 310 @property 311 def is_adb_root(self): 312 """True if adb is running as root for this device. 313 """ 314 return "root" in self.adb.shell("id -u").decode("utf-8") 315 316 @property 317 def model(self): 318 """The Android code name for the device. 319 """ 320 # If device is in bootloader mode, get mode name from fastboot. 321 if self.is_bootloader: 322 out = self.fastboot.getvar("product").strip() 323 # "out" is never empty because of the "total time" message fastboot 324 # writes to stderr. 325 lines = out.decode("utf-8").split('\n', 1) 326 if lines: 327 tokens = lines[0].split(' ') 328 if len(tokens) > 1: 329 return tokens[1].lower() 330 return None 331 out = self.adb.shell('getprop | grep ro.build.product') 332 model = out.decode("utf-8").strip().split('[')[-1][:-1].lower() 333 if model == "sprout": 334 return model 335 else: 336 out = self.adb.shell('getprop | grep ro.product.name') 337 model = out.decode("utf-8").strip().split('[')[-1][:-1].lower() 338 return model 339 340 @property 341 def droid(self): 342 """The first sl4a session initiated on this device. None if there isn't 343 one. 344 """ 345 try: 346 session_id = sorted(self._droid_sessions)[0] 347 return self._droid_sessions[session_id][0] 348 except IndexError: 349 return None 350 351 @property 352 def ed(self): 353 """The first event_dispatcher instance created on this device. None if 354 there isn't one. 355 """ 356 try: 357 session_id = sorted(self._event_dispatchers)[0] 358 return self._event_dispatchers[session_id] 359 except IndexError: 360 return None 361 362 @property 363 def droids(self): 364 """A list of the active sl4a sessions on this device. 365 366 If multiple connections exist for the same session, only one connection 367 is listed. 368 """ 369 keys = sorted(self._droid_sessions) 370 results = [] 371 for k in keys: 372 results.append(self._droid_sessions[k][0]) 373 return results 374 375 @property 376 def eds(self): 377 """A list of the event_dispatcher objects on this device. 378 379 The indexing of the list matches that of the droids property. 380 """ 381 keys = sorted(self._event_dispatchers) 382 results = [] 383 for k in keys: 384 results.append(self._event_dispatchers[k]) 385 return results 386 387 @property 388 def is_adb_logcat_on(self): 389 """Whether there is an ongoing adb logcat collection. 390 """ 391 if self.adb_logcat_process: 392 return True 393 return False 394 395 def load_config(self, config): 396 """Add attributes to the AndroidDevice object based on json config. 397 398 Args: 399 config: A dictionary representing the configs. 400 401 Raises: 402 AndroidDeviceError is raised if the config is trying to overwrite 403 an existing attribute. 404 """ 405 for k, v in config.items(): 406 if hasattr(self, k): 407 raise AndroidDeviceError(("Attempting to set existing " 408 "attribute %s on %s") % (k, self.serial)) 409 setattr(self, k, v) 410 411 def root_adb(self): 412 """Change adb to root mode for this device. 413 """ 414 if not self.is_adb_root: 415 self.adb.root() 416 self.adb.wait_for_device() 417 self.adb.remount() 418 self.adb.wait_for_device() 419 420 def get_droid(self, handle_event=True): 421 """Create an sl4a connection to the device. 422 423 Return the connection handler 'droid'. By default, another connection 424 on the same session is made for EventDispatcher, and the dispatcher is 425 returned to the caller as well. 426 If sl4a server is not started on the device, try to start it. 427 428 Args: 429 handle_event: True if this droid session will need to handle 430 events. 431 432 Returns: 433 droid: Android object used to communicate with sl4a on the android 434 device. 435 ed: An optional EventDispatcher to organize events for this droid. 436 437 Examples: 438 Don't need event handling: 439 >>> ad = AndroidDevice() 440 >>> droid = ad.get_droid(False) 441 442 Need event handling: 443 >>> ad = AndroidDevice() 444 >>> droid, ed = ad.get_droid() 445 """ 446 if not self.h_port or not adb.is_port_available(self.h_port): 447 self.h_port = adb.get_available_host_port() 448 self.adb.tcp_forward(self.h_port, self.d_port) 449 try: 450 droid = self.start_new_session() 451 except: 452 self.adb.start_sl4a() 453 droid = self.start_new_session() 454 if handle_event: 455 ed = self.get_dispatcher(droid) 456 return droid, ed 457 return droid 458 459 def get_dispatcher(self, droid): 460 """Return an EventDispatcher for an sl4a session 461 462 Args: 463 droid: Session to create EventDispatcher for. 464 465 Returns: 466 ed: An EventDispatcher for specified session. 467 """ 468 ed_key = self.serial + str(droid.uid) 469 if ed_key in self._event_dispatchers: 470 if self._event_dispatchers[ed_key] is None: 471 raise AndroidDeviceError("EventDispatcher Key Empty") 472 self.log.debug("Returning existing key %s for event dispatcher!", 473 ed_key) 474 return self._event_dispatchers[ed_key] 475 event_droid = self.add_new_connection_to_session(droid.uid) 476 ed = event_dispatcher.EventDispatcher(event_droid) 477 self._event_dispatchers[ed_key] = ed 478 return ed 479 480 def _is_timestamp_in_range(self, target, begin_time, end_time): 481 low = acts_logger.logline_timestamp_comparator(begin_time, target) <= 0 482 high = acts_logger.logline_timestamp_comparator(end_time, target) >= 0 483 return low and high 484 485 def cat_adb_log(self, tag, begin_time): 486 """Takes an excerpt of the adb logcat log from a certain time point to 487 current time. 488 489 Args: 490 tag: An identifier of the time period, usualy the name of a test. 491 begin_time: Logline format timestamp of the beginning of the time 492 period. 493 """ 494 if not self.adb_logcat_file_path: 495 raise AndroidDeviceError(("Attempting to cat adb log when none has" 496 " been collected on Android device %s." 497 ) % self.serial) 498 end_time = acts_logger.get_log_line_timestamp() 499 self.log.debug("Extracting adb log from logcat.") 500 adb_excerpt_path = os.path.join(self.log_path, "AdbLogExcerpts") 501 utils.create_dir(adb_excerpt_path) 502 f_name = os.path.basename(self.adb_logcat_file_path) 503 out_name = f_name.replace("adblog,", "").replace(".txt", "") 504 out_name = ",{},{}.txt".format(begin_time, out_name) 505 tag_len = utils.MAX_FILENAME_LEN - len(out_name) 506 tag = tag[:tag_len] 507 out_name = tag + out_name 508 full_adblog_path = os.path.join(adb_excerpt_path, out_name) 509 with open(full_adblog_path, 'w', encoding='utf-8') as out: 510 in_file = self.adb_logcat_file_path 511 with open(in_file, 'r', encoding='utf-8', errors='replace') as f: 512 in_range = False 513 while True: 514 line = None 515 try: 516 line = f.readline() 517 if not line: 518 break 519 except: 520 continue 521 line_time = line[:acts_logger.log_line_timestamp_len] 522 if not acts_logger.is_valid_logline_timestamp(line_time): 523 continue 524 if self._is_timestamp_in_range(line_time, begin_time, 525 end_time): 526 in_range = True 527 if not line.endswith('\n'): 528 line += '\n' 529 out.write(line) 530 else: 531 if in_range: 532 break 533 534 def start_adb_logcat(self): 535 """Starts a standing adb logcat collection in separate subprocesses and 536 save the logcat in a file. 537 """ 538 if self.is_adb_logcat_on: 539 raise AndroidDeviceError(("Android device {} already has an adb " 540 "logcat thread going on. Cannot start " 541 "another one.").format(self.serial)) 542 # Disable adb log spam filter. 543 self.adb.shell("logpersist.start") 544 f_name = "adblog,{},{}.txt".format(self.model, self.serial) 545 utils.create_dir(self.log_path) 546 logcat_file_path = os.path.join(self.log_path, f_name) 547 try: 548 extra_params = self.adb_logcat_param 549 except AttributeError: 550 extra_params = "" 551 cmd = "adb -s {} logcat -v threadtime {} >> {}".format( 552 self.serial, extra_params, logcat_file_path) 553 self.adb_logcat_process = utils.start_standing_subprocess(cmd) 554 self.adb_logcat_file_path = logcat_file_path 555 556 def stop_adb_logcat(self): 557 """Stops the adb logcat collection subprocess. 558 """ 559 if not self.is_adb_logcat_on: 560 raise AndroidDeviceError(("Android device {} does not have an " 561 "ongoing adb logcat collection." 562 ).format(self.serial)) 563 utils.stop_standing_subprocess(self.adb_logcat_process) 564 self.adb_logcat_process = None 565 566 def take_bug_report(self, test_name, begin_time): 567 """Takes a bug report on the device and stores it in a file. 568 569 Args: 570 test_name: Name of the test case that triggered this bug report. 571 begin_time: Logline format timestamp taken when the test started. 572 """ 573 br_path = os.path.join(self.log_path, "BugReports") 574 utils.create_dir(br_path) 575 base_name = ",{},{}.txt".format(begin_time, self.serial) 576 test_name_len = utils.MAX_FILENAME_LEN - len(base_name) 577 out_name = test_name[:test_name_len] + base_name 578 full_out_path = os.path.join(br_path, out_name.replace(' ', '\ ')) 579 self.log.info("Taking bugreport for %s on %s", test_name, self.serial) 580 self.adb.bugreport(" > {}".format(full_out_path)) 581 self.log.info("Bugreport for %s taken at %s", test_name, full_out_path) 582 583 def start_new_session(self): 584 """Start a new session in sl4a. 585 586 Also caches the droid in a dict with its uid being the key. 587 588 Returns: 589 An Android object used to communicate with sl4a on the android 590 device. 591 592 Raises: 593 SL4AException: Something is wrong with sl4a and it returned an 594 existing uid to a new session. 595 """ 596 droid = android.Android(port=self.h_port) 597 if droid.uid in self._droid_sessions: 598 raise android.SL4AException(("SL4A returned an existing uid for a " 599 "new session. Abort.")) 600 self._droid_sessions[droid.uid] = [droid] 601 return droid 602 603 def add_new_connection_to_session(self, session_id): 604 """Create a new connection to an existing sl4a session. 605 606 Args: 607 session_id: UID of the sl4a session to add connection to. 608 609 Returns: 610 An Android object used to communicate with sl4a on the android 611 device. 612 613 Raises: 614 DoesNotExistError: Raised if the session it's trying to connect to 615 does not exist. 616 """ 617 if session_id not in self._droid_sessions: 618 raise DoesNotExistError("Session %d doesn't exist." % session_id) 619 droid = android.Android(cmd='continue', uid=session_id, 620 port=self.h_port) 621 return droid 622 623 def terminate_session(self, session_id): 624 """Terminate a session in sl4a. 625 626 Send terminate signal to sl4a server; stop dispatcher associated with 627 the session. Clear corresponding droids and dispatchers from cache. 628 629 Args: 630 session_id: UID of the sl4a session to terminate. 631 """ 632 if self._droid_sessions and (session_id in self._droid_sessions): 633 for droid in self._droid_sessions[session_id]: 634 droid.closeSl4aSession() 635 droid.close() 636 del self._droid_sessions[session_id] 637 ed_key = self.serial + str(session_id) 638 if ed_key in self._event_dispatchers: 639 self._event_dispatchers[ed_key].clean_up() 640 del self._event_dispatchers[ed_key] 641 642 def terminate_all_sessions(self): 643 """Terminate all sl4a sessions on the AndroidDevice instance. 644 645 Terminate all sessions and clear caches. 646 """ 647 if self._droid_sessions: 648 session_ids = list(self._droid_sessions.keys()) 649 for session_id in session_ids: 650 try: 651 self.terminate_session(session_id) 652 except: 653 msg = "Failed to terminate session %d." % session_id 654 self.log.exception(msg) 655 self.log.error(traceback.format_exc()) 656 if self.h_port: 657 self.adb.forward("--remove tcp:%d" % self.h_port) 658 self.h_port = None 659 660 def run_iperf_client(self, server_host, extra_args=""): 661 """Start iperf client on the device. 662 663 Return status as true if iperf client start successfully. 664 And data flow information as results. 665 666 Args: 667 server_host: Address of the iperf server. 668 extra_args: A string representing extra arguments for iperf client, 669 e.g. "-i 1 -t 30". 670 671 Returns: 672 status: true if iperf client start successfully. 673 results: results have data flow information 674 """ 675 out = self.adb.shell("iperf3 -c {} {}".format(server_host, extra_args)) 676 clean_out = str(out,'utf-8').strip().split('\n') 677 if "error" in clean_out[0].lower(): 678 return False, clean_out 679 return True, clean_out 680 681 @utils.timeout(15 * 60) 682 def wait_for_boot_completion(self): 683 """Waits for the Android framework to boot back up and ready to launch 684 apps. 685 686 This function times out after 15 minutes. 687 """ 688 self.adb.wait_for_device() 689 while True: 690 try: 691 out = self.adb.shell("getprop sys.boot_completed") 692 completed = out.decode('utf-8').strip() 693 if completed == '1': 694 return 695 except adb.AdbError: 696 # adb shell calls may fail during certain period of booting 697 # process, which is normal. Ignoring these errors. 698 pass 699 time.sleep(5) 700 701 def reboot(self): 702 """Reboots the device. 703 704 Terminate all sl4a sessions, reboot the device, wait for device to 705 complete booting, and restart an sl4a session. 706 707 This is a blocking method. 708 709 This is probably going to print some error messages in console. Only 710 use if there's no other option. 711 712 Example: 713 droid, ed = ad.reboot() 714 715 Returns: 716 An sl4a session with an event_dispatcher. 717 718 Raises: 719 AndroidDeviceError is raised if waiting for completion timed 720 out. 721 """ 722 if self.is_bootloader: 723 self.fastboot.reboot() 724 return 725 has_adb_log = self.is_adb_logcat_on 726 if has_adb_log: 727 self.stop_adb_logcat() 728 self.terminate_all_sessions() 729 self.adb.reboot() 730 self.wait_for_boot_completion() 731 self.root_adb() 732 droid, ed = self.get_droid() 733 ed.start() 734 if has_adb_log: 735 self.start_adb_logcat() 736 return droid, ed 737