1# This Source Code Form is subject to the terms of the Mozilla Public 2# License, v. 2.0. If a copy of the MPL was not distributed with this file, 3# You can obtain one at http://mozilla.org/MPL/2.0/. 4 5from __future__ import absolute_import, print_function 6 7import os 8import re 9import time 10 11from abc import ABCMeta 12 13from . import version_codes 14 15from .adb import ADBDevice, ADBError, ADBRootError 16 17 18class ADBAndroid(ADBDevice): 19 """ADBAndroid implements :class:`ADBDevice` providing Android-specific 20 functionality. 21 22 :: 23 24 from mozdevice import ADBAndroid 25 26 adbdevice = ADBAndroid() 27 print(adbdevice.list_files("/mnt/sdcard")) 28 if adbdevice.process_exist("org.mozilla.fennec"): 29 print("Fennec is running") 30 """ 31 __metaclass__ = ABCMeta 32 33 def __init__(self, 34 device=None, 35 adb='adb', 36 adb_host=None, 37 adb_port=None, 38 test_root='', 39 logger_name='adb', 40 timeout=300, 41 verbose=False, 42 device_ready_retry_wait=20, 43 device_ready_retry_attempts=3): 44 """Initializes the ADBAndroid object. 45 46 :param device: When a string is passed, it is interpreted as the 47 device serial number. This form is not compatible with 48 devices containing a ":" in the serial; in this case 49 ValueError will be raised. 50 When a dictionary is passed it must have one or both of 51 the keys "device_serial" and "usb". This is compatible 52 with the dictionaries in the list returned by 53 ADBHost.devices(). If the value of device_serial is a 54 valid serial not containing a ":" it will be used to 55 identify the device, otherwise the value of the usb key, 56 prefixed with "usb:" is used. 57 If None is passed and there is exactly one device attached 58 to the host, that device is used. If there is more than one 59 device attached, ValueError is raised. If no device is 60 attached the constructor will block until a device is 61 attached or the timeout is reached. 62 :type device: dict, str or None 63 :param adb_host: host of the adb server to connect to. 64 :type adb_host: str or None 65 :param adb_port: port of the adb server to connect to. 66 :type adb_port: integer or None 67 :param str logger_name: logging logger name. Defaults to 'adb'. 68 :param integer device_ready_retry_wait: number of seconds to wait 69 between attempts to check if the device is ready after a 70 reboot. 71 :param integer device_ready_retry_attempts: number of attempts when 72 checking if a device is ready. 73 74 :raises: * ADBError 75 * ADBTimeoutError 76 * ValueError 77 """ 78 ADBDevice.__init__(self, device=device, adb=adb, 79 adb_host=adb_host, adb_port=adb_port, 80 test_root=test_root, 81 logger_name=logger_name, timeout=timeout, 82 verbose=verbose, 83 device_ready_retry_wait=device_ready_retry_wait, 84 device_ready_retry_attempts=device_ready_retry_attempts) 85 # https://source.android.com/devices/tech/security/selinux/index.html 86 # setenforce 87 # usage: setenforce [ Enforcing | Permissive | 1 | 0 ] 88 # getenforce returns either Enforcing or Permissive 89 90 try: 91 self.selinux = True 92 if self.shell_output('getenforce', timeout=timeout) != 'Permissive': 93 self._logger.info('Setting SELinux Permissive Mode') 94 self.shell_output("setenforce Permissive", timeout=timeout, root=True) 95 except (ADBError, ADBRootError) as e: 96 self._logger.warning('Unable to set SELinux Permissive due to %s.' % e) 97 self.selinux = False 98 99 self.version = int(self.shell_output("getprop ro.build.version.sdk", 100 timeout=timeout)) 101 102 def reboot(self, timeout=None): 103 """Reboots the device. 104 105 :param timeout: optional integer specifying the maximum time in 106 seconds for any spawned adb process to complete before 107 throwing an ADBTimeoutError. 108 This timeout is per adb call. The total time spent 109 may exceed this value. If it is not specified, the value 110 set in the ADB constructor is used. 111 :raises: * ADBTimeoutError 112 * ADBError 113 114 reboot() reboots the device, issues an adb wait-for-device in order to 115 wait for the device to complete rebooting, then calls is_device_ready() 116 to determine if the device has completed booting. 117 118 If the device supports running adbd as root, adbd will be 119 restarted running as root. Then, if the device supports 120 SELinux, setenforce Permissive will be called to change 121 SELinux to permissive. This must be done after adbd is 122 restarted in order for the SELinux Permissive setting to 123 persist. 124 125 """ 126 ready = ADBDevice.reboot(self, timeout=timeout) 127 self._check_adb_root(timeout=timeout) 128 return ready 129 130 # Informational methods 131 132 def get_battery_percentage(self, timeout=None): 133 """Returns the battery charge as a percentage. 134 135 :param timeout: The maximum time in 136 seconds for any spawned adb process to complete before 137 throwing an ADBTimeoutError. 138 This timeout is per adb call. The total time spent 139 may exceed this value. If it is not specified, the value 140 set in the ADBDevice constructor is used. 141 :type timeout: integer or None 142 :returns: battery charge as a percentage. 143 :raises: * ADBTimeoutError 144 * ADBError 145 """ 146 level = None 147 scale = None 148 percentage = 0 149 cmd = "dumpsys battery" 150 re_parameter = re.compile(r'\s+(\w+):\s+(\d+)') 151 lines = self.shell_output(cmd, timeout=timeout).splitlines() 152 for line in lines: 153 match = re_parameter.match(line) 154 if match: 155 parameter = match.group(1) 156 value = match.group(2) 157 if parameter == 'level': 158 level = float(value) 159 elif parameter == 'scale': 160 scale = float(value) 161 if parameter is not None and scale is not None: 162 percentage = 100.0 * level / scale 163 break 164 return percentage 165 166 def get_top_activity(self, timeout=None): 167 """Returns the name of the top activity (focused app) reported by dumpsys 168 169 :param timeout: The maximum time in 170 seconds for any spawned adb process to complete before 171 throwing an ADBTimeoutError. 172 This timeout is per adb call. The total time spent 173 may exceed this value. If it is not specified, the value 174 set in the ADBDevice constructor is used. 175 :type timeout: integer or None 176 :returns: package name of top activity or None (cannot be determined) 177 :raises: * ADBTimeoutError 178 * ADBError 179 """ 180 package = None 181 data = None 182 cmd = "dumpsys window windows" 183 try: 184 data = self.shell_output(cmd, timeout=timeout) 185 except Exception: 186 # dumpsys intermittently fails on some platforms (4.3 arm emulator) 187 return package 188 m = re.search('mFocusedApp(.+)/', data) 189 if not m: 190 # alternative format seen on newer versions of Android 191 m = re.search('FocusedApplication(.+)/', data) 192 if m: 193 line = m.group(0) 194 # Extract package name: string of non-whitespace ending in forward slash 195 m = re.search('(\S+)/$', line) 196 if m: 197 package = m.group(1) 198 if self._verbose: 199 self._logger.debug('get_top_activity: %s' % str(package)) 200 return package 201 202 # System control methods 203 204 def is_device_ready(self, timeout=None): 205 """Checks if a device is ready for testing. 206 207 This method uses the android only package manager to check for 208 readiness. 209 210 :param timeout: The maximum time 211 in seconds for any spawned adb process to complete before 212 throwing an ADBTimeoutError. 213 This timeout is per adb call. The total time spent 214 may exceed this value. If it is not specified, the value 215 set in the ADB constructor is used. 216 :type timeout: integer or None 217 :raises: * ADBTimeoutError 218 * ADBError 219 """ 220 # command_output automatically inserts a 'wait-for-device' 221 # argument to adb. Issuing an empty command is the same as adb 222 # -s <device> wait-for-device. We don't send an explicit 223 # 'wait-for-device' since that would add duplicate 224 # 'wait-for-device' arguments which is an error in newer 225 # versions of adb. 226 self.command_output([], timeout=timeout) 227 pm_error_string = "Error: Could not access the Package Manager" 228 pm_list_commands = ["packages", "permission-groups", "permissions", 229 "instrumentation", "features", "libraries"] 230 ready_path = os.path.join(self.test_root, "ready") 231 for attempt in range(self._device_ready_retry_attempts): 232 failure = 'Unknown failure' 233 success = True 234 try: 235 state = self.get_state(timeout=timeout) 236 if state != 'device': 237 failure = "Device state: %s" % state 238 success = False 239 else: 240 if (self.selinux and self.shell_output('getenforce', 241 timeout=timeout) != 'Permissive'): 242 self._logger.info('Setting SELinux Permissive Mode') 243 self.shell_output("setenforce Permissive", timeout=timeout, root=True) 244 if self.is_dir(ready_path, timeout=timeout): 245 self.rmdir(ready_path, timeout=timeout) 246 self.mkdir(ready_path, timeout=timeout) 247 self.rmdir(ready_path, timeout=timeout) 248 # Invoke the pm list commands to see if it is up and 249 # running. 250 for pm_list_cmd in pm_list_commands: 251 data = self.shell_output("pm list %s" % pm_list_cmd, 252 timeout=timeout) 253 if pm_error_string in data: 254 failure = data 255 success = False 256 break 257 except ADBError as e: 258 success = False 259 failure = e.message 260 261 if not success: 262 self._logger.debug('Attempt %s of %s device not ready: %s' % ( 263 attempt + 1, self._device_ready_retry_attempts, 264 failure)) 265 time.sleep(self._device_ready_retry_wait) 266 267 return success 268 269 def power_on(self, timeout=None): 270 """Sets the device's power stayon value. 271 272 :param timeout: The maximum time in 273 seconds for any spawned adb process to complete before 274 throwing an ADBTimeoutError. 275 This timeout is per adb call. The total time spent 276 may exceed this value. If it is not specified, the value 277 set in the ADB constructor is used. 278 :type timeout: integer or None 279 :raises: * ADBTimeoutError 280 * ADBError 281 """ 282 try: 283 self.shell_output('svc power stayon true', 284 timeout=timeout, 285 root=True) 286 except ADBError as e: 287 # Executing this via adb shell errors, but not interactively. 288 # Any other exitcode is a real error. 289 if 'exitcode: 137' not in e.message: 290 raise 291 self._logger.warning('Unable to set power stayon true: %s' % e) 292 293 # Application management methods 294 295 def install_app(self, apk_path, replace=False, timeout=None): 296 """Installs an app on the device. 297 298 :param str apk_path: The apk file name to be installed. 299 :param bool replace: If True, replace existing application. 300 :param timeout: The maximum time in 301 seconds for any spawned adb process to complete before 302 throwing an ADBTimeoutError. 303 This timeout is per adb call. The total time spent 304 may exceed this value. If it is not specified, the value 305 set in the ADB constructor is used. 306 :type timeout: integer or None 307 :raises: * ADBTimeoutError 308 * ADBError 309 """ 310 cmd = ["install"] 311 if self.version >= version_codes.M: 312 cmd.append("-g") 313 if replace: 314 cmd.append("-r") 315 cmd.append(apk_path) 316 data = self.command_output(cmd, timeout=timeout) 317 if data.find('Success') == -1: 318 raise ADBError("install failed for %s. Got: %s" % 319 (apk_path, data)) 320 321 def is_app_installed(self, app_name, timeout=None): 322 """Returns True if an app is installed on the device. 323 324 :param str app_name: The name of the app to be checked. 325 :param timeout: The maximum time in 326 seconds for any spawned adb process to complete before 327 throwing an ADBTimeoutError. 328 This timeout is per adb call. The total time spent 329 may exceed this value. If it is not specified, the value 330 set in the ADB constructor is used. 331 :type timeout: integer or None 332 :raises: * ADBTimeoutError 333 * ADBError 334 """ 335 pm_error_string = 'Error: Could not access the Package Manager' 336 data = self.shell_output("pm list package %s" % app_name, timeout=timeout) 337 if pm_error_string in data: 338 raise ADBError(pm_error_string) 339 if app_name not in data: 340 return False 341 return True 342 343 def launch_application(self, app_name, activity_name, intent, url=None, 344 extras=None, wait=True, fail_if_running=True, 345 timeout=None): 346 """Launches an Android application 347 348 :param str app_name: Name of application (e.g. `com.android.chrome`) 349 :param str activity_name: Name of activity to launch (e.g. `.Main`) 350 :param str intent: Intent to launch application with 351 :param url: URL to open 352 :type url: str or None 353 :param extras: Extra arguments for application. 354 :type extras: dict or None 355 :param bool wait: If True, wait for application to start before 356 returning. 357 :param bool fail_if_running: Raise an exception if instance of 358 application is already running. 359 :param timeout: The maximum time in 360 seconds for any spawned adb process to complete before 361 throwing an ADBTimeoutError. 362 This timeout is per adb call. The total time spent 363 may exceed this value. If it is not specified, the value 364 set in the ADB constructor is used. 365 :type timeout: integer or None 366 :raises: * ADBTimeoutError 367 * ADBError 368 """ 369 # If fail_if_running is True, we throw an exception here. Only one 370 # instance of an application can be running at once on Android, 371 # starting a new instance may not be what we want depending on what 372 # we want to do 373 if fail_if_running and self.process_exist(app_name, timeout=timeout): 374 raise ADBError("Only one instance of an application may be running " 375 "at once") 376 377 acmd = ["am", "start"] + \ 378 ["-W" if wait else '', "-n", "%s/%s" % (app_name, activity_name)] 379 380 if intent: 381 acmd.extend(["-a", intent]) 382 383 if extras: 384 for (key, val) in extras.iteritems(): 385 if isinstance(val, int): 386 extra_type_param = "--ei" 387 elif isinstance(val, bool): 388 extra_type_param = "--ez" 389 else: 390 extra_type_param = "--es" 391 acmd.extend([extra_type_param, str(key), str(val)]) 392 393 if url: 394 acmd.extend(["-d", url]) 395 396 cmd = self._escape_command_line(acmd) 397 self.shell_output(cmd, timeout=timeout) 398 399 def launch_fennec(self, app_name, intent="android.intent.action.VIEW", 400 moz_env=None, extra_args=None, url=None, wait=True, 401 fail_if_running=True, timeout=None): 402 """Convenience method to launch Fennec on Android with various 403 debugging arguments 404 405 :param str app_name: Name of fennec application (e.g. 406 `org.mozilla.fennec`) 407 :param str intent: Intent to launch application. 408 :param moz_env: Mozilla specific environment to pass into 409 application. 410 :type moz_env: str or None 411 :param extra_args: Extra arguments to be parsed by fennec. 412 :type extra_args: str or None 413 :param url: URL to open 414 :type url: str or None 415 :param bool wait: If True, wait for application to start before 416 returning. 417 :param bool fail_if_running: Raise an exception if instance of 418 application is already running. 419 :param timeout: The maximum time in 420 seconds for any spawned adb process to complete before 421 throwing an ADBTimeoutError. 422 This timeout is per adb call. The total time spent 423 may exceed this value. If it is not specified, the value 424 set in the ADB constructor is used. 425 :type timeout: integer or None 426 :raises: * ADBTimeoutError 427 * ADBError 428 """ 429 extras = {} 430 431 if moz_env: 432 # moz_env is expected to be a dictionary of environment variables: 433 # Fennec itself will set them when launched 434 for (env_count, (env_key, env_val)) in enumerate(moz_env.iteritems()): 435 extras["env" + str(env_count)] = env_key + "=" + env_val 436 437 # Additional command line arguments that fennec will read and use (e.g. 438 # with a custom profile) 439 if extra_args: 440 extras['args'] = " ".join(extra_args) 441 442 self.launch_application(app_name, ".App", 443 intent, url=url, extras=extras, 444 wait=wait, fail_if_running=fail_if_running, 445 timeout=timeout) 446 447 def stop_application(self, app_name, timeout=None, root=False): 448 """Stops the specified application 449 450 For Android 3.0+, we use the "am force-stop" to do this, which 451 is reliable and does not require root. For earlier versions of 452 Android, we simply try to manually kill the processes started 453 by the app repeatedly until none is around any more. This is 454 less reliable and does require root. 455 456 :param str app_name: Name of application (e.g. `com.android.chrome`) 457 :param timeout: The maximum time in 458 seconds for any spawned adb process to complete before 459 throwing an ADBTimeoutError. 460 This timeout is per adb call. The total time spent 461 may exceed this value. If it is not specified, the value 462 set in the ADB constructor is used. 463 :type timeout: integer or None 464 :param bool root: Flag specifying if the command should be 465 executed as root. 466 :raises: * ADBTimeoutError 467 * ADBError 468 """ 469 if self.version >= version_codes.HONEYCOMB: 470 self.shell_output("am force-stop %s" % app_name, 471 timeout=timeout, root=root) 472 else: 473 num_tries = 0 474 max_tries = 5 475 while self.process_exist(app_name, timeout=timeout): 476 if num_tries > max_tries: 477 raise ADBError("Couldn't successfully kill %s after %s " 478 "tries" % (app_name, max_tries)) 479 self.pkill(app_name, timeout=timeout, root=root) 480 num_tries += 1 481 482 # sleep for a short duration to make sure there are no 483 # additional processes in the process of being launched 484 # (this is not 100% guaranteed to work since it is inherently 485 # racey, but it's the best we can do) 486 time.sleep(1) 487 488 def uninstall_app(self, app_name, reboot=False, timeout=None): 489 """Uninstalls an app on the device. 490 491 :param str app_name: The name of the app to be 492 uninstalled. 493 :param bool reboot: Flag indicating that the device should 494 be rebooted after the app is uninstalled. No reboot occurs 495 if the app is not installed. 496 :param timeout: The maximum time in 497 seconds for any spawned adb process to complete before 498 throwing an ADBTimeoutError. 499 This timeout is per adb call. The total time spent 500 may exceed this value. If it is not specified, the value 501 set in the ADB constructor is used. 502 :type timeout: integer or None 503 :raises: * ADBTimeoutError 504 * ADBError 505 """ 506 if self.is_app_installed(app_name, timeout=timeout): 507 data = self.command_output(["uninstall", app_name], timeout=timeout) 508 if data.find('Success') == -1: 509 self._logger.debug('uninstall_app failed: %s' % data) 510 raise ADBError("uninstall failed for %s. Got: %s" % (app_name, data)) 511 if reboot: 512 self.reboot(timeout=timeout) 513 514 def update_app(self, apk_path, timeout=None): 515 """Updates an app on the device and reboots. 516 517 :param str apk_path: The apk file name to be 518 updated. 519 :param timeout: The maximum time in 520 seconds for any spawned adb process to complete before 521 throwing an ADBTimeoutError. 522 This timeout is per adb call. The total time spent 523 may exceed this value. If it is not specified, the value 524 set in the ADB constructor is used. 525 :type timeout: integer or None 526 :raises: * ADBTimeoutError 527 * ADBError 528 """ 529 cmd = ["install", "-r"] 530 if self.version >= version_codes.M: 531 cmd.append("-g") 532 cmd.append(apk_path) 533 output = self.command_output(cmd, timeout=timeout) 534 self.reboot(timeout=timeout) 535 return output 536