aboutsummaryrefslogtreecommitdiff
path: root/catapult/devil/devil/android/device_utils.py
diff options
context:
space:
mode:
Diffstat (limited to 'catapult/devil/devil/android/device_utils.py')
-rw-r--r--catapult/devil/devil/android/device_utils.py1522
1 files changed, 989 insertions, 533 deletions
diff --git a/catapult/devil/devil/android/device_utils.py b/catapult/devil/devil/android/device_utils.py
index 6182a527..0a041edb 100644
--- a/catapult/devil/devil/android/device_utils.py
+++ b/catapult/devil/devil/android/device_utils.py
@@ -1,11 +1,7 @@
# Copyright 2014 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
-
-"""Provides a variety of device interactions based on adb.
-
-Eventually, this will be based on adb_wrapper.
-"""
+"""Provides a variety of device interactions based on adb."""
# pylint: disable=unused-argument
import calendar
@@ -42,7 +38,6 @@ from devil.android import md5sum
from devil.android.sdk import adb_wrapper
from devil.android.sdk import intent
from devil.android.sdk import keyevent
-from devil.android.sdk import split_select
from devil.android.sdk import version_codes
from devil.utils import host_utils
from devil.utils import parallelizer
@@ -64,7 +59,7 @@ _DEFAULT_TIMEOUT = 30
_DEFAULT_RETRIES = 3
# A sentinel object for default values
-# TODO(jbudorick,perezju): revisit how default values are handled by
+# TODO(jbudorick): revisit how default values are handled by
# the timeout_retry decorators.
DEFAULT = object()
@@ -73,22 +68,29 @@ DEFAULT = object()
# as_root param.
_FORCE_SU = object()
-_RECURSIVE_DIRECTORY_LIST_SCRIPT = """
- function list_subdirs() {
- for f in "$1"/* ;
+# Lists all files for the specified directories.
+# In order to minimize data transfer, prints directories as absolute paths
+# followed by files within that directory without their path.
+_FILE_LIST_SCRIPT = """
+ function list_files() {
+ for f in "$1"/{.,}*
do
- if [ -d "$f" ] ;
+ if [ "$f" == "." ] || [ "$f" == ".." ] || [ "$f" == "${1}/.*" ] \
+ || [ "$f" == "${1}/*" ]
then
- if [ "$f" == "." ] || [ "$f" == ".." ] ;
- then
- continue ;
- fi ;
- echo "$f" ;
- list_subdirs "$f" ;
- fi ;
- done ;
- } ;
- list_subdirs %s
+ continue
+ fi
+ base=${f##*/} # Get the basename for the file, dropping the path.
+ echo "$base"
+ done
+ }
+ for dir in %s
+ do
+ if [ -d "$dir" ]; then
+ echo "$dir"
+ list_files "$dir"
+ fi
+ done
"""
_RESTART_ADBD_SCRIPT = """
@@ -103,74 +105,78 @@ _RESTART_ADBD_SCRIPT = """
"""
# Not all permissions can be set.
-_PERMISSIONS_BLACKLIST_RE = re.compile('|'.join(fnmatch.translate(p) for p in [
- 'android.permission.ACCESS_LOCATION_EXTRA_COMMANDS',
- 'android.permission.ACCESS_MOCK_LOCATION',
- 'android.permission.ACCESS_NETWORK_STATE',
- 'android.permission.ACCESS_NOTIFICATION_POLICY',
- 'android.permission.ACCESS_VR_STATE',
- 'android.permission.ACCESS_WIFI_STATE',
- 'android.permission.AUTHENTICATE_ACCOUNTS',
- 'android.permission.BLUETOOTH',
- 'android.permission.BLUETOOTH_ADMIN',
- 'android.permission.BROADCAST_STICKY',
- 'android.permission.CHANGE_NETWORK_STATE',
- 'android.permission.CHANGE_WIFI_MULTICAST_STATE',
- 'android.permission.CHANGE_WIFI_STATE',
- 'android.permission.DISABLE_KEYGUARD',
- 'android.permission.DOWNLOAD_WITHOUT_NOTIFICATION',
- 'android.permission.EXPAND_STATUS_BAR',
- 'android.permission.FOREGROUND_SERVICE',
- 'android.permission.GET_PACKAGE_SIZE',
- 'android.permission.INSTALL_SHORTCUT',
- 'android.permission.INJECT_EVENTS',
- 'android.permission.INTERNET',
- 'android.permission.KILL_BACKGROUND_PROCESSES',
- 'android.permission.MANAGE_ACCOUNTS',
- 'android.permission.MODIFY_AUDIO_SETTINGS',
- 'android.permission.NFC',
- 'android.permission.READ_SYNC_SETTINGS',
- 'android.permission.READ_SYNC_STATS',
- 'android.permission.RECEIVE_BOOT_COMPLETED',
- 'android.permission.RECORD_VIDEO',
- 'android.permission.REORDER_TASKS',
- 'android.permission.REQUEST_INSTALL_PACKAGES',
- 'android.permission.RESTRICTED_VR_ACCESS',
- 'android.permission.RUN_INSTRUMENTATION',
- 'android.permission.SET_ALARM',
- 'android.permission.SET_TIME_ZONE',
- 'android.permission.SET_WALLPAPER',
- 'android.permission.SET_WALLPAPER_HINTS',
- 'android.permission.TRANSMIT_IR',
- 'android.permission.USE_CREDENTIALS',
- 'android.permission.USE_FINGERPRINT',
- 'android.permission.VIBRATE',
- 'android.permission.WAKE_LOCK',
- 'android.permission.WRITE_SYNC_SETTINGS',
- 'com.android.browser.permission.READ_HISTORY_BOOKMARKS',
- 'com.android.browser.permission.WRITE_HISTORY_BOOKMARKS',
- 'com.android.launcher.permission.INSTALL_SHORTCUT',
- 'com.chrome.permission.DEVICE_EXTRAS',
- 'com.google.android.apps.now.CURRENT_ACCOUNT_ACCESS',
- 'com.google.android.c2dm.permission.RECEIVE',
- 'com.google.android.providers.gsf.permission.READ_GSERVICES',
- 'com.google.vr.vrcore.permission.VRCORE_INTERNAL',
- 'com.sec.enterprise.knox.MDM_CONTENT_PROVIDER',
- '*.permission.C2D_MESSAGE',
- '*.permission.READ_WRITE_BOOKMARK_FOLDERS',
- '*.TOS_ACKED',
-]))
+_PERMISSIONS_DENYLIST_RE = re.compile('|'.join(
+ fnmatch.translate(p) for p in [
+ 'android.permission.ACCESS_LOCATION_EXTRA_COMMANDS',
+ 'android.permission.ACCESS_MOCK_LOCATION',
+ 'android.permission.ACCESS_NETWORK_STATE',
+ 'android.permission.ACCESS_NOTIFICATION_POLICY',
+ 'android.permission.ACCESS_VR_STATE',
+ 'android.permission.ACCESS_WIFI_STATE',
+ 'android.permission.AUTHENTICATE_ACCOUNTS',
+ 'android.permission.BLUETOOTH',
+ 'android.permission.BLUETOOTH_ADMIN',
+ 'android.permission.BROADCAST_STICKY',
+ 'android.permission.CHANGE_NETWORK_STATE',
+ 'android.permission.CHANGE_WIFI_MULTICAST_STATE',
+ 'android.permission.CHANGE_WIFI_STATE',
+ 'android.permission.DISABLE_KEYGUARD',
+ 'android.permission.DOWNLOAD_WITHOUT_NOTIFICATION',
+ 'android.permission.EXPAND_STATUS_BAR',
+ 'android.permission.FOREGROUND_SERVICE',
+ 'android.permission.GET_PACKAGE_SIZE',
+ 'android.permission.INSTALL_SHORTCUT',
+ 'android.permission.INJECT_EVENTS',
+ 'android.permission.INTERNET',
+ 'android.permission.KILL_BACKGROUND_PROCESSES',
+ 'android.permission.MANAGE_ACCOUNTS',
+ 'android.permission.MODIFY_AUDIO_SETTINGS',
+ 'android.permission.NFC',
+ 'android.permission.READ_SYNC_SETTINGS',
+ 'android.permission.READ_SYNC_STATS',
+ 'android.permission.RECEIVE_BOOT_COMPLETED',
+ 'android.permission.RECORD_VIDEO',
+ 'android.permission.REORDER_TASKS',
+ 'android.permission.REQUEST_INSTALL_PACKAGES',
+ 'android.permission.RESTRICTED_VR_ACCESS',
+ 'android.permission.RUN_INSTRUMENTATION',
+ 'android.permission.SET_ALARM',
+ 'android.permission.SET_TIME_ZONE',
+ 'android.permission.SET_WALLPAPER',
+ 'android.permission.SET_WALLPAPER_HINTS',
+ 'android.permission.TRANSMIT_IR',
+ 'android.permission.USE_CREDENTIALS',
+ 'android.permission.USE_FINGERPRINT',
+ 'android.permission.VIBRATE',
+ 'android.permission.WAKE_LOCK',
+ 'android.permission.WRITE_SYNC_SETTINGS',
+ 'com.android.browser.permission.READ_HISTORY_BOOKMARKS',
+ 'com.android.browser.permission.WRITE_HISTORY_BOOKMARKS',
+ 'com.android.launcher.permission.INSTALL_SHORTCUT',
+ 'com.chrome.permission.DEVICE_EXTRAS',
+ 'com.google.android.apps.now.CURRENT_ACCOUNT_ACCESS',
+ 'com.google.android.c2dm.permission.RECEIVE',
+ 'com.google.android.providers.gsf.permission.READ_GSERVICES',
+ 'com.google.vr.vrcore.permission.VRCORE_INTERNAL',
+ 'com.sec.enterprise.knox.MDM_CONTENT_PROVIDER',
+ '*.permission.C2D_MESSAGE',
+ '*.permission.READ_WRITE_BOOKMARK_FOLDERS',
+ '*.TOS_ACKED',
+ ]))
_SHELL_OUTPUT_SEPARATOR = '~X~'
-_PERMISSIONS_EXCEPTION_RE = re.compile(
- r'java\.lang\.\w+Exception: .*$', re.MULTILINE)
+_PERMISSIONS_EXCEPTION_RE = re.compile(r'java\.lang\.\w+Exception: .*$',
+ re.MULTILINE)
_CURRENT_FOCUS_CRASH_RE = re.compile(
r'\s*mCurrentFocus.*Application (Error|Not Responding): (\S+)}')
_GETPROP_RE = re.compile(r'\[(.*?)\]: \[(.*?)\]')
+_VERSION_CODE_SDK_RE = re.compile(
+ r'\s*versionCode=(\d+).*minSdk=(\d+).*targetSdk=(.*)\s*')
# Regex to parse the long (-l) output of 'ls' command, c.f.
# https://github.com/landley/toybox/blob/master/toys/posix/ls.c#L446
+# yapf: disable
_LONG_LS_OUTPUT_RE = re.compile(
r'(?P<st_mode>[\w-]{10})\s+' # File permissions
r'(?:(?P<st_nlink>\d+)\s+)?' # Number of links (optional)
@@ -187,44 +193,60 @@ _LONG_LS_OUTPUT_RE = re.compile(
r'(?: -> (?P<symbolic_link_to>.+))?' # Symbolic link (optional)
r'$' # End of string
)
+# yapf: enable
+
_LS_DATE_FORMAT = '%Y-%m-%d %H:%M'
_FILE_MODE_RE = re.compile(r'[dbclps-](?:[r-][w-][xSs-]){2}[r-][w-][xTt-]$')
_FILE_MODE_KIND = {
- 'd': stat.S_IFDIR, 'b': stat.S_IFBLK, 'c': stat.S_IFCHR,
- 'l': stat.S_IFLNK, 'p': stat.S_IFIFO, 's': stat.S_IFSOCK,
- '-': stat.S_IFREG}
+ 'd': stat.S_IFDIR,
+ 'b': stat.S_IFBLK,
+ 'c': stat.S_IFCHR,
+ 'l': stat.S_IFLNK,
+ 'p': stat.S_IFIFO,
+ 's': stat.S_IFSOCK,
+ '-': stat.S_IFREG
+}
_FILE_MODE_PERMS = [
- stat.S_IRUSR, stat.S_IWUSR, stat.S_IXUSR,
- stat.S_IRGRP, stat.S_IWGRP, stat.S_IXGRP,
- stat.S_IROTH, stat.S_IWOTH, stat.S_IXOTH,
+ stat.S_IRUSR,
+ stat.S_IWUSR,
+ stat.S_IXUSR,
+ stat.S_IRGRP,
+ stat.S_IWGRP,
+ stat.S_IXGRP,
+ stat.S_IROTH,
+ stat.S_IWOTH,
+ stat.S_IXOTH,
]
_FILE_MODE_SPECIAL = [
('s', stat.S_ISUID),
('s', stat.S_ISGID),
('t', stat.S_ISVTX),
]
-_PS_COLUMNS = {
- 'pid': 1,
- 'ppid': 2,
- 'name': -1
-}
-_SELINUX_MODE = {
- 'enforcing': True,
- 'permissive': False,
- 'disabled': None
-}
+_PS_COLUMNS = {'pid': 1, 'ppid': 2, 'name': -1}
+_SELINUX_MODE = {'enforcing': True, 'permissive': False, 'disabled': None}
# Some devices require different logic for checking if root is necessary
_SPECIAL_ROOT_DEVICE_LIST = [
- 'marlin', # Pixel XL
- 'sailfish', # Pixel
- 'taimen', # Pixel 2 XL
- 'vega', # Lenovo Mirage Solo
- 'walleye', # Pixel 2
- 'crosshatch', # Pixel 3 XL
- 'blueline', # Pixel 3
+ 'marlin', # Pixel XL
+ 'sailfish', # Pixel
+ 'taimen', # Pixel 2 XL
+ 'vega', # Lenovo Mirage Solo
+ 'walleye', # Pixel 2
+ 'crosshatch', # Pixel 3 XL
+ 'blueline', # Pixel 3
+ 'sargo', # Pixel 3a
+ 'bonito', # Pixel 3a XL
+ 'sdk_goog3_x86', # Crow emulator
+]
+_SPECIAL_ROOT_DEVICE_LIST += [
+ 'aosp_%s' % _d for _d in _SPECIAL_ROOT_DEVICE_LIST
+]
+
+# Somce devices are slow/timeout when using default install.
+# Devices listed here will perform no_streaming app installation.
+_NO_STREAMING_DEVICE_LIST = [
+ 'flounder', # Nexus 9
+ 'volantis', # Another product name for Nexus 9
]
-_SPECIAL_ROOT_DEVICE_LIST += ['aosp_%s' % _d for _d in
- _SPECIAL_ROOT_DEVICE_LIST]
_IMEI_RE = re.compile(r' Device ID = (.+)$')
# The following regex is used to match result parcels like:
@@ -236,8 +258,6 @@ Result: Parcel(
"""
_PARCEL_RESULT_RE = re.compile(
r'0x[0-9a-f]{8}\: (?:[0-9a-f]{8}\s+){1,4}\'(.{16})\'')
-_EBUSY_RE = re.compile(
- r'mkdir failed for ([^,]*), Device or resource busy')
# http://bit.ly/2WLZhUF added a timeout to adb wait-for-device. We sometimes
# want to wait longer than the implicit call within adb root allows.
@@ -245,8 +265,7 @@ _WAIT_FOR_DEVICE_TIMEOUT_STR = 'timeout expired while waiting for device'
_WEBVIEW_SYSUPDATE_CURRENT_PKG_RE = re.compile(
r'Current WebView package.*:.*\(([a-z.]*),')
-_WEBVIEW_SYSUPDATE_NULL_PKG_RE = re.compile(
- r'Current WebView package is null')
+_WEBVIEW_SYSUPDATE_NULL_PKG_RE = re.compile(r'Current WebView package is null')
_WEBVIEW_SYSUPDATE_FALLBACK_LOGIC_RE = re.compile(
r'Fallback logic enabled: (true|false)')
_WEBVIEW_SYSUPDATE_PACKAGE_INSTALLED_RE = re.compile(
@@ -262,8 +281,7 @@ PS_COLUMNS = ('name', 'pid', 'ppid')
ProcessInfo = collections.namedtuple('ProcessInfo', PS_COLUMNS)
-@decorators.WithExplicitTimeoutAndRetries(
- _DEFAULT_TIMEOUT, _DEFAULT_RETRIES)
+@decorators.WithExplicitTimeoutAndRetries(_DEFAULT_TIMEOUT, _DEFAULT_RETRIES)
def GetAVDs():
"""Returns a list of Android Virtual Devices.
@@ -271,9 +289,10 @@ def GetAVDs():
A list containing the configured AVDs.
"""
lines = cmd_helper.GetCmdOutput([
- os.path.join(devil_env.config.LocalPath('android_sdk'),
- 'tools', 'android'),
- 'list', 'avd']).splitlines()
+ os.path.join(
+ devil_env.config.LocalPath('android_sdk'), 'tools', 'android'),
+ 'list', 'avd'
+ ]).splitlines()
avds = []
for line in lines:
if 'Name:' not in line:
@@ -284,14 +303,14 @@ def GetAVDs():
return avds
-@decorators.WithExplicitTimeoutAndRetries(
- _DEFAULT_TIMEOUT, _DEFAULT_RETRIES)
+@decorators.WithExplicitTimeoutAndRetries(_DEFAULT_TIMEOUT, _DEFAULT_RETRIES)
def RestartServer():
"""Restarts the adb server.
Raises:
CommandFailedError if we fail to kill or restart the server.
"""
+
def adb_killed():
return not adb_wrapper.AdbWrapper.IsServerOnline()
@@ -300,7 +319,8 @@ def RestartServer():
adb_wrapper.AdbWrapper.KillServer()
if not timeout_retry.WaitFor(adb_killed, wait_period=1, max_tries=5):
- # TODO(perezju): raise an exception after fixng http://crbug.com/442319
+ # TODO(crbug.com/442319): Switch this to raise an exception if we
+ # figure out why sometimes not all adb servers on bots get killed.
logger.warning('Failed to kill adb server')
adb_wrapper.AdbWrapper.StartServer()
if not timeout_retry.WaitFor(adb_started, wait_period=1, max_tries=5):
@@ -356,12 +376,62 @@ def _FormatPartialOutputError(output):
return '\n'.join(message)
+_PushableComponents = collections.namedtuple('_PushableComponents',
+ ('host', 'device', 'collapse'))
+
+
+def _IterPushableComponents(host_path, device_path):
+ """Yields a sequence of paths that can be pushed directly via adb push.
+
+ `adb push` doesn't currently handle pushing directories that contain
+ symlinks: https://bit.ly/2pMBlW5
+
+ To circumvent this issue, we get the smallest set of files and/or
+ directories that can be pushed without attempting to push a directory
+ that contains a symlink.
+
+ This function does so by recursing through |host_path|. Each call
+ yields 3-tuples that include the smallest set of (host, device) path pairs
+ that can be passed to adb push and a bool indicating whether the parent
+ directory can be pushed -- i.e., if True, the host path is neither a
+ symlink nor a directory that contains a symlink.
+
+ Args:
+ host_path: an absolute path of a file or directory on the host
+ device_path: an absolute path of a file or directory on the device
+ Yields:
+ 3-tuples containing
+ host (str): the host path, with symlinks dereferenced
+ device (str): the device path
+ collapse (bool): whether this entity permits its parent to be pushed
+ in its entirety. (Parents need permission from all child entities
+ in order to be pushed in their entirety.)
+ """
+ if os.path.isfile(host_path):
+ yield _PushableComponents(
+ os.path.realpath(host_path), device_path, not os.path.islink(host_path))
+ else:
+ components = []
+ for child in os.listdir(host_path):
+ components.extend(
+ _IterPushableComponents(
+ os.path.join(host_path, child), posixpath.join(
+ device_path, child)))
+
+ if all(c.collapse for c in components):
+ yield _PushableComponents(
+ os.path.realpath(host_path), device_path,
+ not os.path.islink(host_path))
+ else:
+ for c in components:
+ yield c
+
+
class DeviceUtils(object):
_MAX_ADB_COMMAND_LENGTH = 512
_MAX_ADB_OUTPUT_LENGTH = 32768
- _LAUNCHER_FOCUSED_RE = re.compile(
- r'\s*mCurrentFocus.*(Launcher|launcher).*')
+ _LAUNCHER_FOCUSED_RE = re.compile(r'\s*mCurrentFocus.*(Launcher|launcher).*')
_VALID_SHELL_VARIABLE = re.compile('^[a-zA-Z_][a-zA-Z0-9_]*$')
LOCAL_PROPERTIES_PATH = posixpath.join('/', 'data', 'local.prop')
@@ -369,7 +439,9 @@ class DeviceUtils(object):
# Property in /data/local.prop that controls Java assertions.
JAVA_ASSERT_PROPERTY = 'dalvik.vm.enableassertions'
- def __init__(self, device, enable_device_files_cache=False,
+ def __init__(self,
+ device,
+ enable_device_files_cache=False,
default_timeout=_DEFAULT_TIMEOUT,
default_retries=_DEFAULT_RETRIES):
"""DeviceUtils constructor.
@@ -489,7 +561,12 @@ class DeviceUtils(object):
# 'eng' builds have root enabled by default and the adb session cannot
# be unrooted.
return True
- if self.product_name in _SPECIAL_ROOT_DEVICE_LIST:
+ # Devices using the system-as-root partition layout appear to not have
+ # a /root directory. See http://bit.ly/37F34sx for more context.
+ if (self.build_system_root_image == 'true'
+ or self.build_version_sdk >= version_codes.Q
+ # This may be redundant with the checks above.
+ or self.product_name in _SPECIAL_ROOT_DEVICE_LIST):
return self.GetProp('service.adb.root') == '1'
self.RunShellCommand(['ls', '/root'], check_return=True)
return True
@@ -515,13 +592,21 @@ class DeviceUtils(object):
"""
if 'needs_su' not in self._cache:
cmd = '%s && ! ls /root' % self._Su('ls /root')
- if self.product_name in _SPECIAL_ROOT_DEVICE_LIST:
+ # Devices using the system-as-root partition layout appear to not have
+ # a /root directory. See http://bit.ly/37F34sx for more context.
+ if (self.build_system_root_image == 'true'
+ or self.build_version_sdk >= version_codes.Q
+ # This may be redundant with the checks above.
+ or self.product_name in _SPECIAL_ROOT_DEVICE_LIST):
if self.HasRoot():
self._cache['needs_su'] = False
return False
cmd = 'which which && which su'
try:
- self.RunShellCommand(cmd, shell=True, check_return=True,
+ self.RunShellCommand(
+ cmd,
+ shell=True,
+ check_return=True,
timeout=self._default_timeout if timeout is DEFAULT else timeout,
retries=self._default_retries if retries is DEFAULT else retries)
self._cache['needs_su'] = True
@@ -529,7 +614,6 @@ class DeviceUtils(object):
self._cache['needs_su'] = False
return self._cache['needs_su']
-
def _Su(self, command):
if self.build_version_sdk >= version_codes.MARSHMALLOW:
return 'su 0 %s' % command
@@ -596,6 +680,9 @@ class DeviceUtils(object):
def GetExternalStoragePath(self, timeout=None, retries=None):
"""Get the device's path to its SD card.
+ Note: this path is read-only by apps in R+. Use GetAppWritablePath() to
+ obtain a path writable by apps.
+
Args:
timeout: timeout in seconds
retries: number of retries
@@ -614,6 +701,30 @@ class DeviceUtils(object):
str(self))
return self._cache['external_storage']
+ def GetAppWritablePath(self, timeout=None, retries=None):
+ """Get a path that on the device's SD card that apps can write.
+
+ Args:
+ timeout: timeout in seconds
+ retries: number of retries
+
+ Returns:
+ A app-writeable path on the device's SD card.
+
+ Raises:
+ CommandFailedError if the external storage path could not be determined.
+ CommandTimeoutError on timeout.
+ DeviceUnreachableError on missing device.
+ """
+ if self.build_version_sdk >= version_codes.Q:
+ # On Q+ apps don't require permissions to access well-defined media
+ # locations like /sdcard/Download. On R+ the WRITE_EXTERNAL_STORAGE
+ # permission no longer provides access to the external storage root. See
+ # https://developer.android.com/preview/privacy/storage#permissions-target-11
+ # So use /sdcard/Download for the app-writable path on those versions.
+ return posixpath.join(self.GetExternalStoragePath(), 'Download')
+ return self.GetExternalStoragePath()
+
@decorators.WithTimeoutAndRetriesFromInstance()
def GetIMEI(self, timeout=None, retries=None):
"""Get the device's IMEI.
@@ -633,7 +744,8 @@ class DeviceUtils(object):
if self.build_version_sdk < 21:
out = self.RunShellCommand(['dumpsys', 'iphonesubinfo'],
- raw_output=True, check_return=True)
+ raw_output=True,
+ check_return=True)
if out:
match = re.search(_IMEI_RE, out)
if match:
@@ -656,6 +768,23 @@ class DeviceUtils(object):
raise device_errors.CommandFailedError('Unable to fetch IMEI.')
@decorators.WithTimeoutAndRetriesFromInstance()
+ def IsApplicationInstalled(self, package, timeout=None, retries=None):
+ """Determines whether a particular package is installed on the device.
+
+ Args:
+ package: Name of the package.
+
+ Returns:
+ True if the application is installed, False otherwise.
+ """
+ # `pm list packages` allows matching substrings, but we want exact matches
+ # only.
+ matching_packages = self.RunShellCommand(
+ ['pm', 'list', 'packages', package], check_return=True)
+ desired_line = 'package:' + package
+ return desired_line in matching_packages
+
+ @decorators.WithTimeoutAndRetriesFromInstance()
def GetApplicationPaths(self, package, timeout=None, retries=None):
"""Get the paths of the installed apks on the device for the given package.
@@ -685,8 +814,8 @@ class DeviceUtils(object):
# TODO(jbudorick): Check if this is fixed as new Android versions are
# released to put an upper bound on this.
should_check_return = (self.build_version_sdk < version_codes.LOLLIPOP)
- output = self.RunShellCommand(
- ['pm', 'path', package], check_return=should_check_return)
+ output = self.RunShellCommand(['pm', 'path', package],
+ check_return=should_check_return)
apks = []
bad_output = False
for line in output:
@@ -718,8 +847,8 @@ class DeviceUtils(object):
A string with the version name or None if the package is not found
on the device.
"""
- output = self.RunShellCommand(
- ['dumpsys', 'package', package], check_return=True)
+ output = self.RunShellCommand(['dumpsys', 'package', package],
+ check_return=True)
if not output:
return None
for line in output:
@@ -730,6 +859,35 @@ class DeviceUtils(object):
'Version name for %s not found on dumpsys output' % package, str(self))
@decorators.WithTimeoutAndRetriesFromInstance()
+ def GetApplicationTargetSdk(self, package, timeout=None, retries=None):
+ """Get the targetSdkVersion of a package installed on the device.
+
+ Args:
+ package: Name of the package.
+
+ Returns:
+ A string with the targetSdkVersion or None if the package is not found on
+ the device. Note: this cannot always be cast to an integer. If this
+ application targets a pre-release SDK, this returns the version codename
+ instead (ex. "R").
+ """
+ if not self.IsApplicationInstalled(package):
+ return None
+ lines = self._GetDumpsysOutput(['package', package], 'targetSdk=')
+ for line in lines:
+ m = _VERSION_CODE_SDK_RE.match(line)
+ if m:
+ value = m.group(3)
+ # 10000 is the code used by Android for a pre-finalized SDK.
+ if value == '10000':
+ return self.GetProp('ro.build.version.codename', cache=True)
+ else:
+ return value
+ raise device_errors.CommandFailedError(
+ 'targetSdkVersion for %s not found on dumpsys output' % package,
+ str(self))
+
+ @decorators.WithTimeoutAndRetriesFromInstance()
def GetPackageArchitecture(self, package, timeout=None, retries=None):
"""Get the architecture of a package installed on the device.
@@ -758,6 +916,9 @@ class DeviceUtils(object):
CommandFailedError if the package's data directory can't be found,
whether because it's not installed or otherwise.
"""
+ if not self.IsApplicationInstalled(package):
+ raise device_errors.CommandFailedError('%s is not installed' % package,
+ str(self))
output = self._RunPipedShellCommand(
'pm dump %s | grep dataDir=' % cmd_helper.SingleQuote(package))
for line in output:
@@ -765,11 +926,14 @@ class DeviceUtils(object):
if dataDir:
return dataDir
raise device_errors.CommandFailedError(
- 'Could not find data directory for %s', package)
+ 'Could not find data directory for %s' % package, str(self))
@decorators.WithTimeoutAndRetriesFromInstance()
- def GetSecurityContextForPackage(self, package, encrypted=False, timeout=None,
- retries=None):
+ def GetSecurityContextForPackage(self,
+ package,
+ encrypted=False,
+ timeout=None,
+ retries=None):
"""Gets the SELinux security context for the given package.
Args:
@@ -782,7 +946,8 @@ class DeviceUtils(object):
"""
directory = '/data/user_de/0/' if encrypted else '/data/data/'
for line in self.RunShellCommand(['ls', '-Z', directory],
- as_root=True, check_return=True):
+ as_root=True,
+ check_return=True):
split_line = line.split()
# ls -Z output differs between Android versions, but the package is
# always last and the context always starts with "u:object"
@@ -792,7 +957,7 @@ class DeviceUtils(object):
return column
return None
- def TakeBugReport(self, path, timeout=60*5, retries=None):
+ def TakeBugReport(self, path, timeout=60 * 5, retries=None):
"""Takes a bug report and dumps it to the specified path.
This doesn't use adb's bugreport option since its behavior is dependent on
@@ -812,15 +977,23 @@ class DeviceUtils(object):
self.PullFile(device_tmp_file.name, path)
@decorators.WithTimeoutAndRetriesFromInstance()
- def WaitUntilFullyBooted(self, wifi=False, timeout=None, retries=None):
+ def WaitUntilFullyBooted(self,
+ wifi=False,
+ decrypt=False,
+ timeout=None,
+ retries=None):
"""Wait for the device to fully boot.
This means waiting for the device to boot, the package manager to be
- available, and the SD card to be ready. It can optionally mean waiting
- for wifi to come up, too.
+ available, and the SD card to be ready.
+ It can optionally wait the following:
+ - Wait for wifi to come up.
+ - Wait for full-disk decryption to complete.
Args:
wifi: A boolean indicating if we should wait for wifi to come up or not.
+ decrypt: A boolean indicating if we should wait for full-disk decryption
+ to complete.
timeout: timeout in seconds
retries: number of retries
@@ -829,10 +1002,11 @@ class DeviceUtils(object):
CommandTimeoutError if one of the component waits times out.
DeviceUnreachableError if the device becomes unresponsive.
"""
+
def sd_card_ready():
try:
- self.RunShellCommand(['test', '-d', self.GetExternalStoragePath()],
- check_return=True)
+ self.RunShellCommand(
+ ['test', '-d', self.GetExternalStoragePath()], check_return=True)
return True
except device_errors.AdbCommandFailedError:
return False
@@ -853,24 +1027,49 @@ class DeviceUtils(object):
return 'Wi-Fi is enabled' in self.RunShellCommand(['dumpsys', 'wifi'],
check_return=False)
+ def decryption_completed():
+ try:
+ decrypt = self.GetProp('vold.decrypt', cache=False)
+ # The prop "void.decrypt" will only be set when the device uses
+ # full-disk encryption (FDE).
+ # Return true when:
+ # - The prop is empty, which means the device is unencrypted or uses
+ # file-based encryption (FBE).
+ # - or the prop has value "trigger_restart_framework", which means
+ # the decription is finished.
+ return decrypt == '' or decrypt == 'trigger_restart_framework'
+ except device_errors.CommandFailedError:
+ return False
+
self.adb.WaitForDevice()
timeout_retry.WaitFor(sd_card_ready)
timeout_retry.WaitFor(pm_ready)
timeout_retry.WaitFor(boot_completed)
if wifi:
timeout_retry.WaitFor(wifi_enabled)
+ if decrypt:
+ timeout_retry.WaitFor(decryption_completed)
REBOOT_DEFAULT_TIMEOUT = 10 * _DEFAULT_TIMEOUT
@decorators.WithTimeoutAndRetriesFromInstance(
min_default_timeout=REBOOT_DEFAULT_TIMEOUT)
- def Reboot(self, block=True, wifi=False, timeout=None, retries=None):
+ def Reboot(self,
+ block=True,
+ wifi=False,
+ decrypt=False,
+ timeout=None,
+ retries=None):
"""Reboot the device.
Args:
block: A boolean indicating if we should wait for the reboot to complete.
wifi: A boolean indicating if we should wait for wifi to be enabled after
- the reboot. The option has no effect unless |block| is also True.
+ the reboot.
+ The option has no effect unless |block| is also True.
+ decrypt: A boolean indicating if we should wait for full-disk decryption
+ to complete after the reboot.
+ The option has no effect unless |block| is also True.
timeout: timeout in seconds
retries: number of retries
@@ -878,6 +1077,7 @@ class DeviceUtils(object):
CommandTimeoutError on timeout.
DeviceUnreachableError on missing device.
"""
+
def device_offline():
return not self.IsOnline()
@@ -885,14 +1085,23 @@ class DeviceUtils(object):
self.ClearCache()
timeout_retry.WaitFor(device_offline, wait_period=1)
if block:
- self.WaitUntilFullyBooted(wifi=wifi)
+ self.WaitUntilFullyBooted(wifi=wifi, decrypt=decrypt)
INSTALL_DEFAULT_TIMEOUT = 8 * _DEFAULT_TIMEOUT
+ MODULES_SRC_DIRECTORY_PATH = '/data/local/tmp/modules'
@decorators.WithTimeoutAndRetriesFromInstance(
min_default_timeout=INSTALL_DEFAULT_TIMEOUT)
- def Install(self, apk, allow_downgrade=False, reinstall=False,
- permissions=None, timeout=None, retries=None, modules=None):
+ def Install(self,
+ apk,
+ allow_downgrade=False,
+ reinstall=False,
+ permissions=None,
+ timeout=None,
+ retries=None,
+ modules=None,
+ fake_modules=None,
+ additional_locales=None):
"""Install an APK or app bundle.
Noop if an identical APK is already installed. If installing a bundle, the
@@ -911,21 +1120,93 @@ class DeviceUtils(object):
retries: number of retries
modules: An iterable containing specific bundle modules to install.
Error if set and |apk| points to an APK instead of a bundle.
+ fake_modules: An iterable containing specific bundle modules that should
+ have their apks copied to |MODULES_SRC_DIRECTORY_PATH| subdirectory
+ rather than installed. Thus the app can emulate SplitCompat while
+ running. This should not have any overlap with |modules|.
+ additional_locales: An iterable with additional locales to install for a
+ bundle.
Raises:
CommandFailedError if the installation fails.
CommandTimeoutError if the installation times out.
DeviceUnreachableError on missing device.
"""
- self._InstallInternal(apk, None, allow_downgrade=allow_downgrade,
- reinstall=reinstall, permissions=permissions,
- modules=modules)
+ apk = apk_helper.ToHelper(apk)
+ modules_set = set(modules or [])
+ fake_modules_set = set(fake_modules or [])
+ assert modules_set.isdisjoint(fake_modules_set), (
+ 'These modules overlap: %s' % (modules_set & fake_modules_set))
+ all_modules = modules_set | fake_modules_set
+ package_name = apk.GetPackageName()
+
+ with apk.GetApkPaths(self,
+ modules=all_modules,
+ additional_locales=additional_locales) as apk_paths:
+ if apk.SupportsSplits():
+ fake_apk_paths = self._GetFakeInstallPaths(apk_paths, fake_modules)
+ self._FakeInstall(fake_apk_paths, fake_modules, package_name)
+ apk_paths_to_install = [p for p in apk_paths if p not in fake_apk_paths]
+ else:
+ apk_paths_to_install = apk_paths
+ self._InstallInternal(
+ apk,
+ apk_paths_to_install,
+ allow_downgrade=allow_downgrade,
+ reinstall=reinstall,
+ permissions=permissions)
+
+ @staticmethod
+ def _GetFakeInstallPaths(apk_paths, fake_modules):
+ def IsFakeModulePath(path):
+ filename = os.path.basename(path)
+ return any(filename.startswith(f + '-') for f in fake_modules)
+
+ if not fake_modules:
+ return set()
+ return set(p for p in apk_paths if IsFakeModulePath(p))
+
+ def _FakeInstall(self, fake_apk_paths, fake_modules, package_name):
+ with tempfile_ext.NamedTemporaryDirectory() as modules_dir:
+ device_dir = posixpath.join(self.MODULES_SRC_DIRECTORY_PATH, package_name)
+ if not fake_modules:
+ # Push empty module dir to clear device dir and update the cache.
+ self.PushChangedFiles([(modules_dir, device_dir)],
+ delete_device_stale=True)
+ return
+
+ still_need_master = set(fake_modules)
+ for path in fake_apk_paths:
+ filename = os.path.basename(path)
+ # Example names: base-en.apk, test_dummy-master.apk.
+ module_name, suffix = filename.split('-', 1)
+ if 'master' in suffix:
+ assert module_name in still_need_master, (
+ 'Duplicate master apk file for %s' % module_name)
+ still_need_master.remove(module_name)
+ new_filename = '%s.apk' % module_name
+ else:
+ # |suffix| includes .apk extension.
+ new_filename = '%s.config.%s' % (module_name, suffix)
+ new_path = os.path.join(modules_dir, new_filename)
+ os.rename(path, new_path)
+
+ assert not still_need_master, (
+ 'Missing master apk file for %s' % still_need_master)
+ self.PushChangedFiles([(modules_dir, device_dir)],
+ delete_device_stale=True)
@decorators.WithTimeoutAndRetriesFromInstance(
min_default_timeout=INSTALL_DEFAULT_TIMEOUT)
- def InstallSplitApk(self, base_apk, split_apks, allow_downgrade=False,
- reinstall=False, allow_cached_props=False,
- permissions=None, timeout=None, retries=None):
+ def InstallSplitApk(self,
+ base_apk,
+ split_apks,
+ allow_downgrade=False,
+ reinstall=False,
+ allow_cached_props=False,
+ permissions=None,
+ timeout=None,
+ retries=None):
"""Install a split APK.
Noop if all of the APK splits are already installed.
@@ -948,73 +1229,57 @@ class DeviceUtils(object):
DeviceUnreachableError on missing device.
DeviceVersionError if device SDK is less than Android L.
"""
- self._InstallInternal(base_apk, split_apks, reinstall=reinstall,
- allow_cached_props=allow_cached_props,
- permissions=permissions,
- allow_downgrade=allow_downgrade)
-
- def _InstallInternal(self, base_apk, split_apks, allow_downgrade=False,
- reinstall=False, allow_cached_props=False,
- permissions=None, modules=None):
- base_apk = apk_helper.ToHelper(base_apk)
- if base_apk.is_bundle:
- if split_apks:
- raise device_errors.CommandFailedError(
- 'Attempted to install a bundle {} while specifying split apks'
- .format(base_apk))
- if allow_downgrade:
- logging.warning('Installation of a bundle requested with '
- 'allow_downgrade=False. This is not possible with '
- 'bundletools, no downgrading is possible. This '
- 'flag will be ignored and installation will proceed.')
- # |allow_cached_props| is unused and ignored for bundles.
- self._InstallBundleInternal(base_apk, permissions, modules)
- return
-
- if modules:
- raise device_errors.CommandFailedError(
- 'Attempted to specify modules to install when providing an APK')
-
- if split_apks:
+ apk = apk_helper.ToSplitHelper(base_apk, split_apks)
+ with apk.GetApkPaths(
+ self, allow_cached_props=allow_cached_props) as apk_paths:
+ self._InstallInternal(
+ apk,
+ apk_paths,
+ reinstall=reinstall,
+ permissions=permissions,
+ allow_downgrade=allow_downgrade)
+
+ def _InstallInternal(self,
+ apk,
+ apk_paths,
+ allow_downgrade=False,
+ reinstall=False,
+ permissions=None):
+ if not apk_paths:
+ raise device_errors.CommandFailedError('Did not get any APKs to install')
+
+ if len(apk_paths) > 1:
self._CheckSdkLevel(version_codes.LOLLIPOP)
- all_apks = [base_apk.path]
- if split_apks:
- all_apks += split_select.SelectSplits(
- self, base_apk.path, split_apks, allow_cached_props=allow_cached_props)
- if len(all_apks) == 1:
- logger.warning('split-select did not select any from %s', split_apks)
-
- missing_apks = [apk for apk in all_apks if not os.path.exists(apk)]
+ missing_apks = [a for a in apk_paths if not os.path.exists(a)]
if missing_apks:
raise device_errors.CommandFailedError(
- 'Attempted to install non-existent apks: %s'
- % pprint.pformat(missing_apks))
+ 'Attempted to install non-existent apks: %s' %
+ pprint.pformat(missing_apks))
- package_name = base_apk.GetPackageName()
+ package_name = apk.GetPackageName()
device_apk_paths = self._GetApplicationPathsInternal(package_name)
- apks_to_install = None
host_checksums = None
if not device_apk_paths:
- apks_to_install = all_apks
- elif len(device_apk_paths) > 1 and not split_apks:
+ apks_to_install = apk_paths
+ elif len(device_apk_paths) > 1 and len(apk_paths) == 1:
logger.warning(
'Installing non-split APK when split APK was previously installed')
- apks_to_install = all_apks
- elif len(device_apk_paths) == 1 and split_apks:
+ apks_to_install = apk_paths
+ elif len(device_apk_paths) == 1 and len(apk_paths) > 1:
logger.warning(
'Installing split APK when non-split APK was previously installed')
- apks_to_install = all_apks
+ apks_to_install = apk_paths
else:
try:
- apks_to_install, host_checksums = (
- self._ComputeStaleApks(package_name, all_apks))
- except EnvironmentError as e:
+ apks_to_install, host_checksums = (self._ComputeStaleApks(
+ package_name, apk_paths))
+ except device_errors.CommandFailedError as e:
logger.warning('Error calculating md5: %s', e)
- apks_to_install, host_checksums = all_apks, None
+ apks_to_install, host_checksums = apk_paths, None
if apks_to_install and not reinstall:
- apks_to_install = all_apks
+ apks_to_install = apk_paths
if device_apk_paths and apks_to_install and not reinstall:
self.Uninstall(package_name)
@@ -1023,14 +1288,23 @@ class DeviceUtils(object):
# Assume that we won't know the resulting device state.
self._cache['package_apk_paths'].pop(package_name, 0)
self._cache['package_apk_checksums'].pop(package_name, 0)
- if split_apks:
- partial = package_name if len(apks_to_install) < len(all_apks) else None
+ partial = package_name if len(apks_to_install) < len(apk_paths) else None
+ streaming = None
+ if self.product_name in _NO_STREAMING_DEVICE_LIST:
+ streaming = False
+ if len(apks_to_install) > 1 or partial:
self.adb.InstallMultiple(
- apks_to_install, partial=partial, reinstall=reinstall,
+ apks_to_install,
+ partial=partial,
+ reinstall=reinstall,
+ streaming=streaming,
allow_downgrade=allow_downgrade)
else:
self.adb.Install(
- base_apk.path, reinstall=reinstall, allow_downgrade=allow_downgrade)
+ apks_to_install[0],
+ reinstall=reinstall,
+ streaming=streaming,
+ allow_downgrade=allow_downgrade)
else:
# Running adb install terminates running instances of the app, so to be
# consistent, we explicitly terminate it when skipping the install.
@@ -1038,26 +1312,12 @@ class DeviceUtils(object):
if (permissions is None
and self.build_version_sdk >= version_codes.MARSHMALLOW):
- permissions = base_apk.GetPermissions()
+ permissions = apk.GetPermissions()
self.GrantPermissions(package_name, permissions)
# Upon success, we know the device checksums, but not their paths.
if host_checksums is not None:
self._cache['package_apk_checksums'][package_name] = host_checksums
- def _InstallBundleInternal(self, bundle, permissions, modules):
- cmd = [bundle.path, 'install', '--device', self.serial]
- if modules:
- for m in modules:
- cmd.extend(['-m', m])
- status = cmd_helper.RunCmd(cmd)
- if status != 0:
- raise device_errors.CommandFailedError('Cound not install {}'.format(
- bundle.path))
- if (permissions is None
- and self.build_version_sdk >= version_codes.MARSHMALLOW):
- permissions = bundle.GetPermissions()
- self.GrantPermissions(bundle.GetPackageName(), permissions)
-
@decorators.WithTimeoutAndRetriesFromInstance()
def Uninstall(self, package_name, keep_data=False, timeout=None,
retries=None):
@@ -1092,12 +1352,21 @@ class DeviceUtils(object):
raise device_errors.DeviceVersionError(
('Requires SDK level %s, device is SDK level %s' %
(required_sdk_level, self.build_version_sdk)),
- device_serial=self.serial)
+ device_serial=self.serial)
@decorators.WithTimeoutAndRetriesFromInstance()
- def RunShellCommand(self, cmd, shell=False, check_return=False, cwd=None,
- env=None, run_as=None, as_root=False, single_line=False,
- large_output=False, raw_output=False, timeout=None,
+ def RunShellCommand(self,
+ cmd,
+ shell=False,
+ check_return=False,
+ cwd=None,
+ env=None,
+ run_as=None,
+ as_root=False,
+ single_line=False,
+ large_output=False,
+ raw_output=False,
+ timeout=None,
retries=None):
"""Run an ADB shell command.
@@ -1120,8 +1389,8 @@ class DeviceUtils(object):
This behaviour is consistent with that of command runners in cmd_helper as
well as Python's own subprocess.Popen.
- TODO(perezju) Change the default of |check_return| to True when callers
- have switched to the new behaviour.
+ TODO(crbug.com/1029769) Change the default of |check_return| to True when
+ callers have switched to the new behaviour.
Args:
cmd: A sequence containing the command to run and its arguments, or a
@@ -1157,6 +1426,7 @@ class DeviceUtils(object):
CommandTimeoutError on timeout.
DeviceUnreachableError on missing device.
"""
+
def env_quote(key, value):
if not DeviceUtils._VALID_SHELL_VARIABLE.match(key):
raise KeyError('Invalid shell variable name %r' % key)
@@ -1181,8 +1451,8 @@ class DeviceUtils(object):
else:
with device_temp_file.DeviceTempFile(self.adb, suffix='.sh') as script:
self._WriteFileWithPush(script.name, cmd)
- logger.info('Large shell command will be run from file: %s ...',
- cmd[:self._MAX_ADB_COMMAND_LENGTH])
+ logger.debug('Large shell command will be run from file: %s ...',
+ cmd[:self._MAX_ADB_COMMAND_LENGTH])
return handle_check_return('sh %s' % script.name_quoted)
def handle_large_output(cmd, large_output_mode):
@@ -1213,6 +1483,7 @@ class DeviceUtils(object):
if isinstance(cmd, basestring):
if not shell:
+ # TODO(crbug.com/1029769): Make this an error instead.
logger.warning(
'The command to run should preferably be passed as a sequence of'
' args. If shell features are needed (pipes, wildcards, variables)'
@@ -1259,22 +1530,27 @@ class DeviceUtils(object):
if not pipestatus_line.startswith(PIPESTATUS_LEADER):
logger.error('Pipe exit statuses of shell script missing.')
raise device_errors.AdbShellCommandFailedError(
- script, output, status=None,
- device_serial=self.serial)
+ script, output, status=None, device_serial=self.serial)
output = output[:-1]
statuses = [
- int(s) for s in pipestatus_line[len(PIPESTATUS_LEADER):].split()]
+ int(s) for s in pipestatus_line[len(PIPESTATUS_LEADER):].split()
+ ]
if any(statuses):
raise device_errors.AdbShellCommandFailedError(
- script, output, status=statuses,
- device_serial=self.serial)
+ script, output, status=statuses, device_serial=self.serial)
return output
@decorators.WithTimeoutAndRetriesFromInstance()
- def KillAll(self, process_name, exact=False, signum=device_signal.SIGKILL,
- as_root=False, blocking=False, quiet=False,
- timeout=None, retries=None):
+ def KillAll(self,
+ process_name,
+ exact=False,
+ signum=device_signal.SIGKILL,
+ as_root=False,
+ blocking=False,
+ quiet=False,
+ timeout=None,
+ retries=None):
"""Kill all processes with the given name on the device.
Args:
@@ -1312,8 +1588,8 @@ class DeviceUtils(object):
'No processes matching %r (exact=%r)' % (process_name, exact),
str(self))
- logger.info(
- 'KillAll(%r, ...) attempting to kill the following:', process_name)
+ logger.info('KillAll(%r, ...) attempting to kill the following:',
+ process_name)
for p in processes:
logger.info(' %05d %s', p.pid, p.name)
@@ -1331,8 +1607,13 @@ class DeviceUtils(object):
return len(pids)
@decorators.WithTimeoutAndRetriesFromInstance()
- def StartActivity(self, intent_obj, blocking=False, trace_file_name=None,
- force_stop=False, timeout=None, retries=None):
+ def StartActivity(self,
+ intent_obj,
+ blocking=False,
+ trace_file_name=None,
+ force_stop=False,
+ timeout=None,
+ retries=None):
"""Start package's activity on the device.
Args:
@@ -1392,8 +1673,13 @@ class DeviceUtils(object):
raise device_errors.CommandFailedError(line, str(self))
@decorators.WithTimeoutAndRetriesFromInstance()
- def StartInstrumentation(self, component, finish=True, raw=False,
- extras=None, timeout=None, retries=None):
+ def StartInstrumentation(self,
+ component,
+ finish=True,
+ raw=False,
+ extras=None,
+ timeout=None,
+ retries=None):
if extras is None:
extras = {}
@@ -1411,8 +1697,8 @@ class DeviceUtils(object):
package = component.split('/')[0]
shell_snippet = 'p=%s;%s' % (package,
cmd_helper.ShrinkToSnippet(cmd, 'p', package))
- return self.RunShellCommand(shell_snippet, shell=True, check_return=True,
- large_output=True)
+ return self.RunShellCommand(
+ shell_snippet, shell=True, check_return=True, large_output=True)
@decorators.WithTimeoutAndRetriesFromInstance()
def BroadcastIntent(self, intent_obj, timeout=None, retries=None):
@@ -1445,9 +1731,11 @@ class DeviceUtils(object):
CommandTimeoutError on timeout.
DeviceUnreachableError on missing device.
"""
+
def is_launcher_focused():
output = self.RunShellCommand(['dumpsys', 'window', 'windows'],
- check_return=True, large_output=True)
+ check_return=True,
+ large_output=True)
return any(self._LAUNCHER_FOCUSED_RE.match(l) for l in output)
def dismiss_popups():
@@ -1462,8 +1750,9 @@ class DeviceUtils(object):
return
self.StartActivity(
- intent.Intent(action='android.intent.action.MAIN',
- category='android.intent.category.HOME'),
+ intent.Intent(
+ action='android.intent.action.MAIN',
+ category='android.intent.category.HOME'),
blocking=True)
if not is_launcher_focused():
@@ -1486,8 +1775,11 @@ class DeviceUtils(object):
self.RunShellCommand(['am', 'force-stop', package], check_return=True)
@decorators.WithTimeoutAndRetriesFromInstance()
- def ClearApplicationState(
- self, package, permissions=None, timeout=None, retries=None):
+ def ClearApplicationState(self,
+ package,
+ permissions=None,
+ timeout=None,
+ retries=None):
"""Clear all state for the given package.
Args:
@@ -1523,15 +1815,18 @@ class DeviceUtils(object):
CommandTimeoutError on timeout.
DeviceUnreachableError on missing device.
"""
- self.RunShellCommand(['input', 'keyevent', format(keycode, 'd')],
- check_return=True)
+ self.RunShellCommand(
+ ['input', 'keyevent', format(keycode, 'd')], check_return=True)
PUSH_CHANGED_FILES_DEFAULT_TIMEOUT = 10 * _DEFAULT_TIMEOUT
@decorators.WithTimeoutAndRetriesFromInstance(
min_default_timeout=PUSH_CHANGED_FILES_DEFAULT_TIMEOUT)
- def PushChangedFiles(self, host_device_tuples, timeout=None,
- retries=None, delete_device_stale=False):
+ def PushChangedFiles(self,
+ host_device_tuples,
+ delete_device_stale=False,
+ timeout=None,
+ retries=None):
"""Push files to the device, skipping files that don't need updating.
When a directory is pushed, it is traversed recursively on the host and
@@ -1544,194 +1839,208 @@ class DeviceUtils(object):
|host_path| is an absolute path of a file or directory on the host
that should be minimially pushed to the device, and |device_path| is
an absolute path of the destination on the device.
+ delete_device_stale: option to delete stale files on device
timeout: timeout in seconds
retries: number of retries
- delete_device_stale: option to delete stale files on device
Raises:
CommandFailedError on failure.
CommandTimeoutError on timeout.
DeviceUnreachableError on missing device.
"""
+ # TODO(crbug.com/1005504): Experiment with this on physical devices after
+ # upgrading devil's default adb beyond 1.0.39.
+ # TODO(crbug.com/1020716): disabled as can result in extra directory.
+ enable_push_sync = False
- all_changed_files = []
- all_stale_files = []
- missing_dirs = set()
- cache_commit_funcs = []
- for h, d in host_device_tuples:
- assert os.path.isabs(h) and posixpath.isabs(d)
- h = os.path.realpath(h)
- changed_files, up_to_date_files, stale_files, cache_commit_func = (
- self._GetChangedAndStaleFiles(h, d, delete_device_stale))
- all_changed_files += changed_files
- all_stale_files += stale_files
- cache_commit_funcs.append(cache_commit_func)
- if changed_files and not up_to_date_files and not stale_files:
- if os.path.isdir(h):
- missing_dirs.add(d)
- else:
- missing_dirs.add(posixpath.dirname(d))
+ if enable_push_sync:
+ try:
+ self._PushChangedFilesSync(host_device_tuples)
+ return
+ except device_errors.AdbVersionError as e:
+ # If we don't meet the adb requirements, fall back to the previous
+ # sync-unaware implementation.
+ logging.warning(str(e))
- if delete_device_stale and all_stale_files:
- self.RemovePath(all_stale_files, force=True, recursive=True)
+ changed_files, missing_dirs, cache_commit_func = (self._GetChangedFiles(
+ host_device_tuples, delete_device_stale))
- if all_changed_files:
+ if changed_files:
if missing_dirs:
- try:
- self.RunShellCommand(['mkdir', '-p'] + list(missing_dirs),
- check_return=True)
- except device_errors.AdbShellCommandFailedError as e:
- # TODO(crbug.com/739899): This is attempting to diagnose flaky EBUSY
- # errors that have been popping up in single-device scenarios.
- # Remove it once we've figured out what's causing them and how best
- # to handle them.
- m = _EBUSY_RE.search(e.output)
- if m:
- logging.error(
- 'Hit EBUSY while attempting to make missing directories.')
- logging.error('lsof output:')
- # Don't check for return below since grep exits with a non-zero when
- # no match is found.
- for l in self.RunShellCommand(
- 'lsof | grep %s' % cmd_helper.SingleQuote(m.group(1)),
- check_return=False):
- logging.error(' %s', l)
- raise
- self._PushFilesImpl(host_device_tuples, all_changed_files)
- for func in cache_commit_funcs:
- func()
+ self.RunShellCommand(['mkdir', '-p'] + list(missing_dirs),
+ check_return=True)
+ self._PushFilesImpl(host_device_tuples, changed_files)
+ cache_commit_func()
+
+ def _PushChangedFilesSync(self, host_device_tuples):
+ """Push changed files via `adb sync`.
+
+ Args:
+ host_device_tuples: Same as PushChangedFiles.
+ """
+ for h, d in host_device_tuples:
+ for ph, pd, _ in _IterPushableComponents(h, d):
+ self.adb.Push(ph, pd, sync=True)
+
- def _GetChangedAndStaleFiles(self, host_path, device_path, track_stale=False):
- """Get files to push and delete
+ def _GetDeviceNodes(self, paths):
+ """Get the set of all files and directories on the device contained within
+ the provided list of paths, without recursively expanding directories.
Args:
- host_path: an absolute path of a file or directory on the host
- device_path: an absolute path of a file or directory on the device
- track_stale: whether to bother looking for stale files (slower)
+ paths: The list of paths for which to list files and directories.
Returns:
- a four-element tuple
+ a set containing all files and directories contained within |paths| on the
+ device.
+ """
+ nodes = set()
+ paths = [p.replace(' ', r'\ ') for p in paths]
+ command = _FILE_LIST_SCRIPT % ' '.join(paths)
+ current_path = ""
+ # We use shell=True to evaluate the command as a script through the shell,
+ # otherwise RunShellCommand tries to interpret it as the name of a (non
+ # existent) command to run.
+ for line in self.RunShellCommand(command, shell=True, check_return=True):
+ # If the line is an absolute path it's a directory, otherwise it's a file
+ # within the most recent directory.
+ if posixpath.isabs(line):
+ current_path = line + '/'
+ else:
+ line = current_path + line
+ nodes.add(line)
+
+ return nodes
+
+ def _GetChangedFiles(self, host_device_tuples, delete_stale=False):
+ """Get files to push and delete.
+
+ Args:
+ host_device_tuples: a list of (host_files_path, device_files_path) tuples
+ to find changed files from
+ delete_stale: Whether to delete stale files
+
+ Returns:
+ a three-element tuple
1st element: a list of (host_files_path, device_files_path) tuples to push
- 2nd element: a list of host_files_path that are up-to-date
- 3rd element: a list of stale files under device_path, or [] when
- track_stale == False
- 4th element: a cache commit function.
+ 2nd element: a list of missing device directories to mkdir
+ 3rd element: a cache commit function
"""
- try:
- # Length calculations below assume no trailing /.
- host_path = host_path.rstrip('/')
- device_path = device_path.rstrip('/')
-
- specific_device_paths = [device_path]
- ignore_other_files = not track_stale and os.path.isdir(host_path)
- if ignore_other_files:
- specific_device_paths = []
+ # The fully expanded list of host/device tuples of files to push.
+ file_tuples = []
+ # All directories we're pushing files to.
+ device_dirs_to_push_to = set()
+ # All files and directories we expect to have on the device after pushing
+ # files.
+ expected_device_nodes = set()
+
+ for h, d in host_device_tuples:
+ assert os.path.isabs(h) and posixpath.isabs(d)
+ h = os.path.realpath(h)
+ host_path = h.rstrip('/')
+ device_dir = d.rstrip('/')
+
+ expected_device_nodes.add(device_dir)
+
+ # Add all parent directories to the directories we expect to have so we
+ # don't delete empty nested directories.
+ parent = posixpath.dirname(device_dir)
+ while parent and parent != '/':
+ expected_device_nodes.add(parent)
+ parent = posixpath.dirname(parent)
+
+ if os.path.isdir(host_path):
+ device_dirs_to_push_to.add(device_dir)
for root, _, filenames in os.walk(host_path):
- relative_dir = root[len(host_path) + 1:]
- specific_device_paths.extend(
- posixpath.join(device_path, relative_dir, f) for f in filenames)
+ # ignore hidden directories
+ if os.path.sep + '.' in root:
+ continue
+ relative_dir = os.path.relpath(root, host_path).rstrip('.')
+ device_path = posixpath.join(device_dir, relative_dir).rstrip('/')
+ expected_device_nodes.add(device_path)
+ device_dirs_to_push_to.add(device_path)
+ files = (
+ [posixpath.join(device_dir, relative_dir, f) for f in filenames])
+ expected_device_nodes.update(files)
+ file_tuples.extend(zip(
+ (os.path.join(root, f) for f in filenames), files))
+ else:
+ device_dirs_to_push_to.add(posixpath.dirname(device_dir))
+ file_tuples.append((host_path, device_dir))
- def calculate_host_checksums():
- return md5sum.CalculateHostMd5Sums([host_path])
+ if file_tuples or delete_stale:
+ current_device_nodes = self._GetDeviceNodes(device_dirs_to_push_to)
+ nodes_to_delete = current_device_nodes - expected_device_nodes
- def calculate_device_checksums():
- if self._enable_device_files_cache:
- cache_entry = self._cache['device_path_checksums'].get(device_path)
- if cache_entry and cache_entry[0] == ignore_other_files:
- return dict(cache_entry[1])
+ missing_dirs = device_dirs_to_push_to - current_device_nodes
- sums = md5sum.CalculateDeviceMd5Sums(specific_device_paths, self)
+ if not file_tuples:
+ if delete_stale and nodes_to_delete:
+ self.RemovePath(nodes_to_delete, force=True, recursive=True)
+ return (host_device_tuples, missing_dirs, lambda: 0)
- cache_entry = [ignore_other_files, sums]
- self._cache['device_path_checksums'][device_path] = cache_entry
- return dict(sums)
+ possibly_stale_device_nodes = current_device_nodes - nodes_to_delete
+ possibly_stale_tuples = (
+ [t for t in file_tuples if t[1] in possibly_stale_device_nodes])
- host_checksums, device_checksums = reraiser_thread.RunAsync((
- calculate_host_checksums,
- calculate_device_checksums))
- except EnvironmentError as e:
- logger.warning('Error calculating md5: %s', e)
- return ([(host_path, device_path)], [], [], lambda: 0)
-
- to_push = []
- up_to_date = []
- to_delete = []
- if os.path.isfile(host_path):
- host_checksum = host_checksums.get(host_path)
- device_checksum = device_checksums.get(device_path)
- if host_checksum == device_checksum:
- up_to_date.append(host_path)
+ def calculate_host_checksums():
+ # Need to compute all checksums when caching.
+ if self._enable_device_files_cache:
+ return md5sum.CalculateHostMd5Sums([t[0] for t in file_tuples])
else:
- to_push.append((host_path, device_path))
- else:
- for host_abs_path, host_checksum in host_checksums.iteritems():
- device_abs_path = posixpath.join(
- device_path, os.path.relpath(host_abs_path, host_path))
- device_checksum = device_checksums.pop(device_abs_path, None)
- if device_checksum == host_checksum:
- up_to_date.append(host_abs_path)
- else:
- to_push.append((host_abs_path, device_abs_path))
- to_delete = device_checksums.keys()
- # We can't rely solely on the checksum approach since it does not catch
- # stale directories, which can result in empty directories that cause issues
- # during copying in efficient_android_directory_copy.sh. So, find any stale
- # directories here so they can be removed in addition to stale files.
- if track_stale:
- to_delete.extend(self._GetStaleDirectories(host_path, device_path))
+ return md5sum.CalculateHostMd5Sums(
+ [t[0] for t in possibly_stale_tuples])
- def cache_commit_func():
- # When host_path is a not a directory, the path.join() call below would
- # have an '' as the second argument, causing an unwanted / to be appended.
- if os.path.isfile(host_path):
- assert len(host_checksums) == 1
- new_sums = {device_path: host_checksums[host_path]}
+ def calculate_device_checksums():
+ paths = set([t[1] for t in possibly_stale_tuples])
+ if not paths:
+ return dict()
+ sums = dict()
+ if self._enable_device_files_cache:
+ paths_not_in_cache = set()
+ for path in paths:
+ cache_entry = self._cache['device_path_checksums'].get(path)
+ if cache_entry:
+ sums[path] = cache_entry
+ else:
+ paths_not_in_cache.add(path)
+ paths = paths_not_in_cache
+ sums.update(dict(md5sum.CalculateDeviceMd5Sums(paths, self)))
+ if self._enable_device_files_cache:
+ for path, checksum in sums.iteritems():
+ self._cache['device_path_checksums'][path] = checksum
+ return sums
+ try:
+ host_checksums, device_checksums = reraiser_thread.RunAsync(
+ (calculate_host_checksums, calculate_device_checksums))
+ except device_errors.CommandFailedError as e:
+ logger.warning('Error calculating md5: %s', e)
+ return (host_device_tuples, set(), lambda: 0)
+
+ up_to_date = set()
+
+ for host_path, device_path in possibly_stale_tuples:
+ device_checksum = device_checksums.get(device_path, None)
+ host_checksum = host_checksums.get(host_path, None)
+ if device_checksum == host_checksum and device_checksum is not None:
+ up_to_date.add(device_path)
else:
- new_sums = {posixpath.join(device_path, path[len(host_path) + 1:]): val
- for path, val in host_checksums.iteritems()}
- cache_entry = [ignore_other_files, new_sums]
- self._cache['device_path_checksums'][device_path] = cache_entry
+ nodes_to_delete.add(device_path)
- return (to_push, up_to_date, to_delete, cache_commit_func)
+ if delete_stale and nodes_to_delete:
+ self.RemovePath(nodes_to_delete, force=True, recursive=True)
- def _GetStaleDirectories(self, host_path, device_path):
- """Gets a list of stale directories on the device.
+ to_push = (
+ [t for t in file_tuples if t[1] not in up_to_date])
- Args:
- host_path: an absolute path of a directory on the host
- device_path: an absolute path of a directory on the device
+ def cache_commit_func():
+ if not self._enable_device_files_cache:
+ return
+ for host_path, device_path in file_tuples:
+ host_checksum = host_checksums.get(host_path, None)
+ self._cache['device_path_checksums'][device_path] = host_checksum
- Returns:
- A list containing absolute paths to directories on the device that are
- considered stale.
- """
- def get_device_dirs(path):
- directories = set()
- command = _RECURSIVE_DIRECTORY_LIST_SCRIPT % cmd_helper.SingleQuote(path)
- # We use shell=True to evaluate the command as a script through the shell,
- # otherwise RunShellCommand tries to interpret it as the name of a (non
- # existent) command to run.
- for line in self.RunShellCommand(
- command, shell=True, check_return=True):
- directories.add(posixpath.relpath(posixpath.normpath(line), path))
- return directories
-
- def get_host_dirs(path):
- directories = set()
- if not os.path.isdir(path):
- return directories
- for root, _, _ in os.walk(path):
- if root != path:
- # Strip off the top level directory so we can compare the device and
- # host.
- directories.add(
- os.path.relpath(root, path).replace(os.sep, posixpath.sep))
- return directories
-
- host_dirs = get_host_dirs(host_path)
- device_dirs = get_device_dirs(device_path)
- stale_dirs = device_dirs - host_dirs
- return [posixpath.join(device_path, d) for d in stale_dirs]
+ return (to_push, missing_dirs, cache_commit_func)
def _ComputeDeviceChecksumsForApks(self, package_name):
ret = self._cache['package_apk_checksums'].get(package_name)
@@ -1749,10 +2058,11 @@ class DeviceUtils(object):
def calculate_device_checksums():
return self._ComputeDeviceChecksumsForApks(package_name)
- host_checksums, device_checksums = reraiser_thread.RunAsync((
- calculate_host_checksums, calculate_device_checksums))
- stale_apks = [k for (k, v) in host_checksums.iteritems()
- if v not in device_checksums]
+ host_checksums, device_checksums = reraiser_thread.RunAsync(
+ (calculate_host_checksums, calculate_device_checksums))
+ stale_apks = [
+ k for (k, v) in host_checksums.iteritems() if v not in device_checksums
+ ]
return stale_apks, set(host_checksums.values())
def _PushFilesImpl(self, host_device_tuples, files):
@@ -1761,8 +2071,8 @@ class DeviceUtils(object):
size = sum(host_utils.GetRecursiveDiskUsage(h) for h, _ in files)
file_count = len(files)
- dir_size = sum(host_utils.GetRecursiveDiskUsage(h)
- for h, _ in host_device_tuples)
+ dir_size = sum(
+ host_utils.GetRecursiveDiskUsage(h) for h, _ in host_device_tuples)
dir_file_count = 0
for h, _ in host_device_tuples:
if os.path.isdir(h):
@@ -1770,8 +2080,8 @@ class DeviceUtils(object):
else:
dir_file_count += 1
- push_duration = self._ApproximateDuration(
- file_count, file_count, size, False)
+ push_duration = self._ApproximateDuration(file_count, file_count, size,
+ False)
dir_push_duration = self._ApproximateDuration(
len(host_device_tuples), dir_file_count, dir_size, False)
zip_duration = self._ApproximateDuration(1, 1, size, True)
@@ -1786,8 +2096,8 @@ class DeviceUtils(object):
elif self._commands_installed is False:
# Already tried and failed to install unzip command.
self._PushChangedFilesIndividually(files)
- elif not self._PushChangedFilesZipped(
- files, [d for _, d in host_device_tuples]):
+ elif not self._PushChangedFilesZipped(files,
+ [d for _, d in host_device_tuples]):
self._PushChangedFilesIndividually(files)
def _MaybeInstallCommands(self):
@@ -1859,13 +2169,15 @@ class DeviceUtils(object):
quoted_dirs = ' '.join(cmd_helper.SingleQuote(d) for d in dirs)
self.RunShellCommand(
'unzip %s&&chmod -R 777 %s' % (device_temp.name, quoted_dirs),
- shell=True, as_root=True,
+ shell=True,
+ as_root=True,
env={'PATH': '%s:$PATH' % install_commands.BIN_DIR},
check_return=True)
return True
- # TODO(nednguyen): remove this and migrate the callsite to PathExists().
+ # TODO(crbug.com/1111556): remove this and migrate the callsite to
+ # PathExists().
@decorators.WithTimeoutAndRetriesFromInstance()
def FileExists(self, device_path, timeout=None, retries=None):
"""Checks whether the given file exists on the device.
@@ -1895,22 +2207,32 @@ class DeviceUtils(object):
"""
paths = device_paths
if isinstance(paths, basestring):
- paths = (paths,)
+ paths = (paths, )
if not paths:
return True
cmd = ['test', '-e', paths[0]]
for p in paths[1:]:
cmd.extend(['-a', '-e', p])
try:
- self.RunShellCommand(cmd, as_root=as_root, check_return=True,
- timeout=timeout, retries=retries)
+ self.RunShellCommand(
+ cmd,
+ as_root=as_root,
+ check_return=True,
+ timeout=timeout,
+ retries=retries)
return True
except device_errors.CommandFailedError:
return False
@decorators.WithTimeoutAndRetriesFromInstance()
- def RemovePath(self, device_path, force=False, recursive=False,
- as_root=False, rename=False, timeout=None, retries=None):
+ def RemovePath(self,
+ device_path,
+ force=False,
+ recursive=False,
+ as_root=False,
+ rename=False,
+ timeout=None,
+ retries=None):
"""Removes the given path(s) from the device.
Args:
@@ -1925,16 +2247,19 @@ class DeviceUtils(object):
timeout: timeout in seconds
retries: number of retries
"""
+
def _RenamePath(path):
- random_suffix = hex(random.randint(2 ** 12, 2 ** 16 - 1))[2:]
+ random_suffix = hex(random.randint(2**12, 2**16 - 1))[2:]
dest = '%s-%s' % (path, random_suffix)
try:
- self.RunShellCommand(
- ['mv', path, dest], as_root=as_root, check_return=True)
+ self.RunShellCommand(['mv', path, dest],
+ as_root=as_root,
+ check_return=True)
return dest
except device_errors.AdbShellCommandFailedError:
# If it couldn't be moved, just try rm'ing the original path instead.
return path
+
args = ['rm']
if force:
args.append('-f')
@@ -1968,7 +2293,11 @@ class DeviceUtils(object):
yield device_temp
@decorators.WithTimeoutAndRetriesFromInstance()
- def PullFile(self, device_path, host_path, as_root=False, timeout=None,
+ def PullFile(self,
+ device_path,
+ host_path,
+ as_root=False,
+ timeout=None,
retries=None):
"""Pull a file from the device.
@@ -2010,8 +2339,12 @@ class DeviceUtils(object):
shutil.rmtree(d)
@decorators.WithTimeoutAndRetriesFromInstance()
- def ReadFile(self, device_path, as_root=False, force_pull=False,
- timeout=None, retries=None):
+ def ReadFile(self,
+ device_path,
+ as_root=False,
+ force_pull=False,
+ timeout=None,
+ retries=None):
"""Reads the contents of a file from the device.
Args:
@@ -2035,16 +2368,22 @@ class DeviceUtils(object):
CommandTimeoutError on timeout.
DeviceUnreachableError on missing device.
"""
+
def get_size(path):
return self.FileSize(path, as_root=as_root)
- if (not force_pull
- and 0 < get_size(device_path) <= self._MAX_ADB_OUTPUT_LENGTH):
- return _JoinLines(self.RunShellCommand(
- ['cat', device_path], as_root=as_root, check_return=True))
- elif as_root and self.NeedsSU():
- with self._CopyToReadableLocation(device_path) as readable_temp_file:
- return self._ReadFileWithPull(readable_temp_file.name)
+ # Reading by pulling is faster than first getting the file size and cat-ing,
+ # so only read by cat when we need root.
+ if as_root and self.NeedsSU():
+ if (not force_pull
+ and 0 < get_size(device_path) <= self._MAX_ADB_OUTPUT_LENGTH):
+ return _JoinLines(
+ self.RunShellCommand(['cat', device_path],
+ as_root=as_root,
+ check_return=True))
+ else:
+ with self._CopyToReadableLocation(device_path) as readable_temp_file:
+ return self._ReadFileWithPull(readable_temp_file.name)
else:
return self._ReadFileWithPull(device_path)
@@ -2055,8 +2394,13 @@ class DeviceUtils(object):
self.adb.Push(host_temp.name, device_path)
@decorators.WithTimeoutAndRetriesFromInstance()
- def WriteFile(self, device_path, contents, as_root=False, force_push=False,
- timeout=None, retries=None):
+ def WriteFile(self,
+ device_path,
+ contents,
+ as_root=False,
+ force_push=False,
+ timeout=None,
+ retries=None):
"""Writes |contents| to a file on the device.
Args:
@@ -2091,7 +2435,8 @@ class DeviceUtils(object):
# destination files might be on different file systems (e.g.
# on internal storage and an external sd card).
self.RunShellCommand(['cp', device_temp.name, device_path],
- as_root=True, check_return=True)
+ as_root=True,
+ check_return=True)
else:
# If root is not needed, we can push directly to the desired location.
self._WriteFileWithPush(device_path, contents)
@@ -2099,11 +2444,13 @@ class DeviceUtils(object):
def _ParseLongLsOutput(self, device_path, as_root=False, **kwargs):
"""Run and scrape the output of 'ls -a -l' on a device directory."""
device_path = posixpath.join(device_path, '') # Force trailing '/'.
- output = self.RunShellCommand(
- ['ls', '-a', '-l', device_path], as_root=as_root,
- check_return=True, env={'TZ': 'utc'}, **kwargs)
+ output = self.RunShellCommand(['ls', '-a', '-l', device_path],
+ as_root=as_root,
+ check_return=True,
+ env={'TZ': 'utc'},
+ **kwargs)
if output and output[0].startswith('total '):
- output.pop(0) # pylint: disable=maybe-no-member
+ output.pop(0) # pylint: disable=maybe-no-member
entries = []
for line in output:
@@ -2278,6 +2625,7 @@ class DeviceUtils(object):
Raises:
CommandTimeoutError on timeout.
"""
+
def find_property(lines, property_name):
for index, line in enumerate(lines):
if line.strip() == '':
@@ -2360,18 +2708,21 @@ class DeviceUtils(object):
def screen_density(self):
"""Returns the screen density of the device."""
DPI_TO_DENSITY = {
- 120: 'ldpi',
- 160: 'mdpi',
- 240: 'hdpi',
- 320: 'xhdpi',
- 480: 'xxhdpi',
- 640: 'xxxhdpi',
+ 120: 'ldpi',
+ 160: 'mdpi',
+ 240: 'hdpi',
+ 320: 'xhdpi',
+ 480: 'xxhdpi',
+ 640: 'xxxhdpi',
}
return DPI_TO_DENSITY.get(self.pixel_density, 'tvdpi')
@property
def pixel_density(self):
- return int(self.GetProp('ro.sf.lcd_density', cache=True))
+ density = self.GetProp('ro.sf.lcd_density', cache=True)
+ if not density and self.adb.is_emulator:
+ density = self.GetProp('qemu.sf.lcd_density', cache=True)
+ return int(density)
@property
def build_description(self):
@@ -2402,6 +2753,15 @@ class DeviceUtils(object):
return self.GetProp('ro.build.product', cache=True)
@property
+ def build_system_root_image(self):
+ """Returns the system_root_image property.
+
+ This seems to indicate whether the device is using a system-as-root
+ partition layout. See http://bit.ly/37F34sx for more info.
+ """
+ return self.GetProp('ro.build.system_root_image', cache=True)
+
+ @property
def build_type(self):
"""Returns the build type of the system (e.g. 'user')."""
return self.GetProp('ro.build.type', cache=True)
@@ -2426,6 +2786,11 @@ class DeviceUtils(object):
'Invalid build version sdk: %r' % value)
@property
+ def tracing_path(self):
+ """Returns the tracing path of the device for atrace."""
+ return self.GetTracingPath()
+
+ @property
def product_cpu_abi(self):
"""Returns the product cpu abi of the device (e.g. 'armeabi-v7a').
@@ -2435,6 +2800,11 @@ class DeviceUtils(object):
return self.GetProp('ro.product.cpu.abi', cache=True)
@property
+ def product_cpu_abis(self):
+ """Returns all product cpu abi of the device."""
+ return self.GetProp('ro.product.cpu.abilist', cache=True).split(',')
+
+ @property
def product_model(self):
"""Returns the name of the product model (e.g. 'Nexus 7')."""
return self.GetProp('ro.product.model', cache=True)
@@ -2459,13 +2829,10 @@ class DeviceUtils(object):
# Change the token every time to ensure that it will match only the
# previously dumped cache.
token = str(uuid.uuid1())
- cmd = (
- 'c=/data/local/tmp/cache_token;'
- 'echo $EXTERNAL_STORAGE;'
- 'cat $c 2>/dev/null||echo;'
- 'echo "%s">$c &&' % token +
- 'getprop'
- )
+ cmd = ('c=/data/local/tmp/cache_token;'
+ 'echo $EXTERNAL_STORAGE;'
+ 'cat $c 2>/dev/null||echo;'
+ 'echo "%s">$c &&' % token + 'getprop')
output = self.RunShellCommand(
cmd, shell=True, check_return=True, large_output=True)
# Error-checking for this existing is done in GetExternalStoragePath().
@@ -2480,6 +2847,39 @@ class DeviceUtils(object):
self._cache['token'] = token
@decorators.WithTimeoutAndRetriesFromInstance()
+ def GetTracingPath(self, timeout=None, retries=None):
+ """Gets tracing path from the device.
+
+ Args:
+ timeout: timeout in seconds
+ retries: number of retries
+
+ Returns:
+ /sys/kernel/debug/tracing for device with debugfs mount support;
+ /sys/kernel/tracing for device with tracefs support;
+ /sys/kernel/debug/tracing if support can't be determined.
+
+ Raises:
+ CommandTimeoutError on timeout.
+ """
+ tracing_path = self._cache['tracing_path']
+ if tracing_path:
+ return tracing_path
+ with self._cache_lock:
+ tracing_path = '/sys/kernel/debug/tracing'
+ try:
+ lines = self.RunShellCommand(['mount'],
+ check_return=True,
+ timeout=timeout,
+ retries=retries)
+ if not any('debugfs' in line for line in lines):
+ tracing_path = '/sys/kernel/tracing'
+ except device_errors.AdbCommandFailedError:
+ pass
+ self._cache['tracing_path'] = tracing_path
+ return tracing_path
+
+ @decorators.WithTimeoutAndRetriesFromInstance()
def GetProp(self, property_name, cache=False, timeout=None, retries=None):
"""Gets a property from the device.
@@ -2496,8 +2896,9 @@ class DeviceUtils(object):
Raises:
CommandTimeoutError on timeout.
"""
- assert isinstance(property_name, basestring), (
- "property_name is not a string: %r" % property_name)
+ assert isinstance(
+ property_name,
+ basestring), ("property_name is not a string: %r" % property_name)
if cache:
# It takes ~120ms to query a single property, and ~130ms to query all
@@ -2506,15 +2907,21 @@ class DeviceUtils(object):
else:
# timeout and retries are handled down at run shell, because we don't
# want to apply them in the other branch when reading from the cache
- value = self.RunShellCommand(
- ['getprop', property_name], single_line=True, check_return=True,
- timeout=timeout, retries=retries)
+ value = self.RunShellCommand(['getprop', property_name],
+ single_line=True,
+ check_return=True,
+ timeout=timeout,
+ retries=retries)
self._cache['getprop'][property_name] = value
# Non-existent properties are treated as empty strings by getprop.
return self._cache['getprop'].get(property_name, '')
@decorators.WithTimeoutAndRetriesFromInstance()
- def SetProp(self, property_name, value, check=False, timeout=None,
+ def SetProp(self,
+ property_name,
+ value,
+ check=False,
+ timeout=None,
retries=None):
"""Sets a property on the device.
@@ -2533,20 +2940,21 @@ class DeviceUtils(object):
set on the device (e.g. because it is not rooted).
CommandTimeoutError on timeout.
"""
- assert isinstance(property_name, basestring), (
- "property_name is not a string: %r" % property_name)
+ assert isinstance(
+ property_name,
+ basestring), ("property_name is not a string: %r" % property_name)
assert isinstance(value, basestring), "value is not a string: %r" % value
self.RunShellCommand(['setprop', property_name, value], check_return=True)
prop_cache = self._cache['getprop']
if property_name in prop_cache:
del prop_cache[property_name]
- # TODO(perezju) remove the option and make the check mandatory, but using a
- # single shell script to both set- and getprop.
+ # TODO(crbug.com/1029772) remove the option and make the check mandatory,
+ # but using a single shell script to both set- and getprop.
if check and value != self.GetProp(property_name, cache=False):
raise device_errors.CommandFailedError(
- 'Unable to set property %r on the device to %r'
- % (property_name, value), str(self))
+ 'Unable to set property %r on the device to %r' % (property_name,
+ value), str(self))
@decorators.WithTimeoutAndRetriesFromInstance()
def GetABI(self, timeout=None, retries=None):
@@ -2565,6 +2973,12 @@ class DeviceUtils(object):
"""
return self.GetProp('ro.product.cpu.abi', cache=True)
+ @decorators.WithTimeoutAndRetriesFromInstance()
+ def GetFeatures(self, timeout=None, retries=None):
+ """Returns the features supported on the device."""
+ lines = self.RunShellCommand(['pm', 'list', 'features'], check_return=True)
+ return [f[8:] for f in lines if f.startswith('feature:')]
+
def _GetPsOutput(self, pattern):
"""Runs |ps| command on the device and returns its output,
@@ -2677,8 +3091,11 @@ class DeviceUtils(object):
return procs_pids
@decorators.WithTimeoutAndRetriesFromInstance()
- def GetApplicationPids(self, process_name, at_most_one=False,
- timeout=None, retries=None):
+ def GetApplicationPids(self,
+ process_name,
+ at_most_one=False,
+ timeout=None,
+ retries=None):
"""Returns the PID or PIDs of a given process name.
Note that the |process_name|, often the package name, must match exactly.
@@ -2700,13 +3117,15 @@ class DeviceUtils(object):
CommandTimeoutError on timeout.
DeviceUnreachableError on missing device.
"""
- pids = [p.pid for p in self.ListProcesses(process_name)
- if p.name == process_name]
+ pids = [
+ p.pid for p in self.ListProcesses(process_name)
+ if p.name == process_name
+ ]
if at_most_one:
if len(pids) > 1:
raise device_errors.CommandFailedError(
- 'Expected a single PID for %r but found: %r.' % (
- process_name, pids),
+ 'Expected a single PID for %r but found: %r.' % (process_name,
+ pids),
device_serial=str(self))
return pids[0] if pids else None
else:
@@ -2728,8 +3147,9 @@ class DeviceUtils(object):
CommandTimeoutError on timeout.
DeviceUnreachableError on missing device.
"""
- output = self.RunShellCommand(
- ['getenforce'], check_return=True, single_line=True).lower()
+ output = self.RunShellCommand(['getenforce'],
+ check_return=True,
+ single_line=True).lower()
if output not in _SELINUX_MODE:
raise device_errors.CommandFailedError(
'Unexpected getenforce output: %s' % output)
@@ -2750,9 +3170,9 @@ class DeviceUtils(object):
CommandTimeoutError on timeout.
DeviceUnreachableError on missing device.
"""
- self.RunShellCommand(
- ['setenforce', '1' if int(enabled) else '0'], as_root=True,
- check_return=True)
+ self.RunShellCommand(['setenforce', '1' if int(enabled) else '0'],
+ as_root=True,
+ check_return=True)
@decorators.WithTimeoutAndRetriesFromInstance()
def GetWebViewUpdateServiceDump(self, timeout=None, retries=None):
@@ -2780,8 +3200,8 @@ class DeviceUtils(object):
if self.build_version_sdk < version_codes.OREO:
return result
- output = self.RunShellCommand(
- ['dumpsys', 'webviewupdate'], check_return=True)
+ output = self.RunShellCommand(['dumpsys', 'webviewupdate'],
+ check_return=True)
webview_packages = {}
for line in output:
match = re.search(_WEBVIEW_SYSUPDATE_CURRENT_PKG_RE, line)
@@ -2832,8 +3252,7 @@ class DeviceUtils(object):
CommandTimeoutError on timeout.
DeviceUnreachableError on missing device.
"""
- installed = self.GetApplicationPaths(package_name)
- if not installed:
+ if not self.IsApplicationInstalled(package_name):
raise device_errors.CommandFailedError(
'%s is not installed' % package_name, str(self))
output = self.RunShellCommand(
@@ -2861,9 +3280,20 @@ class DeviceUtils(object):
'%s does not declare a WebView native library, so it cannot '
'be a WebView provider' % package_name, str(self))
if re.search(r'SDK version too low', reason):
- raise device_errors.CommandFailedError(
- '%s needs a higher targetSdkVersion (must be >= %d)' %
- (package_name, self.build_version_sdk), str(self))
+ app_target_sdk_version = self.GetApplicationTargetSdk(package_name)
+ is_preview_sdk = self.GetProp('ro.build.version.preview_sdk') == '1'
+ if is_preview_sdk:
+ codename = self.GetProp('ro.build.version.codename')
+ raise device_errors.CommandFailedError(
+ '%s targets a finalized SDK (%r), but valid WebView providers '
+ 'must target a pre-finalized SDK (%r) on this device' %
+ (package_name, app_target_sdk_version, codename), str(self))
+ else:
+ raise device_errors.CommandFailedError(
+ '%s has targetSdkVersion %r, but valid WebView providers must '
+ 'target >= %r on this device' %
+ (package_name, app_target_sdk_version, self.build_version_sdk),
+ str(self))
if re.search(r'Version code too low', reason):
raise device_errors.CommandFailedError(
'%s needs a higher versionCode (must be >= %d)' %
@@ -2920,8 +3350,10 @@ class DeviceUtils(object):
# redundant-packages is the opposite of fallback logic
enable_string = 'disable' if enabled else 'enable'
output = self.RunShellCommand(
- ['cmd', 'webviewupdate', '%s-redundant-packages' % enable_string],
- single_line=True, check_return=True)
+ ['cmd', 'webviewupdate',
+ '%s-redundant-packages' % enable_string],
+ single_line=True,
+ check_return=True)
if output == 'Success':
logging.info('WebView Fallback Logic is %s',
'enabled' if enabled else 'disabled')
@@ -2949,8 +3381,8 @@ class DeviceUtils(object):
DeviceUnreachableError on missing device.
"""
if not host_path:
- host_path = os.path.abspath('screenshot-%s-%s.png' % (
- self.serial, _GetTimeStamp()))
+ host_path = os.path.abspath(
+ 'screenshot-%s-%s.png' % (self.serial, _GetTimeStamp()))
with device_temp_file.DeviceTempFile(self.adb, suffix='.png') as device_tmp:
self.RunShellCommand(['/system/bin/screencap', '-p', device_tmp.name],
check_return=True)
@@ -2964,13 +3396,15 @@ class DeviceUtils(object):
Returns: Name of the crashed package if a dialog is focused,
None otherwise.
"""
+
def _FindFocusedWindow():
match = None
# TODO(jbudorick): Try to grep the output on the device instead of using
# large_output if/when DeviceUtils exposes a public interface for piped
# shell command handling.
for line in self.RunShellCommand(['dumpsys', 'window', 'windows'],
- check_return=True, large_output=True):
+ check_return=True,
+ large_output=True):
match = re.match(_CURRENT_FOCUS_CRASH_RE, line)
if match:
break
@@ -3017,13 +3451,15 @@ class DeviceUtils(object):
'package_apk_checksums': {},
# Map of property_name -> value
'getprop': {},
- # Map of device_path -> [ignore_other_files, map of path->checksum]
+ # Map of device path -> checksum]
'device_path_checksums': {},
# Location of sdcard ($EXTERNAL_STORAGE).
'external_storage': None,
# Token used to detect when LoadCacheData is stale.
'token': None,
'prev_token': None,
+ # Path for tracing.
+ 'tracing_path': None,
}
@decorators.WithTimeoutAndRetriesFromInstance()
@@ -3042,7 +3478,11 @@ class DeviceUtils(object):
Returns:
Whether the cache was loaded.
"""
- obj = json.loads(data)
+ try:
+ obj = json.loads(data)
+ except ValueError:
+ logger.error('Unable to parse cache file. Not using it.')
+ return False
self._EnsureCacheInitialized()
given_token = obj.get('token')
if not given_token or self._cache['prev_token'] != given_token:
@@ -3106,17 +3546,25 @@ class DeviceUtils(object):
return parallelizer.SyncParallelizer(devices)
@classmethod
- def HealthyDevices(cls, blacklist=None, device_arg='default', retries=1,
- enable_usb_resets=False, abis=None, **kwargs):
+ def HealthyDevices(cls,
+ denylist=None,
+ device_arg='default',
+ retries=1,
+ enable_usb_resets=False,
+ abis=None,
+ # TODO(crbug.com/1097306): Remove this once clients have
+ # stopped passing it.
+ blacklist=None,
+ **kwargs):
"""Returns a list of DeviceUtils instances.
- Returns a list of DeviceUtils instances that are attached, not blacklisted,
+ Returns a list of DeviceUtils instances that are attached, not denylisted,
and optionally filtered by --device flags or ANDROID_SERIAL environment
variable.
Args:
- blacklist: A DeviceBlacklist instance (optional). Device serials in this
- blacklist will never be returned, but a warning will be logged if they
+ denylist: A DeviceDenylist instance (optional). Device serials in this
+ denylist will never be returned, but a warning will be logged if they
otherwise would have been.
device_arg: The value of the --device flag. This can be:
'default' -> Same as [], but returns an empty list rather than raise a
@@ -3126,9 +3574,9 @@ class DeviceUtils(object):
attached device. Raises an exception if multiple devices are
attached.
'serial' -> Returns an instance for the given serial, if not
- blacklisted.
+ denylisted.
['A', 'B', ...] -> Returns instances for the subset that is not
- blacklisted.
+ denylisted.
retries: Number of times to restart adb server and query it again if no
devices are found on the previous attempts, with exponential backoffs
up to 60s between each retry.
@@ -3143,7 +3591,7 @@ class DeviceUtils(object):
A list of DeviceUtils instances.
Raises:
- NoDevicesError: Raised when no non-blacklisted devices exist and
+ NoDevicesError: Raised when no non-denylisted devices exist and
device_arg is passed.
MultipleDevicesError: Raise when multiple devices exist, but |device_arg|
is None.
@@ -3157,18 +3605,22 @@ class DeviceUtils(object):
if not (isinstance(device_arg, tuple) or isinstance(device_arg, list)):
select_multiple = False
if device_arg:
- device_arg = (device_arg,)
+ device_arg = (device_arg, )
+
+ # TODO(crbug.com/1097306): Remove this once clients have switched.
+ if blacklist and not denylist:
+ denylist = blacklist
- blacklisted_devices = blacklist.Read() if blacklist else []
+ denylisted_devices = denylist.Read() if denylist else []
# adb looks for ANDROID_SERIAL, so support it as well.
android_serial = os.environ.get('ANDROID_SERIAL')
if not device_arg and android_serial:
- device_arg = (android_serial,)
+ device_arg = (android_serial, )
- def blacklisted(serial):
- if serial in blacklisted_devices:
- logger.warning('Device %s is blacklisted.', serial)
+ def denylisted(serial):
+ if serial in denylisted_devices:
+ logger.warning('Device %s is denylisted.', serial)
return True
return False
@@ -3180,12 +3632,12 @@ class DeviceUtils(object):
def _get_devices():
if device_arg:
- devices = [cls(x, **kwargs) for x in device_arg if not blacklisted(x)]
+ devices = [cls(x, **kwargs) for x in device_arg if not denylisted(x)]
else:
devices = []
for adb in adb_wrapper.AdbWrapper.Devices():
serial = adb.GetDeviceSerial()
- if not blacklisted(serial):
+ if not denylisted(serial):
device = cls(_CreateAdbWrapper(adb), **kwargs)
if supports_abi(device.GetABI(), serial):
devices.append(device)
@@ -3208,7 +3660,7 @@ class DeviceUtils(object):
else:
reset_usb.reset_all_android_devices()
- for attempt in xrange(retries+1):
+ for attempt in xrange(retries + 1):
try:
return _get_devices()
except device_errors.NoDevicesError:
@@ -3233,30 +3685,26 @@ class DeviceUtils(object):
logger.info('Restarting adbd on device.')
with device_temp_file.DeviceTempFile(self.adb, suffix='.sh') as script:
self.WriteFile(script.name, _RESTART_ADBD_SCRIPT)
- self.RunShellCommand(
- ['source', script.name], check_return=True, as_root=True)
+ self.RunShellCommand(['source', script.name],
+ check_return=True,
+ as_root=True)
self.adb.WaitForDevice()
@decorators.WithTimeoutAndRetriesFromInstance()
def GrantPermissions(self, package, permissions, timeout=None, retries=None):
- # Permissions only need to be set on M and above because of the changes to
- # the permission model.
- if not permissions or self.build_version_sdk < version_codes.MARSHMALLOW:
+ if not permissions:
return
- permissions = set(
- p for p in permissions if not _PERMISSIONS_BLACKLIST_RE.match(p))
+ permissions = set(p for p in permissions
+ if not _PERMISSIONS_DENYLIST_RE.match(p))
if ('android.permission.WRITE_EXTERNAL_STORAGE' in permissions
and 'android.permission.READ_EXTERNAL_STORAGE' not in permissions):
permissions.add('android.permission.READ_EXTERNAL_STORAGE')
script = ';'.join([
- 'p={package}',
- 'for q in {permissions}',
- 'do pm grant "$p" "$q"',
- 'echo "{sep}$q{sep}$?{sep}"',
- 'done'
+ 'p={package}', 'for q in {permissions}', 'do pm grant "$p" "$q"',
+ 'echo "{sep}$q{sep}$?{sep}"', 'done'
]).format(
package=cmd_helper.SingleQuote(package),
permissions=' '.join(
@@ -3265,17 +3713,21 @@ class DeviceUtils(object):
logger.info('Setting permissions for %s.', package)
res = self.RunShellCommand(
- script, shell=True, raw_output=True, large_output=True,
+ script,
+ shell=True,
+ raw_output=True,
+ large_output=True,
check_return=True)
res = res.split(_SHELL_OUTPUT_SEPARATOR)
failures = [
- (permission, output.strip())
- for permission, status, output in zip(res[1::3], res[2::3], res[0::3])
- if int(status)]
+ (permission, output.strip())
+ for permission, status, output in zip(res[1::3], res[2::3], res[0::3])
+ if int(status)
+ ]
if failures:
logger.warning(
- 'Failed to grant some permissions. Blacklist may need to be updated?')
+ 'Failed to grant some permissions. Denylist may need to be updated?')
for permission, output in failures:
# Try to grab the relevant error message from the output.
m = _PERMISSIONS_EXCEPTION_RE.search(output)
@@ -3316,8 +3768,8 @@ class DeviceUtils(object):
dumpsys_out = self._RunPipedShellCommand(
'dumpsys input_method | grep %s' % input_check)
if not dumpsys_out:
- raise device_errors.CommandFailedError(
- 'Unable to detect screen state', str(self))
+ raise device_errors.CommandFailedError('Unable to detect screen state',
+ str(self))
return check_value in dumpsys_out[0]
@decorators.WithTimeoutAndRetriesFromInstance()
@@ -3327,6 +3779,7 @@ class DeviceUtils(object):
Args:
on: bool to decide state to switch to. True = on False = off.
"""
+
def screen_test():
return self.IsScreenOn() == on
@@ -3353,7 +3806,10 @@ class DeviceUtils(object):
self.RunShellCommand(['chown', owner_group] + paths, check_return=True)
@decorators.WithTimeoutAndRetriesFromInstance()
- def ChangeSecurityContext(self, security_context, paths, timeout=None,
+ def ChangeSecurityContext(self,
+ security_context,
+ paths,
+ timeout=None,
retries=None):
"""Changes the SELinux security context for files.