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 17import base64 18import concurrent.futures 19import datetime 20import json 21import functools 22import os 23import random 24import re 25import signal 26import string 27import subprocess 28import time 29import traceback 30 31# File name length is limited to 255 chars on some OS, so we need to make sure 32# the file names we output fits within the limit. 33MAX_FILENAME_LEN = 255 34 35 36class NexusModelNames: 37 # TODO(angli): This will be fixed later by angli. 38 ONE = 'sprout' 39 N5 = 'hammerhead' 40 N5v2 = 'bullhead' 41 N6 = 'shamu' 42 N6v2 = 'angler' 43 N6v3 = 'marlin' 44 N5v3 = 'sailfish' 45 46class DozeModeStatus: 47 ACTIVE = "ACTIVE" 48 IDLE = "IDLE" 49 50 51ascii_letters_and_digits = string.ascii_letters + string.digits 52valid_filename_chars = "-_." + ascii_letters_and_digits 53 54models = ("sprout", "occam", "hammerhead", "bullhead", "razor", "razorg", 55 "shamu", "angler", "volantis", "volantisg", "mantaray", "fugu", 56 "ryu", "marlin", "sailfish") 57 58manufacture_name_to_model = { 59 "flo": "razor", 60 "flo_lte": "razorg", 61 "flounder": "volantis", 62 "flounder_lte": "volantisg", 63 "dragon": "ryu" 64} 65 66GMT_to_olson = { 67 "GMT-9": "America/Anchorage", 68 "GMT-8": "US/Pacific", 69 "GMT-7": "US/Mountain", 70 "GMT-6": "US/Central", 71 "GMT-5": "US/Eastern", 72 "GMT-4": "America/Barbados", 73 "GMT-3": "America/Buenos_Aires", 74 "GMT-2": "Atlantic/South_Georgia", 75 "GMT-1": "Atlantic/Azores", 76 "GMT+0": "Africa/Casablanca", 77 "GMT+1": "Europe/Amsterdam", 78 "GMT+2": "Europe/Athens", 79 "GMT+3": "Europe/Moscow", 80 "GMT+4": "Asia/Baku", 81 "GMT+5": "Asia/Oral", 82 "GMT+6": "Asia/Almaty", 83 "GMT+7": "Asia/Bangkok", 84 "GMT+8": "Asia/Hong_Kong", 85 "GMT+9": "Asia/Tokyo", 86 "GMT+10": "Pacific/Guam", 87 "GMT+11": "Pacific/Noumea", 88 "GMT+12": "Pacific/Fiji", 89 "GMT+13": "Pacific/Tongatapu", 90 "GMT-11": "Pacific/Midway", 91 "GMT-10": "Pacific/Honolulu" 92} 93 94 95def abs_path(path): 96 """Resolve the '.' and '~' in a path to get the absolute path. 97 98 Args: 99 path: The path to expand. 100 101 Returns: 102 The absolute path of the input path. 103 """ 104 return os.path.abspath(os.path.expanduser(path)) 105 106 107def create_dir(path): 108 """Creates a directory if it does not exist already. 109 110 Args: 111 path: The path of the directory to create. 112 """ 113 full_path = abs_path(path) 114 if not os.path.exists(full_path): 115 os.makedirs(full_path) 116 117 118def get_current_epoch_time(): 119 """Current epoch time in milliseconds. 120 121 Returns: 122 An integer representing the current epoch time in milliseconds. 123 """ 124 return int(round(time.time() * 1000)) 125 126 127def get_current_human_time(): 128 """Returns the current time in human readable format. 129 130 Returns: 131 The current time stamp in Month-Day-Year Hour:Min:Sec format. 132 """ 133 return time.strftime("%m-%d-%Y %H:%M:%S ") 134 135 136def epoch_to_human_time(epoch_time): 137 """Converts an epoch timestamp to human readable time. 138 139 This essentially converts an output of get_current_epoch_time to an output 140 of get_current_human_time 141 142 Args: 143 epoch_time: An integer representing an epoch timestamp in milliseconds. 144 145 Returns: 146 A time string representing the input time. 147 None if input param is invalid. 148 """ 149 if isinstance(epoch_time, int): 150 try: 151 d = datetime.datetime.fromtimestamp(epoch_time / 1000) 152 return d.strftime("%m-%d-%Y %H:%M:%S ") 153 except ValueError: 154 return None 155 156 157def get_timezone_olson_id(): 158 """Return the Olson ID of the local (non-DST) timezone. 159 160 Returns: 161 A string representing one of the Olson IDs of the local (non-DST) 162 timezone. 163 """ 164 tzoffset = int(time.timezone / 3600) 165 gmt = None 166 if tzoffset <= 0: 167 gmt = "GMT+{}".format(-tzoffset) 168 else: 169 gmt = "GMT-{}".format(tzoffset) 170 return GMT_to_olson[gmt] 171 172 173def find_files(paths, file_predicate): 174 """Locate files whose names and extensions match the given predicate in 175 the specified directories. 176 177 Args: 178 paths: A list of directory paths where to find the files. 179 file_predicate: A function that returns True if the file name and 180 extension are desired. 181 182 Returns: 183 A list of files that match the predicate. 184 """ 185 file_list = [] 186 for path in paths: 187 p = abs_path(path) 188 for dirPath, subdirList, fileList in os.walk(p): 189 for fname in fileList: 190 name, ext = os.path.splitext(fname) 191 if file_predicate(name, ext): 192 file_list.append((dirPath, name, ext)) 193 return file_list 194 195 196def load_config(file_full_path): 197 """Loads a JSON config file. 198 199 Returns: 200 A JSON object. 201 """ 202 with open(file_full_path, 'r') as f: 203 conf = json.load(f) 204 return conf 205 206 207def load_file_to_base64_str(f_path): 208 """Loads the content of a file into a base64 string. 209 210 Args: 211 f_path: full path to the file including the file name. 212 213 Returns: 214 A base64 string representing the content of the file in utf-8 encoding. 215 """ 216 path = abs_path(f_path) 217 with open(path, 'rb') as f: 218 f_bytes = f.read() 219 base64_str = base64.b64encode(f_bytes).decode("utf-8") 220 return base64_str 221 222 223def find_field(item_list, cond, comparator, target_field): 224 """Finds the value of a field in a dict object that satisfies certain 225 conditions. 226 227 Args: 228 item_list: A list of dict objects. 229 cond: A param that defines the condition. 230 comparator: A function that checks if an dict satisfies the condition. 231 target_field: Name of the field whose value to be returned if an item 232 satisfies the condition. 233 234 Returns: 235 Target value or None if no item satisfies the condition. 236 """ 237 for item in item_list: 238 if comparator(item, cond) and target_field in item: 239 return item[target_field] 240 return None 241 242 243def rand_ascii_str(length): 244 """Generates a random string of specified length, composed of ascii letters 245 and digits. 246 247 Args: 248 length: The number of characters in the string. 249 250 Returns: 251 The random string generated. 252 """ 253 letters = [random.choice(ascii_letters_and_digits) for i in range(length)] 254 return ''.join(letters) 255 256 257# Thead/Process related functions. 258def concurrent_exec(func, param_list): 259 """Executes a function with different parameters pseudo-concurrently. 260 261 This is basically a map function. Each element (should be an iterable) in 262 the param_list is unpacked and passed into the function. Due to Python's 263 GIL, there's no true concurrency. This is suited for IO-bound tasks. 264 265 Args: 266 func: The function that parforms a task. 267 param_list: A list of iterables, each being a set of params to be 268 passed into the function. 269 270 Returns: 271 A list of return values from each function execution. If an execution 272 caused an exception, the exception object will be the corresponding 273 result. 274 """ 275 with concurrent.futures.ThreadPoolExecutor(max_workers=30) as executor: 276 # Start the load operations and mark each future with its params 277 future_to_params = {executor.submit(func, *p): p for p in param_list} 278 return_vals = [] 279 for future in concurrent.futures.as_completed(future_to_params): 280 params = future_to_params[future] 281 try: 282 return_vals.append(future.result()) 283 except Exception as exc: 284 print("{} generated an exception: {}".format( 285 params, traceback.format_exc())) 286 return_vals.append(exc) 287 return return_vals 288 289 290def exe_cmd(*cmds): 291 """Executes commands in a new shell. 292 293 Args: 294 cmds: A sequence of commands and arguments. 295 296 Returns: 297 The output of the command run. 298 299 Raises: 300 OSError is raised if an error occurred during the command execution. 301 """ 302 cmd = ' '.join(cmds) 303 proc = subprocess.Popen(cmd, 304 stdout=subprocess.PIPE, 305 stderr=subprocess.PIPE, 306 shell=True) 307 (out, err) = proc.communicate() 308 if not err: 309 return out 310 raise OSError(err) 311 312 313def require_sl4a(android_devices): 314 """Makes sure sl4a connection is established on the given AndroidDevice 315 objects. 316 317 Args: 318 android_devices: A list of AndroidDevice objects. 319 320 Raises: 321 AssertionError is raised if any given android device does not have SL4A 322 connection established. 323 """ 324 for ad in android_devices: 325 msg = "SL4A connection not established properly on %s." % ad.serial 326 assert ad.droid, msg 327 328 329def start_standing_subprocess(cmd): 330 """Starts a non-blocking subprocess that is going to continue running after 331 this function returns. 332 333 A subprocess group is actually started by setting sid, so we can kill all 334 the processes spun out from the subprocess when stopping it. This is 335 necessary in case users pass in pipe commands. 336 337 Args: 338 cmd: Command to start the subprocess with. 339 340 Returns: 341 The subprocess that got started. 342 """ 343 p = subprocess.Popen(cmd, 344 stdout=subprocess.PIPE, 345 stderr=subprocess.PIPE, 346 shell=True, 347 preexec_fn=os.setpgrp) 348 return p 349 350 351def stop_standing_subprocess(p, kill_signal=signal.SIGTERM): 352 """Stops a subprocess started by start_standing_subprocess. 353 354 Catches and ignores the PermissionError which only happens on Macs. 355 356 Args: 357 p: Subprocess to terminate. 358 """ 359 try: 360 os.killpg(p.pid, kill_signal) 361 except PermissionError: 362 pass 363 364 365def wait_for_standing_subprocess(p, timeout=None): 366 """Waits for a subprocess started by start_standing_subprocess to finish 367 or times out. 368 369 Propagates the exception raised by the subprocess.wait(.) function. 370 The subprocess.TimeoutExpired exception is raised if the process timed-out 371 rather then terminating. 372 373 If no exception is raised: the subprocess terminated on its own. No need 374 to call stop_standing_subprocess() to kill it. 375 376 If an exception is raised: the subprocess is still alive - it did not 377 terminate. Either call stop_standing_subprocess() to kill it, or call 378 wait_for_standing_subprocess() to keep waiting for it to terminate on its 379 own. 380 381 Args: 382 p: Subprocess to wait for. 383 timeout: An integer number of seconds to wait before timing out. 384 """ 385 p.wait(timeout) 386 387 388def sync_device_time(ad): 389 """Sync the time of an android device with the current system time. 390 391 Both epoch time and the timezone will be synced. 392 393 Args: 394 ad: The android device to sync time on. 395 """ 396 droid = ad.droid 397 droid.setTimeZone(get_timezone_olson_id()) 398 droid.setTime(get_current_epoch_time()) 399 400 401# Timeout decorator block 402class TimeoutError(Exception): 403 """Exception for timeout decorator related errors. 404 """ 405 pass 406 407 408def _timeout_handler(signum, frame): 409 """Handler function used by signal to terminate a timed out function. 410 """ 411 raise TimeoutError() 412 413 414def timeout(sec): 415 """A decorator used to add time out check to a function. 416 417 Args: 418 sec: Number of seconds to wait before the function times out. 419 No timeout if set to 0 420 421 Returns: 422 What the decorated function returns. 423 424 Raises: 425 TimeoutError is raised when time out happens. 426 """ 427 428 def decorator(func): 429 @functools.wraps(func) 430 def wrapper(*args, **kwargs): 431 if sec: 432 signal.signal(signal.SIGALRM, _timeout_handler) 433 signal.alarm(sec) 434 try: 435 return func(*args, **kwargs) 436 except TimeoutError: 437 raise TimeoutError(("Function {} timed out after {} " 438 "seconds.").format(func.__name__, sec)) 439 finally: 440 signal.alarm(0) 441 442 return wrapper 443 444 return decorator 445 446 447def trim_model_name(model): 448 """Trim any prefix and postfix and return the android designation of the 449 model name. 450 451 e.g. "m_shamu" will be trimmed to "shamu". 452 453 Args: 454 model: model name to be trimmed. 455 456 Returns 457 Trimmed model name if one of the known model names is found. 458 None otherwise. 459 """ 460 # Directly look up first. 461 if model in models: 462 return model 463 if model in manufacture_name_to_model: 464 return manufacture_name_to_model[model] 465 # If not found, try trimming off prefix/postfix and look up again. 466 tokens = re.split("_|-", model) 467 for t in tokens: 468 if t in models: 469 return t 470 if t in manufacture_name_to_model: 471 return manufacture_name_to_model[t] 472 return None 473 474 475def force_airplane_mode(ad, new_state, timeout_value=60): 476 """Force the device to set airplane mode on or off by adb shell command. 477 478 Args: 479 ad: android device object. 480 new_state: Turn on airplane mode if True. 481 Turn off airplane mode if False. 482 timeout_value: max wait time for 'adb wait-for-device' 483 484 Returns: 485 True if success. 486 False if timeout. 487 """ 488 # Using timeout decorator. 489 # Wait for device with timeout. If after <timeout_value> seconds, adb 490 # is still waiting for device, throw TimeoutError exception. 491 @timeout(timeout_value) 492 def wait_for_device_with_timeout(ad): 493 ad.adb.wait_for_device() 494 495 try: 496 wait_for_device_with_timeout(ad) 497 ad.adb.shell("settings put global airplane_mode_on {}".format( 498 1 if new_state else 0)) 499 except TimeoutError: 500 # adb wait for device timeout 501 return False 502 return True 503 504 505def enable_doze(ad): 506 """Force the device into doze mode. 507 508 Args: 509 ad: android device object. 510 511 Returns: 512 True if device is in doze mode. 513 False otherwise. 514 """ 515 ad.adb.shell("dumpsys battery unplug") 516 ad.adb.shell("dumpsys deviceidle enable") 517 ad.adb.shell("dumpsys deviceidle force-idle") 518 ad.droid.goToSleepNow() 519 time.sleep(5) 520 adb_shell_result = ad.adb.shell("dumpsys deviceidle get deep").decode( 521 'utf-8') 522 if not adb_shell_result.startswith(DozeModeStatus.IDLE): 523 info = ("dumpsys deviceidle get deep: {}".format(adb_shell_result)) 524 print(info) 525 return False 526 return True 527 528 529def disable_doze(ad): 530 """Force the device not in doze mode. 531 532 Args: 533 ad: android device object. 534 535 Returns: 536 True if device is not in doze mode. 537 False otherwise. 538 """ 539 ad.adb.shell("dumpsys deviceidle disable") 540 ad.adb.shell("dumpsys battery reset") 541 adb_shell_result = ad.adb.shell("dumpsys deviceidle get deep").decode( 542 'utf-8') 543 if not adb_shell_result.startswith(DozeModeStatus.ACTIVE): 544 info = ("dumpsys deviceidle get deep: {}".format(adb_shell_result)) 545 print(info) 546 return False 547 return True 548 549 550def enable_doze_light(ad): 551 """Force the device into doze light mode. 552 553 Args: 554 ad: android device object. 555 556 Returns: 557 True if device is in doze light mode. 558 False otherwise. 559 """ 560 ad.adb.shell("dumpsys battery unplug") 561 ad.droid.goToSleepNow() 562 time.sleep(5) 563 ad.adb.shell("cmd deviceidle enable light") 564 ad.adb.shell("cmd deviceidle step light") 565 adb_shell_result = ad.adb.shell("dumpsys deviceidle get light").decode( 566 'utf-8') 567 if not adb_shell_result.startswith(DozeModeStatus.IDLE): 568 info = ("dumpsys deviceidle get light: {}".format(adb_shell_result)) 569 print(info) 570 return False 571 return True 572 573 574def disable_doze_light(ad): 575 """Force the device not in doze light mode. 576 577 Args: 578 ad: android device object. 579 580 Returns: 581 True if device is not in doze light mode. 582 False otherwise. 583 """ 584 ad.adb.shell("dumpsys battery reset") 585 ad.adb.shell("cmd deviceidle disable light") 586 adb_shell_result = ad.adb.shell("dumpsys deviceidle get light").decode( 587 'utf-8') 588 if not adb_shell_result.startswith(DozeModeStatus.ACTIVE): 589 info = ("dumpsys deviceidle get light: {}".format(adb_shell_result)) 590 print(info) 591 return False 592 return True 593 594 595def set_ambient_display(ad, new_state): 596 """Set "Ambient Display" in Settings->Display 597 598 Args: 599 ad: android device object. 600 new_state: new state for "Ambient Display". True or False. 601 """ 602 ad.adb.shell("settings put secure doze_enabled {}".format(1 if new_state 603 else 0)) 604 605 606def set_adaptive_brightness(ad, new_state): 607 """Set "Adaptive Brightness" in Settings->Display 608 609 Args: 610 ad: android device object. 611 new_state: new state for "Adaptive Brightness". True or False. 612 """ 613 ad.adb.shell("settings put system screen_brightness_mode {}".format( 614 1 if new_state else 0)) 615 616 617def set_auto_rotate(ad, new_state): 618 """Set "Auto-rotate" in QuickSetting 619 620 Args: 621 ad: android device object. 622 new_state: new state for "Auto-rotate". True or False. 623 """ 624 ad.adb.shell("settings put system accelerometer_rotation {}".format( 625 1 if new_state else 0)) 626 627 628def set_location_service(ad, new_state): 629 """Set Location service on/off in Settings->Location 630 631 Args: 632 ad: android device object. 633 new_state: new state for "Location service". 634 If new_state is False, turn off location service. 635 If new_state if True, set location service to "High accuracy". 636 """ 637 if new_state: 638 ad.adb.shell("settings put secure location_providers_allowed +gps") 639 ad.adb.shell("settings put secure location_providers_allowed +network") 640 else: 641 ad.adb.shell("settings put secure location_providers_allowed -gps") 642 ad.adb.shell("settings put secure location_providers_allowed -network") 643 644 645def set_mobile_data_always_on(ad, new_state): 646 """Set Mobile_Data_Always_On feature bit 647 648 Args: 649 ad: android device object. 650 new_state: new state for "mobile_data_always_on" 651 if new_state is False, set mobile_data_always_on disabled. 652 if new_state if True, set mobile_data_always_on enabled. 653 """ 654 ad.adb.shell("settings put global mobile_data_always_on {}".format( 655 1 if new_state else 0)) 656