summaryrefslogtreecommitdiffstats
path: root/testing/mozbase/mozdevice/mozdevice/adb_android.py
diff options
context:
space:
mode:
Diffstat (limited to 'testing/mozbase/mozdevice/mozdevice/adb_android.py')
-rw-r--r--testing/mozbase/mozdevice/mozdevice/adb_android.py493
1 files changed, 493 insertions, 0 deletions
diff --git a/testing/mozbase/mozdevice/mozdevice/adb_android.py b/testing/mozbase/mozdevice/mozdevice/adb_android.py
new file mode 100644
index 000000000..bf5fffc0e
--- /dev/null
+++ b/testing/mozbase/mozdevice/mozdevice/adb_android.py
@@ -0,0 +1,493 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import os
+import re
+import time
+
+from abc import ABCMeta
+
+import version_codes
+
+from adb import ADBDevice, ADBError
+
+
+class ADBAndroid(ADBDevice):
+ """ADBAndroid implements :class:`ADBDevice` providing Android-specific
+ functionality.
+
+ ::
+
+ from mozdevice import ADBAndroid
+
+ adbdevice = ADBAndroid()
+ print adbdevice.list_files("/mnt/sdcard")
+ if adbdevice.process_exist("org.mozilla.fennec"):
+ print "Fennec is running"
+ """
+ __metaclass__ = ABCMeta
+
+ def __init__(self,
+ device=None,
+ adb='adb',
+ adb_host=None,
+ adb_port=None,
+ test_root='',
+ logger_name='adb',
+ timeout=300,
+ verbose=False,
+ device_ready_retry_wait=20,
+ device_ready_retry_attempts=3):
+ """Initializes the ADBAndroid object.
+
+ :param device: When a string is passed, it is interpreted as the
+ device serial number. This form is not compatible with
+ devices containing a ":" in the serial; in this case
+ ValueError will be raised.
+ When a dictionary is passed it must have one or both of
+ the keys "device_serial" and "usb". This is compatible
+ with the dictionaries in the list returned by
+ ADBHost.devices(). If the value of device_serial is a
+ valid serial not containing a ":" it will be used to
+ identify the device, otherwise the value of the usb key,
+ prefixed with "usb:" is used.
+ If None is passed and there is exactly one device attached
+ to the host, that device is used. If there is more than one
+ device attached, ValueError is raised. If no device is
+ attached the constructor will block until a device is
+ attached or the timeout is reached.
+ :type device: dict, str or None
+ :param adb_host: host of the adb server to connect to.
+ :type adb_host: str or None
+ :param adb_port: port of the adb server to connect to.
+ :type adb_port: integer or None
+ :param str logger_name: logging logger name. Defaults to 'adb'.
+ :param integer device_ready_retry_wait: number of seconds to wait
+ between attempts to check if the device is ready after a
+ reboot.
+ :param integer device_ready_retry_attempts: number of attempts when
+ checking if a device is ready.
+
+ :raises: * ADBError
+ * ADBTimeoutError
+ * ValueError
+ """
+ ADBDevice.__init__(self, device=device, adb=adb,
+ adb_host=adb_host, adb_port=adb_port,
+ test_root=test_root,
+ logger_name=logger_name, timeout=timeout,
+ verbose=verbose,
+ device_ready_retry_wait=device_ready_retry_wait,
+ device_ready_retry_attempts=device_ready_retry_attempts)
+ # https://source.android.com/devices/tech/security/selinux/index.html
+ # setenforce
+ # usage: setenforce [ Enforcing | Permissive | 1 | 0 ]
+ # getenforce returns either Enforcing or Permissive
+
+ try:
+ self.selinux = True
+ if self.shell_output('getenforce', timeout=timeout) != 'Permissive':
+ self._logger.info('Setting SELinux Permissive Mode')
+ self.shell_output("setenforce Permissive", timeout=timeout, root=True)
+ except ADBError:
+ self.selinux = False
+
+ self.version = int(self.shell_output("getprop ro.build.version.sdk",
+ timeout=timeout))
+
+ def reboot(self, timeout=None):
+ """Reboots the device.
+
+ :param timeout: optional integer specifying the maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADB constructor is used.
+ :raises: * ADBTimeoutError
+ * ADBError
+
+ reboot() reboots the device, issues an adb wait-for-device in order to
+ wait for the device to complete rebooting, then calls is_device_ready()
+ to determine if the device has completed booting.
+
+ If the device supports running adbd as root, adbd will be
+ restarted running as root. Then, if the device supports
+ SELinux, setenforce Permissive will be called to change
+ SELinux to permissive. This must be done after adbd is
+ restarted in order for the SELinux Permissive setting to
+ persist.
+
+ """
+ ready = ADBDevice.reboot(self, timeout=timeout)
+ self._check_adb_root(timeout=timeout)
+ return ready
+
+ # Informational methods
+
+ def get_battery_percentage(self, timeout=None):
+ """Returns the battery charge as a percentage.
+
+ :param timeout: The maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADBDevice constructor is used.
+ :type timeout: integer or None
+ :returns: battery charge as a percentage.
+ :raises: * ADBTimeoutError
+ * ADBError
+ """
+ level = None
+ scale = None
+ percentage = 0
+ cmd = "dumpsys battery"
+ re_parameter = re.compile(r'\s+(\w+):\s+(\d+)')
+ lines = self.shell_output(cmd, timeout=timeout).split('\r')
+ for line in lines:
+ match = re_parameter.match(line)
+ if match:
+ parameter = match.group(1)
+ value = match.group(2)
+ if parameter == 'level':
+ level = float(value)
+ elif parameter == 'scale':
+ scale = float(value)
+ if parameter is not None and scale is not None:
+ percentage = 100.0 * level / scale
+ break
+ return percentage
+
+ # System control methods
+
+ def is_device_ready(self, timeout=None):
+ """Checks if a device is ready for testing.
+
+ This method uses the android only package manager to check for
+ readiness.
+
+ :param timeout: The maximum time
+ in seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADB constructor is used.
+ :type timeout: integer or None
+ :raises: * ADBTimeoutError
+ * ADBError
+ """
+ # command_output automatically inserts a 'wait-for-device'
+ # argument to adb. Issuing an empty command is the same as adb
+ # -s <device> wait-for-device. We don't send an explicit
+ # 'wait-for-device' since that would add duplicate
+ # 'wait-for-device' arguments which is an error in newer
+ # versions of adb.
+ self.command_output([], timeout=timeout)
+ pm_error_string = "Error: Could not access the Package Manager"
+ pm_list_commands = ["packages", "permission-groups", "permissions",
+ "instrumentation", "features", "libraries"]
+ ready_path = os.path.join(self.test_root, "ready")
+ for attempt in range(self._device_ready_retry_attempts):
+ failure = 'Unknown failure'
+ success = True
+ try:
+ state = self.get_state(timeout=timeout)
+ if state != 'device':
+ failure = "Device state: %s" % state
+ success = False
+ else:
+ if (self.selinux and self.shell_output('getenforce',
+ timeout=timeout) != 'Permissive'):
+ self._logger.info('Setting SELinux Permissive Mode')
+ self.shell_output("setenforce Permissive", timeout=timeout, root=True)
+ if self.is_dir(ready_path, timeout=timeout, root=True):
+ self.rmdir(ready_path, timeout=timeout, root=True)
+ self.mkdir(ready_path, timeout=timeout, root=True)
+ self.rmdir(ready_path, timeout=timeout, root=True)
+ # Invoke the pm list commands to see if it is up and
+ # running.
+ for pm_list_cmd in pm_list_commands:
+ data = self.shell_output("pm list %s" % pm_list_cmd,
+ timeout=timeout)
+ if pm_error_string in data:
+ failure = data
+ success = False
+ break
+ except ADBError as e:
+ success = False
+ failure = e.message
+
+ if not success:
+ self._logger.debug('Attempt %s of %s device not ready: %s' % (
+ attempt + 1, self._device_ready_retry_attempts,
+ failure))
+ time.sleep(self._device_ready_retry_wait)
+
+ return success
+
+ def power_on(self, timeout=None):
+ """Sets the device's power stayon value.
+
+ :param timeout: The maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADB constructor is used.
+ :type timeout: integer or None
+ :raises: * ADBTimeoutError
+ * ADBError
+ """
+ try:
+ self.shell_output('svc power stayon true',
+ timeout=timeout,
+ root=True)
+ except ADBError as e:
+ # Executing this via adb shell errors, but not interactively.
+ # Any other exitcode is a real error.
+ if 'exitcode: 137' not in e.message:
+ raise
+ self._logger.warning('Unable to set power stayon true: %s' % e)
+
+ # Application management methods
+
+ def install_app(self, apk_path, timeout=None):
+ """Installs an app on the device.
+
+ :param str apk_path: The apk file name to be installed.
+ :param timeout: The maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADB constructor is used.
+ :type timeout: integer or None
+ :raises: * ADBTimeoutError
+ * ADBError
+ """
+ cmd = ["install"]
+ if self.version >= version_codes.M:
+ cmd.append("-g")
+ cmd.append(apk_path)
+ data = self.command_output(cmd, timeout=timeout)
+ if data.find('Success') == -1:
+ raise ADBError("install failed for %s. Got: %s" %
+ (apk_path, data))
+
+ def is_app_installed(self, app_name, timeout=None):
+ """Returns True if an app is installed on the device.
+
+ :param str app_name: The name of the app to be checked.
+ :param timeout: The maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADB constructor is used.
+ :type timeout: integer or None
+ :raises: * ADBTimeoutError
+ * ADBError
+ """
+ pm_error_string = 'Error: Could not access the Package Manager'
+ data = self.shell_output("pm list package %s" % app_name, timeout=timeout)
+ if pm_error_string in data:
+ raise ADBError(pm_error_string)
+ if app_name not in data:
+ return False
+ return True
+
+ def launch_application(self, app_name, activity_name, intent, url=None,
+ extras=None, wait=True, fail_if_running=True,
+ timeout=None):
+ """Launches an Android application
+
+ :param str app_name: Name of application (e.g. `com.android.chrome`)
+ :param str activity_name: Name of activity to launch (e.g. `.Main`)
+ :param str intent: Intent to launch application with
+ :param url: URL to open
+ :type url: str or None
+ :param extras: Extra arguments for application.
+ :type extras: dict or None
+ :param bool wait: If True, wait for application to start before
+ returning.
+ :param bool fail_if_running: Raise an exception if instance of
+ application is already running.
+ :param timeout: The maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADB constructor is used.
+ :type timeout: integer or None
+ :raises: * ADBTimeoutError
+ * ADBError
+ """
+ # If fail_if_running is True, we throw an exception here. Only one
+ # instance of an application can be running at once on Android,
+ # starting a new instance may not be what we want depending on what
+ # we want to do
+ if fail_if_running and self.process_exist(app_name, timeout=timeout):
+ raise ADBError("Only one instance of an application may be running "
+ "at once")
+
+ acmd = ["am", "start"] + \
+ ["-W" if wait else '', "-n", "%s/%s" % (app_name, activity_name)]
+
+ if intent:
+ acmd.extend(["-a", intent])
+
+ if extras:
+ for (key, val) in extras.iteritems():
+ if isinstance(val, int):
+ extra_type_param = "--ei"
+ elif isinstance(val, bool):
+ extra_type_param = "--ez"
+ else:
+ extra_type_param = "--es"
+ acmd.extend([extra_type_param, str(key), str(val)])
+
+ if url:
+ acmd.extend(["-d", url])
+
+ cmd = self._escape_command_line(acmd)
+ self.shell_output(cmd, timeout=timeout)
+
+ def launch_fennec(self, app_name, intent="android.intent.action.VIEW",
+ moz_env=None, extra_args=None, url=None, wait=True,
+ fail_if_running=True, timeout=None):
+ """Convenience method to launch Fennec on Android with various
+ debugging arguments
+
+ :param str app_name: Name of fennec application (e.g.
+ `org.mozilla.fennec`)
+ :param str intent: Intent to launch application.
+ :param moz_env: Mozilla specific environment to pass into
+ application.
+ :type moz_env: str or None
+ :param extra_args: Extra arguments to be parsed by fennec.
+ :type extra_args: str or None
+ :param url: URL to open
+ :type url: str or None
+ :param bool wait: If True, wait for application to start before
+ returning.
+ :param bool fail_if_running: Raise an exception if instance of
+ application is already running.
+ :param timeout: The maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADB constructor is used.
+ :type timeout: integer or None
+ :raises: * ADBTimeoutError
+ * ADBError
+ """
+ extras = {}
+
+ if moz_env:
+ # moz_env is expected to be a dictionary of environment variables:
+ # Fennec itself will set them when launched
+ for (env_count, (env_key, env_val)) in enumerate(moz_env.iteritems()):
+ extras["env" + str(env_count)] = env_key + "=" + env_val
+
+ # Additional command line arguments that fennec will read and use (e.g.
+ # with a custom profile)
+ if extra_args:
+ extras['args'] = " ".join(extra_args)
+
+ self.launch_application(app_name, "org.mozilla.gecko.BrowserApp",
+ intent, url=url, extras=extras,
+ wait=wait, fail_if_running=fail_if_running,
+ timeout=timeout)
+
+ def stop_application(self, app_name, timeout=None, root=False):
+ """Stops the specified application
+
+ For Android 3.0+, we use the "am force-stop" to do this, which
+ is reliable and does not require root. For earlier versions of
+ Android, we simply try to manually kill the processes started
+ by the app repeatedly until none is around any more. This is
+ less reliable and does require root.
+
+ :param str app_name: Name of application (e.g. `com.android.chrome`)
+ :param timeout: The maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADB constructor is used.
+ :type timeout: integer or None
+ :param bool root: Flag specifying if the command should be
+ executed as root.
+ :raises: * ADBTimeoutError
+ * ADBError
+ """
+ if self.version >= version_codes.HONEYCOMB:
+ self.shell_output("am force-stop %s" % app_name,
+ timeout=timeout, root=root)
+ else:
+ num_tries = 0
+ max_tries = 5
+ while self.process_exist(app_name, timeout=timeout):
+ if num_tries > max_tries:
+ raise ADBError("Couldn't successfully kill %s after %s "
+ "tries" % (app_name, max_tries))
+ self.pkill(app_name, timeout=timeout, root=root)
+ num_tries += 1
+
+ # sleep for a short duration to make sure there are no
+ # additional processes in the process of being launched
+ # (this is not 100% guaranteed to work since it is inherently
+ # racey, but it's the best we can do)
+ time.sleep(1)
+
+ def uninstall_app(self, app_name, reboot=False, timeout=None):
+ """Uninstalls an app on the device.
+
+ :param str app_name: The name of the app to be
+ uninstalled.
+ :param bool reboot: Flag indicating that the device should
+ be rebooted after the app is uninstalled. No reboot occurs
+ if the app is not installed.
+ :param timeout: The maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADB constructor is used.
+ :type timeout: integer or None
+ :raises: * ADBTimeoutError
+ * ADBError
+ """
+ if self.is_app_installed(app_name, timeout=timeout):
+ data = self.command_output(["uninstall", app_name], timeout=timeout)
+ if data.find('Success') == -1:
+ self._logger.debug('uninstall_app failed: %s' % data)
+ raise ADBError("uninstall failed for %s. Got: %s" % (app_name, data))
+ if reboot:
+ self.reboot(timeout=timeout)
+
+ def update_app(self, apk_path, timeout=None):
+ """Updates an app on the device and reboots.
+
+ :param str apk_path: The apk file name to be
+ updated.
+ :param timeout: The maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADB constructor is used.
+ :type timeout: integer or None
+ :raises: * ADBTimeoutError
+ * ADBError
+ """
+ cmd = ["install", "-r"]
+ if self.version >= version_codes.M:
+ cmd.append("-g")
+ cmd.append(apk_path)
+ output = self.command_output(cmd, timeout=timeout)
+ self.reboot(timeout=timeout)
+ return output