2015-03-10 13:09:31 +00:00
|
|
|
# Copyright 2013-2015 ARM Limited
|
|
|
|
#
|
|
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
# you may not use this file except in compliance with the License.
|
|
|
|
# You may obtain a copy of the License at
|
|
|
|
#
|
|
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
#
|
|
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
# See the License for the specific language governing permissions and
|
|
|
|
# limitations under the License.
|
|
|
|
#
|
|
|
|
|
|
|
|
# pylint: disable=E1101
|
|
|
|
import os
|
|
|
|
import sys
|
|
|
|
import re
|
|
|
|
import time
|
|
|
|
import tempfile
|
|
|
|
import shutil
|
|
|
|
import threading
|
2016-05-24 11:27:29 +01:00
|
|
|
import json
|
2016-08-02 16:21:47 +01:00
|
|
|
import xml.dom.minidom
|
2015-03-10 13:09:31 +00:00
|
|
|
from subprocess import CalledProcessError
|
|
|
|
|
|
|
|
from wlauto.core.extension import Parameter
|
2016-05-09 16:10:02 +01:00
|
|
|
from wlauto.common.resources import Executable
|
|
|
|
from wlauto.core.resource import NO_ONE
|
2015-04-28 13:16:48 +01:00
|
|
|
from wlauto.common.linux.device import BaseLinuxDevice, PsEntry
|
2015-03-10 13:09:31 +00:00
|
|
|
from wlauto.exceptions import DeviceError, WorkerThreadError, TimeoutError, DeviceNotRespondingError
|
2016-12-07 16:34:15 +00:00
|
|
|
from wlauto.utils.misc import convert_new_lines, ABI_MAP
|
2015-03-10 13:09:31 +00:00
|
|
|
from wlauto.utils.types import boolean, regex
|
|
|
|
from wlauto.utils.android import (adb_shell, adb_background_shell, adb_list_devices,
|
|
|
|
adb_command, AndroidProperties, ANDROID_VERSION_MAP)
|
|
|
|
|
|
|
|
|
2015-12-11 10:58:32 +00:00
|
|
|
SCREEN_STATE_REGEX = re.compile('(?:mPowerState|mScreenOn|Display Power: state)=([0-9]+|true|false|ON|OFF)', re.I)
|
2015-10-01 09:53:21 +01:00
|
|
|
SCREEN_SIZE_REGEX = re.compile(r'mUnrestrictedScreen=\(\d+,\d+\)\s+(?P<width>\d+)x(?P<height>\d+)')
|
2015-03-10 13:09:31 +00:00
|
|
|
|
|
|
|
|
|
|
|
class AndroidDevice(BaseLinuxDevice): # pylint: disable=W0223
|
|
|
|
"""
|
|
|
|
Device running Android OS.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
platform = 'android'
|
|
|
|
|
|
|
|
parameters = [
|
|
|
|
Parameter('adb_name',
|
|
|
|
description='The unique ID of the device as output by "adb devices".'),
|
2015-06-26 16:25:44 +01:00
|
|
|
Parameter('android_prompt', kind=regex, default=re.compile('^.*(shell|root)@.*:/\S* [#$] ', re.MULTILINE),
|
2015-03-10 13:09:31 +00:00
|
|
|
description='The format of matching the shell prompt in Android.'),
|
2016-06-21 13:25:41 +01:00
|
|
|
Parameter('working_directory', default='/sdcard/wa-working', override=True),
|
|
|
|
Parameter('binaries_directory', default='/data/local/tmp/wa-bin', override=True,
|
2015-03-10 13:09:31 +00:00
|
|
|
description='Location of binaries on the device.'),
|
|
|
|
Parameter('package_data_directory', default='/data/data',
|
|
|
|
description='Location of of data for an installed package (APK).'),
|
|
|
|
Parameter('external_storage_directory', default='/sdcard',
|
|
|
|
description='Mount point for external storage.'),
|
|
|
|
Parameter('connection', default='usb', allowed_values=['usb', 'ethernet'],
|
|
|
|
description='Specified the nature of adb connection.'),
|
|
|
|
Parameter('logcat_poll_period', kind=int,
|
|
|
|
description="""
|
|
|
|
If specified and is not ``0``, logcat will be polled every
|
|
|
|
``logcat_poll_period`` seconds, and buffered on the host. This
|
|
|
|
can be used if a lot of output is expected in logcat and the fixed
|
|
|
|
logcat buffer on the device is not big enough. The trade off is that
|
|
|
|
this introduces some minor runtime overhead. Not set by default.
|
|
|
|
"""),
|
|
|
|
Parameter('enable_screen_check', kind=boolean, default=False,
|
|
|
|
description="""
|
|
|
|
Specified whether the device should make sure that the screen is on
|
|
|
|
during initialization.
|
|
|
|
"""),
|
2015-12-11 10:58:32 +00:00
|
|
|
Parameter('swipe_to_unlock', kind=str, default=None,
|
|
|
|
allowed_values=[None, "horizontal", "vertical"],
|
2015-10-01 09:53:21 +01:00
|
|
|
description="""
|
2015-12-11 10:58:32 +00:00
|
|
|
If set a swipe of the specified direction will be performed.
|
2015-10-01 09:53:21 +01:00
|
|
|
This should unlock the screen.
|
|
|
|
"""),
|
2015-03-10 13:09:31 +00:00
|
|
|
]
|
|
|
|
|
|
|
|
default_timeout = 30
|
|
|
|
delay = 2
|
|
|
|
long_delay = 3 * delay
|
|
|
|
ready_timeout = 60
|
|
|
|
|
|
|
|
# Overwritten from Device. For documentation, see corresponding method in
|
|
|
|
# Device.
|
|
|
|
|
|
|
|
@property
|
|
|
|
def is_rooted(self):
|
|
|
|
if self._is_rooted is None:
|
|
|
|
try:
|
|
|
|
result = adb_shell(self.adb_name, 'su', timeout=1)
|
|
|
|
if 'not found' in result:
|
|
|
|
self._is_rooted = False
|
|
|
|
else:
|
|
|
|
self._is_rooted = True
|
|
|
|
except TimeoutError:
|
|
|
|
self._is_rooted = True
|
|
|
|
except DeviceError:
|
|
|
|
self._is_rooted = False
|
|
|
|
return self._is_rooted
|
|
|
|
|
2015-06-16 15:15:18 +01:00
|
|
|
@property
|
|
|
|
def abi(self):
|
2016-12-07 16:34:15 +00:00
|
|
|
val = self.getprop()['ro.product.cpu.abi'].split('-')[0]
|
|
|
|
for abi, architectures in ABI_MAP.iteritems():
|
|
|
|
if val in architectures:
|
|
|
|
return abi
|
|
|
|
return val
|
2015-06-16 15:15:18 +01:00
|
|
|
|
2015-03-10 13:09:31 +00:00
|
|
|
@property
|
2016-12-09 13:49:39 +00:00
|
|
|
def supported_abi(self):
|
2015-03-10 13:09:31 +00:00
|
|
|
props = self.getprop()
|
|
|
|
result = [props['ro.product.cpu.abi']]
|
|
|
|
if 'ro.product.cpu.abi2' in props:
|
|
|
|
result.append(props['ro.product.cpu.abi2'])
|
|
|
|
if 'ro.product.cpu.abilist' in props:
|
2016-12-09 13:49:39 +00:00
|
|
|
for abi in props['ro.product.cpu.abilist'].split(','):
|
|
|
|
if abi not in result:
|
|
|
|
result.append(abi)
|
2016-12-07 16:34:15 +00:00
|
|
|
|
|
|
|
mapped_result = []
|
2016-12-09 13:49:39 +00:00
|
|
|
for supported_abi in result:
|
2016-12-07 16:34:15 +00:00
|
|
|
for abi, architectures in ABI_MAP.iteritems():
|
|
|
|
found = False
|
2016-12-09 13:49:39 +00:00
|
|
|
if supported_abi in architectures and abi not in mapped_result:
|
2016-12-07 16:34:15 +00:00
|
|
|
mapped_result.append(abi)
|
|
|
|
found = True
|
|
|
|
break
|
2016-12-09 13:49:39 +00:00
|
|
|
if not found and supported_abi not in mapped_result:
|
|
|
|
mapped_result.append(supported_abi)
|
2016-12-07 16:34:15 +00:00
|
|
|
return mapped_result
|
2015-03-10 13:09:31 +00:00
|
|
|
|
|
|
|
def __init__(self, **kwargs):
|
|
|
|
super(AndroidDevice, self).__init__(**kwargs)
|
|
|
|
self._logcat_poller = None
|
|
|
|
|
|
|
|
def reset(self):
|
|
|
|
self._is_ready = False
|
|
|
|
self._just_rebooted = True
|
|
|
|
adb_command(self.adb_name, 'reboot', timeout=self.default_timeout)
|
|
|
|
|
|
|
|
def hard_reset(self):
|
|
|
|
super(AndroidDevice, self).hard_reset()
|
|
|
|
self._is_ready = False
|
|
|
|
self._just_rebooted = True
|
|
|
|
|
2015-08-17 14:36:28 +01:00
|
|
|
def boot(self, hard=False, **kwargs):
|
|
|
|
if hard:
|
|
|
|
self.hard_reset()
|
|
|
|
else:
|
|
|
|
self.reset()
|
2015-03-10 13:09:31 +00:00
|
|
|
|
|
|
|
def connect(self): # NOQA pylint: disable=R0912
|
|
|
|
iteration_number = 0
|
|
|
|
max_iterations = self.ready_timeout / self.delay
|
|
|
|
available = False
|
|
|
|
self.logger.debug('Polling for device {}...'.format(self.adb_name))
|
|
|
|
while iteration_number < max_iterations:
|
|
|
|
devices = adb_list_devices()
|
|
|
|
if self.adb_name:
|
|
|
|
for device in devices:
|
|
|
|
if device.name == self.adb_name and device.status != 'offline':
|
|
|
|
available = True
|
|
|
|
else: # adb_name not set
|
|
|
|
if len(devices) == 1:
|
|
|
|
available = True
|
|
|
|
elif len(devices) > 1:
|
|
|
|
raise DeviceError('More than one device is connected and adb_name is not set.')
|
|
|
|
|
|
|
|
if available:
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
time.sleep(self.delay)
|
|
|
|
iteration_number += 1
|
|
|
|
else:
|
|
|
|
raise DeviceError('Could not boot {} ({}).'.format(self.name, self.adb_name))
|
|
|
|
|
|
|
|
while iteration_number < max_iterations:
|
2015-12-09 15:43:35 +00:00
|
|
|
available = (int('0' + (adb_shell(self.adb_name, 'getprop sys.boot_completed', timeout=self.default_timeout))) == 1)
|
2015-03-10 13:09:31 +00:00
|
|
|
if available:
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
time.sleep(self.delay)
|
|
|
|
iteration_number += 1
|
|
|
|
else:
|
|
|
|
raise DeviceError('Could not boot {} ({}).'.format(self.name, self.adb_name))
|
|
|
|
|
|
|
|
if self._just_rebooted:
|
|
|
|
self.logger.debug('Waiting for boot to complete...')
|
|
|
|
# On some devices, adb connection gets reset some time after booting.
|
|
|
|
# This causes errors during execution. To prevent this, open a shell
|
|
|
|
# session and wait for it to be killed. Once its killed, give adb
|
|
|
|
# enough time to restart, and then the device should be ready.
|
|
|
|
# TODO: This is more of a work-around rather than an actual solution.
|
|
|
|
# Need to figure out what is going on the "proper" way of handling it.
|
|
|
|
try:
|
|
|
|
adb_shell(self.adb_name, '', timeout=20)
|
|
|
|
time.sleep(5) # give adb time to re-initialize
|
|
|
|
except TimeoutError:
|
|
|
|
pass # timed out waiting for the session to be killed -- assume not going to be.
|
|
|
|
|
|
|
|
self.logger.debug('Boot completed.')
|
|
|
|
self._just_rebooted = False
|
|
|
|
self._is_ready = True
|
|
|
|
|
2015-06-17 17:41:24 +01:00
|
|
|
def initialize(self, context):
|
2016-05-09 16:10:02 +01:00
|
|
|
self.sqlite = self.deploy_sqlite3(context) # pylint: disable=attribute-defined-outside-init
|
2015-03-10 13:09:31 +00:00
|
|
|
if self.is_rooted:
|
|
|
|
self.disable_screen_lock()
|
|
|
|
self.disable_selinux()
|
|
|
|
if self.enable_screen_check:
|
|
|
|
self.ensure_screen_is_on()
|
|
|
|
|
|
|
|
def disconnect(self):
|
|
|
|
if self._logcat_poller:
|
|
|
|
self._logcat_poller.close()
|
|
|
|
|
|
|
|
def ping(self):
|
|
|
|
try:
|
|
|
|
# May be triggered inside initialize()
|
|
|
|
adb_shell(self.adb_name, 'ls /', timeout=10)
|
|
|
|
except (TimeoutError, CalledProcessError):
|
|
|
|
raise DeviceNotRespondingError(self.adb_name or self.name)
|
|
|
|
|
|
|
|
def start(self):
|
|
|
|
if self.logcat_poll_period:
|
|
|
|
if self._logcat_poller:
|
|
|
|
self._logcat_poller.close()
|
|
|
|
self._logcat_poller = _LogcatPoller(self, self.logcat_poll_period, timeout=self.default_timeout)
|
|
|
|
self._logcat_poller.start()
|
|
|
|
|
|
|
|
def stop(self):
|
|
|
|
if self._logcat_poller:
|
|
|
|
self._logcat_poller.stop()
|
|
|
|
|
|
|
|
def get_android_version(self):
|
|
|
|
return ANDROID_VERSION_MAP.get(self.get_sdk_version(), None)
|
|
|
|
|
|
|
|
def get_android_id(self):
|
|
|
|
"""
|
|
|
|
Get the device's ANDROID_ID. Which is
|
|
|
|
|
|
|
|
"A 64-bit number (as a hex string) that is randomly generated when the user
|
|
|
|
first sets up the device and should remain constant for the lifetime of the
|
|
|
|
user's device."
|
|
|
|
|
|
|
|
.. note:: This will get reset on userdata erasure.
|
|
|
|
|
|
|
|
"""
|
2015-12-08 17:49:43 +00:00
|
|
|
output = self.execute('content query --uri content://settings/secure --projection value --where "name=\'android_id\'"').strip()
|
|
|
|
return output.split('value=')[-1]
|
2015-03-10 13:09:31 +00:00
|
|
|
|
|
|
|
def get_sdk_version(self):
|
|
|
|
try:
|
|
|
|
return int(self.getprop('ro.build.version.sdk'))
|
|
|
|
except (ValueError, TypeError):
|
|
|
|
return None
|
|
|
|
|
|
|
|
def get_installed_package_version(self, package):
|
|
|
|
"""
|
|
|
|
Returns the version (versionName) of the specified package if it is installed
|
|
|
|
on the device, or ``None`` otherwise.
|
|
|
|
|
|
|
|
Added in version 2.1.4
|
|
|
|
|
|
|
|
"""
|
|
|
|
output = self.execute('dumpsys package {}'.format(package))
|
|
|
|
for line in convert_new_lines(output).split('\n'):
|
|
|
|
if 'versionName' in line:
|
|
|
|
return line.split('=', 1)[1]
|
|
|
|
return None
|
|
|
|
|
2016-12-08 15:55:18 +00:00
|
|
|
def get_installed_package_abi(self, package):
|
|
|
|
"""
|
|
|
|
Returns the primary abi of the specified package if it is installed
|
|
|
|
on the device, or ``None`` otherwise.
|
|
|
|
"""
|
|
|
|
output = self.execute('dumpsys package {}'.format(package))
|
|
|
|
val = None
|
|
|
|
for line in convert_new_lines(output).split('\n'):
|
|
|
|
if 'primaryCpuAbi' in line:
|
|
|
|
val = line.split('=', 1)[1]
|
|
|
|
break
|
|
|
|
if val == 'null':
|
|
|
|
return None
|
|
|
|
for abi, architectures in ABI_MAP.iteritems():
|
|
|
|
if val in architectures:
|
|
|
|
return abi
|
|
|
|
return val
|
|
|
|
|
2015-03-10 13:09:31 +00:00
|
|
|
def list_packages(self):
|
|
|
|
"""
|
|
|
|
List packages installed on the device.
|
|
|
|
|
|
|
|
Added in version 2.1.4
|
|
|
|
|
|
|
|
"""
|
|
|
|
output = self.execute('pm list packages')
|
|
|
|
output = output.replace('package:', '')
|
|
|
|
return output.split()
|
|
|
|
|
|
|
|
def package_is_installed(self, package_name):
|
|
|
|
"""
|
|
|
|
Returns ``True`` the if a package with the specified name is installed on
|
|
|
|
the device, and ``False`` otherwise.
|
|
|
|
|
|
|
|
Added in version 2.1.4
|
|
|
|
|
|
|
|
"""
|
|
|
|
return package_name in self.list_packages()
|
|
|
|
|
2016-01-14 16:56:57 +00:00
|
|
|
def executable_is_installed(self, executable_name): # pylint: disable=unused-argument,no-self-use
|
|
|
|
raise AttributeError("""Instead of using is_installed, please use
|
|
|
|
``get_binary_path`` or ``install_if_needed`` instead. You should
|
|
|
|
use the path returned by these functions to then invoke the binary
|
|
|
|
|
|
|
|
please see: https://pythonhosted.org/wlauto/writing_extensions.html""")
|
2015-03-10 13:09:31 +00:00
|
|
|
|
|
|
|
def is_installed(self, name):
|
2016-01-14 16:56:57 +00:00
|
|
|
if self.package_is_installed(name):
|
|
|
|
return True
|
|
|
|
elif "." in name: # assumes android packages have a . in their name and binaries documentation
|
|
|
|
return False
|
|
|
|
else:
|
|
|
|
raise AttributeError("""Instead of using is_installed, please use
|
|
|
|
``get_binary_path`` or ``install_if_needed`` instead. You should
|
|
|
|
use the path returned by these functions to then invoke the binary
|
|
|
|
|
|
|
|
please see: https://pythonhosted.org/wlauto/writing_extensions.html""")
|
2015-03-10 13:09:31 +00:00
|
|
|
|
|
|
|
def listdir(self, path, as_root=False, **kwargs):
|
|
|
|
contents = self.execute('ls {}'.format(path), as_root=as_root)
|
|
|
|
return [x.strip() for x in contents.split()]
|
|
|
|
|
|
|
|
def push_file(self, source, dest, as_root=False, timeout=default_timeout): # pylint: disable=W0221
|
|
|
|
"""
|
|
|
|
Modified in version 2.1.4: added ``as_root`` parameter.
|
|
|
|
|
|
|
|
"""
|
|
|
|
self._check_ready()
|
2015-06-01 17:25:59 +01:00
|
|
|
try:
|
|
|
|
if not as_root:
|
|
|
|
adb_command(self.adb_name, "push '{}' '{}'".format(source, dest), timeout=timeout)
|
|
|
|
else:
|
|
|
|
device_tempfile = self.path.join(self.file_transfer_cache, source.lstrip(self.path.sep))
|
|
|
|
self.execute('mkdir -p {}'.format(self.path.dirname(device_tempfile)))
|
|
|
|
adb_command(self.adb_name, "push '{}' '{}'".format(source, device_tempfile), timeout=timeout)
|
|
|
|
self.execute('cp {} {}'.format(device_tempfile, dest), as_root=True)
|
|
|
|
except CalledProcessError as e:
|
|
|
|
raise DeviceError(e)
|
2015-03-10 13:09:31 +00:00
|
|
|
|
|
|
|
def pull_file(self, source, dest, as_root=False, timeout=default_timeout): # pylint: disable=W0221
|
|
|
|
"""
|
|
|
|
Modified in version 2.1.4: added ``as_root`` parameter.
|
|
|
|
|
|
|
|
"""
|
|
|
|
self._check_ready()
|
2015-06-01 17:25:59 +01:00
|
|
|
try:
|
|
|
|
if not as_root:
|
|
|
|
adb_command(self.adb_name, "pull '{}' '{}'".format(source, dest), timeout=timeout)
|
|
|
|
else:
|
|
|
|
device_tempfile = self.path.join(self.file_transfer_cache, source.lstrip(self.path.sep))
|
|
|
|
self.execute('mkdir -p {}'.format(self.path.dirname(device_tempfile)))
|
|
|
|
self.execute('cp {} {}'.format(source, device_tempfile), as_root=True)
|
|
|
|
adb_command(self.adb_name, "pull '{}' '{}'".format(device_tempfile, dest), timeout=timeout)
|
|
|
|
except CalledProcessError as e:
|
|
|
|
raise DeviceError(e)
|
2015-03-10 13:09:31 +00:00
|
|
|
|
|
|
|
def delete_file(self, filepath, as_root=False): # pylint: disable=W0221
|
|
|
|
self._check_ready()
|
2016-08-26 17:12:24 +01:00
|
|
|
adb_shell(self.adb_name, "rm -rf '{}'".format(filepath), as_root=as_root, timeout=self.default_timeout)
|
2015-03-10 13:09:31 +00:00
|
|
|
|
|
|
|
def file_exists(self, filepath):
|
|
|
|
self._check_ready()
|
|
|
|
output = adb_shell(self.adb_name, 'if [ -e \'{}\' ]; then echo 1; else echo 0; fi'.format(filepath),
|
|
|
|
timeout=self.default_timeout)
|
2015-12-09 15:43:35 +00:00
|
|
|
return bool(int(output))
|
2015-03-10 13:09:31 +00:00
|
|
|
|
2016-06-21 14:21:03 +01:00
|
|
|
def install(self, filepath, timeout=default_timeout, with_name=None, replace=False): # pylint: disable=W0221
|
2015-03-10 13:09:31 +00:00
|
|
|
ext = os.path.splitext(filepath)[1].lower()
|
|
|
|
if ext == '.apk':
|
2016-06-21 14:21:03 +01:00
|
|
|
return self.install_apk(filepath, timeout, replace)
|
2015-03-10 13:09:31 +00:00
|
|
|
else:
|
|
|
|
return self.install_executable(filepath, with_name)
|
|
|
|
|
2016-10-10 12:55:14 +01:00
|
|
|
def install_apk(self, filepath, timeout=default_timeout, replace=False, allow_downgrade=False): # pylint: disable=W0221
|
2015-03-10 13:09:31 +00:00
|
|
|
self._check_ready()
|
|
|
|
ext = os.path.splitext(filepath)[1].lower()
|
|
|
|
if ext == '.apk':
|
2016-06-21 14:21:03 +01:00
|
|
|
flags = []
|
|
|
|
if replace:
|
2016-06-22 19:34:04 +01:00
|
|
|
flags.append('-r') # Replace existing APK
|
2016-10-10 12:55:14 +01:00
|
|
|
if allow_downgrade:
|
|
|
|
flags.append('-d') # Install the APK even if a newer version is already installed
|
2016-06-21 14:21:03 +01:00
|
|
|
if self.get_sdk_version() >= 23:
|
2016-06-22 19:34:04 +01:00
|
|
|
flags.append('-g') # Grant all runtime permissions
|
2016-06-21 14:21:03 +01:00
|
|
|
self.logger.debug("Replace APK = {}, ADB flags = '{}'".format(replace, ' '.join(flags)))
|
|
|
|
return adb_command(self.adb_name, "install {} '{}'".format(' '.join(flags), filepath), timeout=timeout)
|
2015-03-10 13:09:31 +00:00
|
|
|
else:
|
|
|
|
raise DeviceError('Can\'t install {}: unsupported format.'.format(filepath))
|
|
|
|
|
|
|
|
def install_executable(self, filepath, with_name=None):
|
|
|
|
"""
|
2016-01-14 16:56:57 +00:00
|
|
|
Installs a binary executable on device. Returns
|
2015-03-10 13:09:31 +00:00
|
|
|
the path to the installed binary, or ``None`` if the installation has failed.
|
|
|
|
Optionally, ``with_name`` parameter may be used to specify a different name under
|
|
|
|
which the executable will be installed.
|
|
|
|
|
|
|
|
Added in version 2.1.3.
|
|
|
|
Updated in version 2.1.5 with ``with_name`` parameter.
|
|
|
|
|
|
|
|
"""
|
2015-06-01 12:19:54 +01:00
|
|
|
self._ensure_binaries_directory_is_writable()
|
2015-03-10 13:09:31 +00:00
|
|
|
executable_name = with_name or os.path.basename(filepath)
|
|
|
|
on_device_file = self.path.join(self.working_directory, executable_name)
|
|
|
|
on_device_executable = self.path.join(self.binaries_directory, executable_name)
|
|
|
|
self.push_file(filepath, on_device_file)
|
2015-06-01 12:19:54 +01:00
|
|
|
self.execute('cp {} {}'.format(on_device_file, on_device_executable), as_root=self.is_rooted)
|
|
|
|
self.execute('chmod 0777 {}'.format(on_device_executable), as_root=self.is_rooted)
|
|
|
|
return on_device_executable
|
2015-03-10 13:09:31 +00:00
|
|
|
|
|
|
|
def uninstall(self, package):
|
|
|
|
self._check_ready()
|
|
|
|
adb_command(self.adb_name, "uninstall {}".format(package), timeout=self.default_timeout)
|
|
|
|
|
|
|
|
def uninstall_executable(self, executable_name):
|
|
|
|
"""
|
|
|
|
|
|
|
|
Added in version 2.1.3.
|
|
|
|
|
|
|
|
"""
|
2016-01-14 16:56:57 +00:00
|
|
|
on_device_executable = self.get_binary_path(executable_name, search_system_binaries=False)
|
|
|
|
if not on_device_executable:
|
|
|
|
raise DeviceError("Could not uninstall {}, binary not found".format(on_device_executable))
|
2015-06-01 12:19:54 +01:00
|
|
|
self._ensure_binaries_directory_is_writable()
|
|
|
|
self.delete_file(on_device_executable, as_root=self.is_rooted)
|
2015-03-10 13:09:31 +00:00
|
|
|
|
|
|
|
def execute(self, command, timeout=default_timeout, check_exit_code=True, background=False,
|
|
|
|
as_root=False, busybox=False, **kwargs):
|
|
|
|
"""
|
|
|
|
Execute the specified command on the device using adb.
|
|
|
|
|
|
|
|
Parameters:
|
|
|
|
|
|
|
|
:param command: The command to be executed. It should appear exactly
|
|
|
|
as if you were typing it into a shell.
|
|
|
|
:param timeout: Time, in seconds, to wait for adb to return before aborting
|
|
|
|
and raising an error. Defaults to ``AndroidDevice.default_timeout``.
|
|
|
|
:param check_exit_code: If ``True``, the return code of the command on the Device will
|
|
|
|
be check and exception will be raised if it is not 0.
|
|
|
|
Defaults to ``True``.
|
|
|
|
:param background: If ``True``, will execute adb in a subprocess, and will return
|
|
|
|
immediately, not waiting for adb to return. Defaults to ``False``
|
|
|
|
:param busybox: If ``True``, will use busybox to execute the command. Defaults to ``False``.
|
|
|
|
|
|
|
|
Added in version 2.1.3
|
|
|
|
|
2016-01-14 16:56:57 +00:00
|
|
|
.. note:: The device must be rooted to be able to use some busybox features.
|
2015-03-10 13:09:31 +00:00
|
|
|
|
|
|
|
:param as_root: If ``True``, will attempt to execute command in privileged mode. The device
|
|
|
|
must be rooted, otherwise an error will be raised. Defaults to ``False``.
|
|
|
|
|
|
|
|
Added in version 2.1.3
|
|
|
|
|
|
|
|
:returns: If ``background`` parameter is set to ``True``, the subprocess object will
|
|
|
|
be returned; otherwise, the contents of STDOUT from the device will be returned.
|
|
|
|
|
|
|
|
:raises: DeviceError if adb timed out or if the command returned non-zero exit
|
|
|
|
code on the device, or if attempting to execute a command in privileged mode on an
|
|
|
|
unrooted device.
|
|
|
|
|
|
|
|
"""
|
|
|
|
self._check_ready()
|
|
|
|
if as_root and not self.is_rooted:
|
|
|
|
raise DeviceError('Attempting to execute "{}" as root on unrooted device.'.format(command))
|
|
|
|
if busybox:
|
|
|
|
command = ' '.join([self.busybox, command])
|
|
|
|
if background:
|
|
|
|
return adb_background_shell(self.adb_name, command, as_root=as_root)
|
|
|
|
else:
|
|
|
|
return adb_shell(self.adb_name, command, timeout, check_exit_code, as_root)
|
|
|
|
|
2016-05-26 09:59:44 +01:00
|
|
|
def kick_off(self, command, as_root=None):
|
2015-03-10 13:09:31 +00:00
|
|
|
"""
|
|
|
|
Like execute but closes adb session and returns immediately, leaving the command running on the
|
|
|
|
device (this is different from execute(background=True) which keeps adb connection open and returns
|
|
|
|
a subprocess object).
|
|
|
|
|
|
|
|
Added in version 2.1.4
|
|
|
|
|
|
|
|
"""
|
2016-05-26 09:59:44 +01:00
|
|
|
if as_root is None:
|
|
|
|
as_root = self.is_rooted
|
2015-03-10 13:09:31 +00:00
|
|
|
try:
|
2016-05-26 09:59:44 +01:00
|
|
|
command = 'cd {} && {} nohup {}'.format(self.working_directory, self.busybox, command)
|
|
|
|
output = self.execute(command, timeout=1, as_root=as_root)
|
2015-03-10 13:09:31 +00:00
|
|
|
except TimeoutError:
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
raise ValueError('Background command exited before timeout; got "{}"'.format(output))
|
|
|
|
|
2015-04-28 13:16:48 +01:00
|
|
|
def get_pids_of(self, process_name):
|
|
|
|
"""Returns a list of PIDs of all processes with the specified name."""
|
2016-10-17 10:54:10 +01:00
|
|
|
result = (self.execute('ps | {} grep {}'.format(self.busybox, process_name),
|
|
|
|
check_exit_code=False) or '').strip()
|
2015-04-28 13:16:48 +01:00
|
|
|
if result and 'not found' not in result:
|
2016-01-13 17:07:30 +00:00
|
|
|
return [int(x.split()[1]) for x in result.split('\n')]
|
2015-04-28 13:16:48 +01:00
|
|
|
else:
|
|
|
|
return []
|
|
|
|
|
|
|
|
def ps(self, **kwargs):
|
|
|
|
"""
|
|
|
|
Returns the list of running processes on the device. Keyword arguments may
|
|
|
|
be used to specify simple filters for columns.
|
|
|
|
|
|
|
|
Added in version 2.1.4
|
|
|
|
|
|
|
|
"""
|
|
|
|
lines = iter(convert_new_lines(self.execute('ps')).split('\n'))
|
|
|
|
lines.next() # header
|
|
|
|
result = []
|
|
|
|
for line in lines:
|
|
|
|
parts = line.split()
|
|
|
|
if parts:
|
|
|
|
result.append(PsEntry(*(parts[0:1] + map(int, parts[1:5]) + parts[5:])))
|
|
|
|
if not kwargs:
|
|
|
|
return result
|
|
|
|
else:
|
|
|
|
filtered_result = []
|
|
|
|
for entry in result:
|
|
|
|
if all(getattr(entry, k) == v for k, v in kwargs.iteritems()):
|
|
|
|
filtered_result.append(entry)
|
|
|
|
return filtered_result
|
|
|
|
|
2015-03-10 13:09:31 +00:00
|
|
|
def get_properties(self, context):
|
|
|
|
"""Captures and saves the information from /system/build.prop and /proc/version"""
|
2015-06-01 16:41:33 +01:00
|
|
|
props = super(AndroidDevice, self).get_properties(context)
|
2015-11-10 13:25:41 +00:00
|
|
|
props.update(self._get_android_properties(context))
|
|
|
|
return props
|
|
|
|
|
|
|
|
def _get_android_properties(self, context):
|
|
|
|
props = {}
|
2015-03-10 13:09:31 +00:00
|
|
|
props['android_id'] = self.get_android_id()
|
2016-05-24 11:27:29 +01:00
|
|
|
self._update_build_properties(props)
|
2015-03-10 13:09:31 +00:00
|
|
|
|
2015-11-16 13:09:28 +00:00
|
|
|
dumpsys_host_file = os.path.join(context.host_working_directory, 'window.dumpsys')
|
2017-01-24 17:52:51 +00:00
|
|
|
with open(dumpsys_host_file, 'w') as wfh:
|
|
|
|
wfh.write(self.execute('dumpsys window'))
|
|
|
|
context.add_run_artifact('dumpsys_window', dumpsys_host_file, 'meta')
|
2016-05-24 11:27:29 +01:00
|
|
|
|
|
|
|
prop_file = os.path.join(context.host_working_directory, 'android-props.json')
|
|
|
|
with open(prop_file, 'w') as wfh:
|
|
|
|
json.dump(props, wfh)
|
|
|
|
context.add_run_artifact('android_properties', prop_file, 'export')
|
2015-03-10 13:09:31 +00:00
|
|
|
return props
|
|
|
|
|
|
|
|
def getprop(self, prop=None):
|
|
|
|
"""Returns parsed output of Android getprop command. If a property is
|
|
|
|
specified, only the value for that property will be returned (with
|
|
|
|
``None`` returned if the property doesn't exist. Otherwise,
|
|
|
|
``wlauto.utils.android.AndroidProperties`` will be returned, which is
|
|
|
|
a dict-like object."""
|
|
|
|
props = AndroidProperties(self.execute('getprop'))
|
|
|
|
if prop:
|
|
|
|
return props[prop]
|
|
|
|
return props
|
|
|
|
|
2016-05-09 16:10:02 +01:00
|
|
|
def deploy_sqlite3(self, context):
|
|
|
|
host_file = context.resolver.get(Executable(NO_ONE, self.abi, 'sqlite3'))
|
|
|
|
target_file = self.install_if_needed(host_file)
|
|
|
|
return target_file
|
|
|
|
|
2015-03-10 13:09:31 +00:00
|
|
|
# Android-specific methods. These either rely on specifics of adb or other
|
|
|
|
# Android-only concepts in their interface and/or implementation.
|
|
|
|
|
|
|
|
def forward_port(self, from_port, to_port):
|
|
|
|
"""
|
|
|
|
Forward a port on the device to a port on localhost.
|
|
|
|
|
|
|
|
:param from_port: Port on the device which to forward.
|
|
|
|
:param to_port: Port on the localhost to which the device port will be forwarded.
|
|
|
|
|
|
|
|
Ports should be specified using adb spec. See the "adb forward" section in "adb help".
|
|
|
|
|
|
|
|
"""
|
|
|
|
adb_command(self.adb_name, 'forward {} {}'.format(from_port, to_port), timeout=self.default_timeout)
|
|
|
|
|
|
|
|
def dump_logcat(self, outfile, filter_spec=None):
|
|
|
|
"""
|
|
|
|
Dump the contents of logcat, for the specified filter spec to the
|
|
|
|
specified output file.
|
|
|
|
See http://developer.android.com/tools/help/logcat.html
|
|
|
|
|
|
|
|
:param outfile: Output file on the host into which the contents of the
|
|
|
|
log will be written.
|
|
|
|
:param filter_spec: Logcat filter specification.
|
|
|
|
see http://developer.android.com/tools/debugging/debugging-log.html#filteringOutput
|
|
|
|
|
|
|
|
"""
|
|
|
|
if self._logcat_poller:
|
|
|
|
return self._logcat_poller.write_log(outfile)
|
|
|
|
else:
|
|
|
|
if filter_spec:
|
|
|
|
command = 'logcat -d -s {} > {}'.format(filter_spec, outfile)
|
|
|
|
else:
|
|
|
|
command = 'logcat -d > {}'.format(outfile)
|
|
|
|
return adb_command(self.adb_name, command, timeout=self.default_timeout)
|
|
|
|
|
|
|
|
def clear_logcat(self):
|
|
|
|
"""Clear (flush) logcat log."""
|
|
|
|
if self._logcat_poller:
|
|
|
|
return self._logcat_poller.clear_buffer()
|
|
|
|
else:
|
|
|
|
return adb_shell(self.adb_name, 'logcat -c', timeout=self.default_timeout)
|
|
|
|
|
2015-10-01 09:53:21 +01:00
|
|
|
def get_screen_size(self):
|
|
|
|
output = self.execute('dumpsys window')
|
|
|
|
match = SCREEN_SIZE_REGEX.search(output)
|
|
|
|
if match:
|
|
|
|
return (int(match.group('width')),
|
|
|
|
int(match.group('height')))
|
|
|
|
else:
|
|
|
|
return (0, 0)
|
|
|
|
|
2015-12-11 10:58:32 +00:00
|
|
|
def perform_unlock_swipe(self):
|
2015-10-01 09:53:21 +01:00
|
|
|
width, height = self.get_screen_size()
|
|
|
|
command = 'input swipe {} {} {} {}'
|
2015-12-11 10:58:32 +00:00
|
|
|
if self.swipe_to_unlock == "horizontal":
|
|
|
|
swipe_heigh = height * 2 // 3
|
|
|
|
start = 100
|
|
|
|
stop = width - start
|
|
|
|
self.execute(command.format(start, swipe_heigh, stop, swipe_heigh))
|
|
|
|
if self.swipe_to_unlock == "vertical":
|
|
|
|
swipe_middle = height / 2
|
|
|
|
swipe_heigh = height * 2 // 3
|
|
|
|
self.execute(command.format(swipe_middle, swipe_heigh, swipe_middle, 0))
|
|
|
|
else: # Should never reach here
|
|
|
|
raise DeviceError("Invalid swipe direction: {}".format(self.swipe_to_unlock))
|
2015-10-01 09:53:21 +01:00
|
|
|
|
2015-03-10 13:09:31 +00:00
|
|
|
def capture_screen(self, filepath):
|
|
|
|
"""Caputers the current device screen into the specified file in a PNG format."""
|
|
|
|
on_device_file = self.path.join(self.working_directory, 'screen_capture.png')
|
|
|
|
self.execute('screencap -p {}'.format(on_device_file))
|
|
|
|
self.pull_file(on_device_file, filepath)
|
|
|
|
self.delete_file(on_device_file)
|
|
|
|
|
2016-10-17 13:49:19 +01:00
|
|
|
def capture_ui_hierarchy(self, filepath):
|
2016-08-02 16:21:47 +01:00
|
|
|
"""Captures the current view hierarchy into the specified file in a XML format."""
|
|
|
|
on_device_file = self.path.join(self.working_directory, 'screen_capture.xml')
|
|
|
|
self.execute('uiautomator dump {}'.format(on_device_file))
|
|
|
|
self.pull_file(on_device_file, filepath)
|
|
|
|
self.delete_file(on_device_file)
|
|
|
|
|
|
|
|
parsed_xml = xml.dom.minidom.parse(filepath)
|
|
|
|
with open(filepath, 'w') as f:
|
|
|
|
f.write(parsed_xml.toprettyxml())
|
|
|
|
|
2015-03-10 13:09:31 +00:00
|
|
|
def is_screen_on(self):
|
|
|
|
"""Returns ``True`` if the device screen is currently on, ``False`` otherwise."""
|
|
|
|
output = self.execute('dumpsys power')
|
|
|
|
match = SCREEN_STATE_REGEX.search(output)
|
|
|
|
if match:
|
|
|
|
return boolean(match.group(1))
|
|
|
|
else:
|
|
|
|
raise DeviceError('Could not establish screen state.')
|
|
|
|
|
|
|
|
def ensure_screen_is_on(self):
|
|
|
|
if not self.is_screen_on():
|
|
|
|
self.execute('input keyevent 26')
|
2015-12-11 10:58:32 +00:00
|
|
|
if self.swipe_to_unlock:
|
|
|
|
self.perform_unlock_swipe()
|
2015-03-10 13:09:31 +00:00
|
|
|
|
|
|
|
def disable_screen_lock(self):
|
|
|
|
"""
|
|
|
|
Attempts to disable he screen lock on the device.
|
|
|
|
|
|
|
|
.. note:: This does not always work...
|
|
|
|
|
|
|
|
Added inversion 2.1.4
|
|
|
|
|
|
|
|
"""
|
|
|
|
lockdb = '/data/system/locksettings.db'
|
2015-12-15 09:50:01 +00:00
|
|
|
sqlcommand = "update locksettings set value='0' where name='screenlock.disabled';"
|
2016-05-06 13:18:32 +01:00
|
|
|
f = tempfile.NamedTemporaryFile()
|
|
|
|
try:
|
|
|
|
f.write('{} {} "{}"'.format(self.sqlite, lockdb, sqlcommand))
|
|
|
|
f.flush()
|
|
|
|
on_device_executable = self.install_executable(f.name,
|
|
|
|
with_name="disable_screen_lock")
|
|
|
|
finally:
|
|
|
|
f.close()
|
|
|
|
self.execute(on_device_executable, as_root=True)
|
2015-03-10 13:09:31 +00:00
|
|
|
|
|
|
|
def disable_selinux(self):
|
|
|
|
# This may be invoked from intialize() so we can't use execute() or the
|
|
|
|
# standard API for doing this.
|
|
|
|
api_level = int(adb_shell(self.adb_name, 'getprop ro.build.version.sdk',
|
|
|
|
timeout=self.default_timeout).strip())
|
|
|
|
# SELinux was added in Android 4.3 (API level 18). Trying to
|
|
|
|
# 'getenforce' in earlier versions will produce an error.
|
|
|
|
if api_level >= 18:
|
|
|
|
se_status = self.execute('getenforce', as_root=True).strip()
|
|
|
|
if se_status == 'Enforcing':
|
|
|
|
self.execute('setenforce 0', as_root=True)
|
|
|
|
|
2016-01-21 17:11:53 +00:00
|
|
|
def get_device_model(self):
|
|
|
|
try:
|
|
|
|
return self.getprop(prop='ro.product.device')
|
|
|
|
except KeyError:
|
|
|
|
return None
|
|
|
|
|
2016-08-31 10:18:34 +01:00
|
|
|
def broadcast_media_mounted(self, dirpath):
|
|
|
|
"""
|
|
|
|
Force a re-index of the mediaserver cache for the specified directory.
|
|
|
|
"""
|
|
|
|
command = 'am broadcast -a android.intent.action.MEDIA_MOUNTED -d file://'
|
|
|
|
self.execute(command + dirpath)
|
|
|
|
|
2015-03-10 13:09:31 +00:00
|
|
|
# Internal methods: do not use outside of the class.
|
|
|
|
|
2016-05-24 11:27:29 +01:00
|
|
|
def _update_build_properties(self, props):
|
2015-03-10 13:09:31 +00:00
|
|
|
try:
|
2016-05-24 11:27:29 +01:00
|
|
|
def strip(somestring):
|
|
|
|
return somestring.strip().replace('[', '').replace(']', '')
|
|
|
|
for line in self.execute("getprop").splitlines():
|
|
|
|
key, value = line.split(':', 1)
|
|
|
|
key = strip(key)
|
|
|
|
value = strip(value)
|
|
|
|
props[key] = value
|
2015-03-10 13:09:31 +00:00
|
|
|
except ValueError:
|
|
|
|
self.logger.warning('Could not parse build.prop.')
|
|
|
|
|
|
|
|
def _update_versions(self, filepath, props):
|
|
|
|
with open(filepath) as fh:
|
|
|
|
text = fh.read()
|
|
|
|
props['version'] = text
|
|
|
|
text = re.sub(r'#.*', '', text).strip()
|
|
|
|
match = re.search(r'^(Linux version .*?)\s*\((gcc version .*)\)$', text)
|
|
|
|
if match:
|
|
|
|
props['linux_version'] = match.group(1).strip()
|
|
|
|
props['gcc_version'] = match.group(2).strip()
|
|
|
|
else:
|
|
|
|
self.logger.warning('Could not parse version string.')
|
|
|
|
|
2015-06-01 12:19:54 +01:00
|
|
|
def _ensure_binaries_directory_is_writable(self):
|
|
|
|
matched = []
|
|
|
|
for entry in self.list_file_systems():
|
|
|
|
if self.binaries_directory.rstrip('/').startswith(entry.mount_point):
|
|
|
|
matched.append(entry)
|
|
|
|
if matched:
|
|
|
|
entry = sorted(matched, key=lambda x: len(x.mount_point))[-1]
|
|
|
|
if 'rw' not in entry.options:
|
|
|
|
self.execute('mount -o rw,remount {} {}'.format(entry.device, entry.mount_point), as_root=True)
|
|
|
|
else:
|
|
|
|
raise DeviceError('Could not find mount point for binaries directory {}'.format(self.binaries_directory))
|
|
|
|
|
2015-03-10 13:09:31 +00:00
|
|
|
|
|
|
|
class _LogcatPoller(threading.Thread):
|
|
|
|
|
|
|
|
join_timeout = 5
|
|
|
|
|
|
|
|
def __init__(self, device, period, timeout=None):
|
|
|
|
super(_LogcatPoller, self).__init__()
|
|
|
|
self.adb_device = device.adb_name
|
|
|
|
self.logger = device.logger
|
|
|
|
self.period = period
|
|
|
|
self.timeout = timeout
|
|
|
|
self.stop_signal = threading.Event()
|
|
|
|
self.lock = threading.RLock()
|
|
|
|
self.buffer_file = tempfile.mktemp()
|
|
|
|
self.last_poll = 0
|
|
|
|
self.daemon = True
|
|
|
|
self.exc = None
|
|
|
|
|
|
|
|
def run(self):
|
|
|
|
self.logger.debug('Starting logcat polling.')
|
|
|
|
try:
|
|
|
|
while True:
|
|
|
|
if self.stop_signal.is_set():
|
|
|
|
break
|
|
|
|
with self.lock:
|
|
|
|
current_time = time.time()
|
|
|
|
if (current_time - self.last_poll) >= self.period:
|
|
|
|
self._poll()
|
|
|
|
time.sleep(0.5)
|
|
|
|
except Exception: # pylint: disable=W0703
|
|
|
|
self.exc = WorkerThreadError(self.name, sys.exc_info())
|
|
|
|
self.logger.debug('Logcat polling stopped.')
|
|
|
|
|
|
|
|
def stop(self):
|
|
|
|
self.logger.debug('Stopping logcat polling.')
|
|
|
|
self.stop_signal.set()
|
|
|
|
self.join(self.join_timeout)
|
|
|
|
if self.is_alive():
|
|
|
|
self.logger.error('Could not join logcat poller thread.')
|
|
|
|
if self.exc:
|
|
|
|
raise self.exc # pylint: disable=E0702
|
|
|
|
|
|
|
|
def clear_buffer(self):
|
|
|
|
self.logger.debug('Clearing logcat buffer.')
|
|
|
|
with self.lock:
|
|
|
|
adb_shell(self.adb_device, 'logcat -c', timeout=self.timeout)
|
|
|
|
with open(self.buffer_file, 'w') as _: # NOQA
|
|
|
|
pass
|
|
|
|
|
|
|
|
def write_log(self, outfile):
|
|
|
|
self.logger.debug('Writing logbuffer to {}.'.format(outfile))
|
|
|
|
with self.lock:
|
|
|
|
self._poll()
|
|
|
|
if os.path.isfile(self.buffer_file):
|
|
|
|
shutil.copy(self.buffer_file, outfile)
|
|
|
|
else: # there was no logcat trace at this time
|
|
|
|
with open(outfile, 'w') as _: # NOQA
|
|
|
|
pass
|
|
|
|
|
|
|
|
def close(self):
|
|
|
|
self.logger.debug('Closing logcat poller.')
|
|
|
|
if os.path.isfile(self.buffer_file):
|
|
|
|
os.remove(self.buffer_file)
|
|
|
|
|
|
|
|
def _poll(self):
|
|
|
|
with self.lock:
|
|
|
|
self.last_poll = time.time()
|
|
|
|
adb_command(self.adb_device, 'logcat -d >> {}'.format(self.buffer_file), timeout=self.timeout)
|
|
|
|
adb_command(self.adb_device, 'logcat -c', timeout=self.timeout)
|
|
|
|
|
|
|
|
|
|
|
|
class BigLittleDevice(AndroidDevice): # pylint: disable=W0223
|
|
|
|
|
|
|
|
parameters = [
|
|
|
|
Parameter('scheduler', default='hmp', override=True),
|
|
|
|
]
|