| # 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. |
| """ |
| # pylint: disable=unused-argument |
| |
| import calendar |
| import collections |
| import fnmatch |
| import json |
| import logging |
| import os |
| import posixpath |
| import pprint |
| import random |
| import re |
| import shutil |
| import stat |
| import tempfile |
| import time |
| import threading |
| import uuid |
| |
| from devil import base_error |
| from devil import devil_env |
| from devil.utils import cmd_helper |
| from devil.android import apk_helper |
| from devil.android import device_signal |
| from devil.android import decorators |
| from devil.android import device_errors |
| from devil.android import device_temp_file |
| from devil.android import install_commands |
| from devil.android import logcat_monitor |
| 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 |
| from devil.utils import reraiser_thread |
| from devil.utils import timeout_retry |
| from devil.utils import zip_utils |
| |
| from py_utils import tempfile_ext |
| |
| logger = logging.getLogger(__name__) |
| |
| _DEFAULT_TIMEOUT = 30 |
| _DEFAULT_RETRIES = 3 |
| |
| # A sentinel object for default values |
| # TODO(jbudorick,perezju): revisit how default values are handled by |
| # the timeout_retry decorators. |
| DEFAULT = object() |
| |
| _RESTART_ADBD_SCRIPT = """ |
| trap '' HUP |
| trap '' TERM |
| trap '' PIPE |
| function restart() { |
| stop adbd |
| start adbd |
| } |
| restart & |
| """ |
| |
| # 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.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) |
| |
| _CURRENT_FOCUS_CRASH_RE = re.compile( |
| r'\s*mCurrentFocus.*Application (Error|Not Responding): (\S+)}') |
| |
| _GETPROP_RE = re.compile(r'\[(.*?)\]: \[(.*?)\]') |
| |
| # Regex to parse the long (-l) output of 'ls' command, c.f. |
| # https://github.com/landley/toybox/blob/master/toys/posix/ls.c#L446 |
| _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) |
| r'(?P<st_owner>\w+)\s+' # Name of owner |
| r'(?P<st_group>\w+)\s+' # Group of owner |
| r'(?:' # Either ... |
| r'(?P<st_rdev_major>\d+),\s+' # Device major, and |
| r'(?P<st_rdev_minor>\d+)\s+' # Device minor |
| r'|' # .. or |
| r'(?P<st_size>\d+)\s+' # Size in bytes |
| r')?' # .. or nothing |
| r'(?P<st_mtime>\d{4}-\d\d-\d\d \d\d:\d\d)\s+' # Modification date/time |
| r'(?P<filename>.+?)' # File name |
| r'(?: -> (?P<symbolic_link_to>.+))?' # Symbolic link (optional) |
| r'$' # End of string |
| ) |
| _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} |
| _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, |
| ] |
| _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 |
| } |
| # Some devices require different logic for checking if root is necessary |
| _SPECIAL_ROOT_DEVICE_LIST = [ |
| 'marlin', # Pixel XL |
| 'sailfish', # Pixel |
| 'taimen', # Pixel 2 XL |
| 'walleye', # Pixel 2 |
| ] |
| _IMEI_RE = re.compile(r' Device ID = (.+)$') |
| # The following regex is used to match result parcels like: |
| """ |
| Result: Parcel( |
| 0x00000000: 00000000 0000000f 00350033 00360033 '........3.5.3.6.' |
| 0x00000010: 00360032 00370030 00300032 00300039 '2.6.0.7.2.0.9.0.' |
| 0x00000020: 00380033 00000039 '3.8.9... ') |
| """ |
| _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') |
| |
| PS_COLUMNS = ('name', 'pid', 'ppid') |
| ProcessInfo = collections.namedtuple('ProcessInfo', PS_COLUMNS) |
| |
| |
| @decorators.WithExplicitTimeoutAndRetries( |
| _DEFAULT_TIMEOUT, _DEFAULT_RETRIES) |
| def GetAVDs(): |
| """Returns a list of Android Virtual Devices. |
| |
| Returns: |
| A list containing the configured AVDs. |
| """ |
| lines = cmd_helper.GetCmdOutput([ |
| os.path.join(devil_env.config.LocalPath('android_sdk'), |
| 'tools', 'android'), |
| 'list', 'avd']).splitlines() |
| avds = [] |
| for line in lines: |
| if 'Name:' not in line: |
| continue |
| key, value = (s.strip() for s in line.split(':', 1)) |
| if key == 'Name': |
| avds.append(value) |
| return avds |
| |
| |
| @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() |
| |
| def adb_started(): |
| return adb_wrapper.AdbWrapper.IsServerOnline() |
| |
| 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 |
| logger.warning('Failed to kill adb server') |
| adb_wrapper.AdbWrapper.StartServer() |
| if not timeout_retry.WaitFor(adb_started, wait_period=1, max_tries=5): |
| raise device_errors.CommandFailedError('Failed to start adb server') |
| |
| |
| def _ParseModeString(mode_str): |
| """Parse a mode string, e.g. 'drwxrwxrwx', into a st_mode value. |
| |
| Effectively the reverse of |mode_to_string| in, e.g.: |
| https://github.com/landley/toybox/blob/master/lib/lib.c#L896 |
| """ |
| if not _FILE_MODE_RE.match(mode_str): |
| raise ValueError('Unexpected file mode %r', mode_str) |
| mode = _FILE_MODE_KIND[mode_str[0]] |
| for c, flag in zip(mode_str[1:], _FILE_MODE_PERMS): |
| if c != '-' and c.islower(): |
| mode |= flag |
| for c, (t, flag) in zip(mode_str[3::3], _FILE_MODE_SPECIAL): |
| if c.lower() == t: |
| mode |= flag |
| return mode |
| |
| |
| def _GetTimeStamp(): |
| """Return a basic ISO 8601 time stamp with the current local time.""" |
| return time.strftime('%Y%m%dT%H%M%S', time.localtime()) |
| |
| |
| def _JoinLines(lines): |
| # makes sure that the last line is also terminated, and is more memory |
| # efficient than first appending an end-line to each line and then joining |
| # all of them together. |
| return ''.join(s for line in lines for s in (line, '\n')) |
| |
| |
| def _CreateAdbWrapper(device): |
| if isinstance(device, adb_wrapper.AdbWrapper): |
| return device |
| else: |
| return adb_wrapper.AdbWrapper(device) |
| |
| |
| def _FormatPartialOutputError(output): |
| lines = output.splitlines() if isinstance(output, basestring) else output |
| message = ['Partial output found:'] |
| if len(lines) > 11: |
| message.extend('- %s' % line for line in lines[:5]) |
| message.extend('<snip>') |
| message.extend('- %s' % line for line in lines[-5:]) |
| else: |
| message.extend('- %s' % line for line in lines) |
| return '\n'.join(message) |
| |
| |
| class DeviceUtils(object): |
| |
| _MAX_ADB_COMMAND_LENGTH = 512 |
| _MAX_ADB_OUTPUT_LENGTH = 32768 |
| _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') |
| |
| # Property in /data/local.prop that controls Java assertions. |
| JAVA_ASSERT_PROPERTY = 'dalvik.vm.enableassertions' |
| |
| def __init__(self, device, enable_device_files_cache=False, |
| default_timeout=_DEFAULT_TIMEOUT, |
| default_retries=_DEFAULT_RETRIES): |
| """DeviceUtils constructor. |
| |
| Args: |
| device: Either a device serial, an existing AdbWrapper instance, or an |
| an existing AndroidCommands instance. |
| enable_device_files_cache: For PushChangedFiles(), cache checksums of |
| pushed files rather than recomputing them on a subsequent call. |
| default_timeout: An integer containing the default number of seconds to |
| wait for an operation to complete if no explicit value is provided. |
| default_retries: An integer containing the default number or times an |
| operation should be retried on failure if no explicit value is provided. |
| """ |
| self.adb = None |
| if isinstance(device, basestring): |
| self.adb = _CreateAdbWrapper(device) |
| elif isinstance(device, adb_wrapper.AdbWrapper): |
| self.adb = device |
| else: |
| raise ValueError('Unsupported device value: %r' % device) |
| self._commands_installed = None |
| self._default_timeout = default_timeout |
| self._default_retries = default_retries |
| self._enable_device_files_cache = enable_device_files_cache |
| self._cache = {} |
| self._client_caches = {} |
| self._cache_lock = threading.RLock() |
| assert hasattr(self, decorators.DEFAULT_TIMEOUT_ATTR) |
| assert hasattr(self, decorators.DEFAULT_RETRIES_ATTR) |
| |
| self._ClearCache() |
| |
| @property |
| def serial(self): |
| """Returns the device serial.""" |
| return self.adb.GetDeviceSerial() |
| |
| def __eq__(self, other): |
| """Checks whether |other| refers to the same device as |self|. |
| |
| Args: |
| other: The object to compare to. This can be a basestring, an instance |
| of adb_wrapper.AdbWrapper, or an instance of DeviceUtils. |
| Returns: |
| Whether |other| refers to the same device as |self|. |
| """ |
| return self.serial == str(other) |
| |
| def __lt__(self, other): |
| """Compares two instances of DeviceUtils. |
| |
| This merely compares their serial numbers. |
| |
| Args: |
| other: The instance of DeviceUtils to compare to. |
| Returns: |
| Whether |self| is less than |other|. |
| """ |
| return self.serial < other.serial |
| |
| def __str__(self): |
| """Returns the device serial.""" |
| return self.serial |
| |
| @decorators.WithTimeoutAndRetriesFromInstance() |
| def IsOnline(self, timeout=None, retries=None): |
| """Checks whether the device is online. |
| |
| Args: |
| timeout: timeout in seconds |
| retries: number of retries |
| |
| Returns: |
| True if the device is online, False otherwise. |
| |
| Raises: |
| CommandTimeoutError on timeout. |
| """ |
| try: |
| return self.adb.GetState() == 'device' |
| except base_error.BaseError as exc: |
| logger.info('Failed to get state: %s', exc) |
| return False |
| |
| @decorators.WithTimeoutAndRetriesFromInstance() |
| def HasRoot(self, timeout=None, retries=None): |
| """Checks whether or not adbd has root privileges. |
| |
| Args: |
| timeout: timeout in seconds |
| retries: number of retries |
| |
| Returns: |
| True if adbd has root privileges, False otherwise. |
| |
| Raises: |
| CommandTimeoutError on timeout. |
| DeviceUnreachableError on missing device. |
| """ |
| try: |
| if self.product_name in _SPECIAL_ROOT_DEVICE_LIST: |
| return self.GetProp('service.adb.root') == '1' |
| self.RunShellCommand(['ls', '/root'], check_return=True) |
| return True |
| except device_errors.AdbCommandFailedError: |
| return False |
| |
| def NeedsSU(self, timeout=DEFAULT, retries=DEFAULT): |
| """Checks whether 'su' is needed to access protected resources. |
| |
| Args: |
| timeout: timeout in seconds |
| retries: number of retries |
| |
| Returns: |
| True if 'su' is available on the device and is needed to to access |
| protected resources; False otherwise if either 'su' is not available |
| (e.g. because the device has a user build), or not needed (because adbd |
| already has root privileges). |
| |
| Raises: |
| CommandTimeoutError on timeout. |
| DeviceUnreachableError on missing device. |
| """ |
| if 'needs_su' not in self._cache: |
| cmd = '%s && ! ls /root' % self._Su('ls /root') |
| if 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, |
| 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 |
| except device_errors.AdbCommandFailedError: |
| 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 |
| return 'su -c %s' % command |
| |
| @decorators.WithTimeoutAndRetriesFromInstance() |
| def EnableRoot(self, timeout=None, retries=None): |
| """Restarts adbd with root privileges. |
| |
| Args: |
| timeout: timeout in seconds |
| retries: number of retries |
| |
| Raises: |
| CommandFailedError if root could not be enabled. |
| CommandTimeoutError on timeout. |
| """ |
| if 'needs_su' in self._cache: |
| del self._cache['needs_su'] |
| |
| try: |
| self.adb.Root() |
| except device_errors.AdbCommandFailedError: |
| if self.IsUserBuild(): |
| raise device_errors.CommandFailedError( |
| 'Unable to root device with user build.', str(self)) |
| else: |
| raise # Failed probably due to some other reason. |
| |
| def device_online_with_root(): |
| try: |
| self.adb.WaitForDevice() |
| return self.GetProp('service.adb.root', cache=False) == '1' |
| except (device_errors.AdbCommandFailedError, |
| device_errors.DeviceUnreachableError): |
| return False |
| |
| timeout_retry.WaitFor(device_online_with_root, wait_period=1) |
| |
| @decorators.WithTimeoutAndRetriesFromInstance() |
| def IsUserBuild(self, timeout=None, retries=None): |
| """Checks whether or not the device is running a user build. |
| |
| Args: |
| timeout: timeout in seconds |
| retries: number of retries |
| |
| Returns: |
| True if the device is running a user build, False otherwise (i.e. if |
| it's running a userdebug build). |
| |
| Raises: |
| CommandTimeoutError on timeout. |
| DeviceUnreachableError on missing device. |
| """ |
| return self.build_type == 'user' |
| |
| @decorators.WithTimeoutAndRetriesFromInstance() |
| def GetExternalStoragePath(self, timeout=None, retries=None): |
| """Get the device's path to its SD card. |
| |
| Args: |
| timeout: timeout in seconds |
| retries: number of retries |
| |
| Returns: |
| The device's path to its SD card. |
| |
| Raises: |
| CommandFailedError if the external storage path could not be determined. |
| CommandTimeoutError on timeout. |
| DeviceUnreachableError on missing device. |
| """ |
| self._EnsureCacheInitialized() |
| if not self._cache['external_storage']: |
| raise device_errors.CommandFailedError('$EXTERNAL_STORAGE is not set', |
| str(self)) |
| return self._cache['external_storage'] |
| |
| @decorators.WithTimeoutAndRetriesFromInstance() |
| def GetIMEI(self, timeout=None, retries=None): |
| """Get the device's IMEI. |
| |
| Args: |
| timeout: timeout in seconds |
| retries: number of retries |
| |
| Returns: |
| The device's IMEI. |
| |
| Raises: |
| AdbCommandFailedError on error |
| """ |
| if self._cache.get('imei') is not None: |
| return self._cache.get('imei') |
| |
| if self.build_version_sdk < 21: |
| out = self.RunShellCommand(['dumpsys', 'iphonesubinfo'], |
| raw_output=True, check_return=True) |
| if out: |
| match = re.search(_IMEI_RE, out) |
| if match: |
| self._cache['imei'] = match.group(1) |
| return self._cache['imei'] |
| else: |
| out = self.RunShellCommand(['service', 'call', 'iphonesubinfo', '1'], |
| check_return=True) |
| if out: |
| imei = '' |
| for line in out: |
| match = re.search(_PARCEL_RESULT_RE, line) |
| if match: |
| imei = imei + match.group(1) |
| imei = imei.replace('.', '').strip() |
| if imei: |
| self._cache['imei'] = imei |
| return self._cache['imei'] |
| |
| raise device_errors.CommandFailedError('Unable to fetch IMEI.') |
| |
| @decorators.WithTimeoutAndRetriesFromInstance() |
| def GetApplicationPaths(self, package, timeout=None, retries=None): |
| """Get the paths of the installed apks on the device for the given package. |
| |
| Args: |
| package: Name of the package. |
| |
| Returns: |
| List of paths to the apks on the device for the given package. |
| """ |
| return self._GetApplicationPathsInternal(package) |
| |
| def _GetApplicationPathsInternal(self, package, skip_cache=False): |
| cached_result = self._cache['package_apk_paths'].get(package) |
| if cached_result is not None and not skip_cache: |
| if package in self._cache['package_apk_paths_to_verify']: |
| self._cache['package_apk_paths_to_verify'].remove(package) |
| # Don't verify an app that is not thought to be installed. We are |
| # concerned only with apps we think are installed having been |
| # uninstalled manually. |
| if cached_result and not self.PathExists(cached_result): |
| cached_result = None |
| self._cache['package_apk_checksums'].pop(package, 0) |
| if cached_result is not None: |
| return list(cached_result) |
| # 'pm path' is liable to incorrectly exit with a nonzero number starting |
| # in Lollipop. |
| # 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) |
| apks = [] |
| bad_output = False |
| for line in output: |
| if line.startswith('package:'): |
| apks.append(line[len('package:'):]) |
| elif line.startswith('WARNING:'): |
| continue |
| else: |
| bad_output = True # Unexpected line in output. |
| if not apks and output: |
| if bad_output: |
| raise device_errors.CommandFailedError( |
| 'Unexpected pm path output: %r' % '\n'.join(output), str(self)) |
| else: |
| logger.warning('pm returned no paths but the following warnings:') |
| for line in output: |
| logger.warning('- %s', line) |
| self._cache['package_apk_paths'][package] = list(apks) |
| return apks |
| |
| @decorators.WithTimeoutAndRetriesFromInstance() |
| def GetApplicationVersion(self, package, timeout=None, retries=None): |
| """Get the version name of a package installed on the device. |
| |
| Args: |
| package: Name of the package. |
| |
| Returns: |
| 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) |
| if not output: |
| return None |
| for line in output: |
| line = line.strip() |
| if line.startswith('versionName='): |
| return line[len('versionName='):] |
| raise device_errors.CommandFailedError( |
| 'Version name for %s not found on dumpsys output' % package, str(self)) |
| |
| @decorators.WithTimeoutAndRetriesFromInstance() |
| def GetApplicationDataDirectory(self, package, timeout=None, retries=None): |
| """Get the data directory on the device for the given package. |
| |
| Args: |
| package: Name of the package. |
| |
| Returns: |
| The package's data directory. |
| Raises: |
| CommandFailedError if the package's data directory can't be found, |
| whether because it's not installed or otherwise. |
| """ |
| output = self._RunPipedShellCommand( |
| 'pm dump %s | grep dataDir=' % cmd_helper.SingleQuote(package)) |
| for line in output: |
| _, _, dataDir = line.partition('dataDir=') |
| if dataDir: |
| return dataDir |
| raise device_errors.CommandFailedError( |
| 'Could not find data directory for %s', package) |
| |
| 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 |
| both adb version and device OS version. To make it simpler, this directly |
| runs the bugreport command on the device itself and dumps the stdout to a |
| file. |
| |
| Args: |
| path: Path on the host to drop the bug report. |
| timeout: (optional) Timeout per try in seconds. |
| retries: (optional) Number of retries to attempt. |
| """ |
| with device_temp_file.DeviceTempFile(self.adb) as device_tmp_file: |
| cmd = '( bugreport )>%s 2>&1' % device_tmp_file.name |
| self.RunShellCommand( |
| cmd, check_return=True, shell=True, timeout=timeout, retries=retries) |
| self.PullFile(device_tmp_file.name, path) |
| |
| @decorators.WithTimeoutAndRetriesFromInstance() |
| def WaitUntilFullyBooted(self, wifi=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. |
| |
| Args: |
| wifi: A boolean indicating if we should wait for wifi to come up or not. |
| timeout: timeout in seconds |
| retries: number of retries |
| |
| Raises: |
| CommandFailedError on failure. |
| 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) |
| return True |
| except device_errors.AdbCommandFailedError: |
| return False |
| |
| def pm_ready(): |
| try: |
| return self._GetApplicationPathsInternal('android', skip_cache=True) |
| except device_errors.CommandFailedError: |
| return False |
| |
| def boot_completed(): |
| try: |
| return self.GetProp('sys.boot_completed', cache=False) == '1' |
| except device_errors.CommandFailedError: |
| return False |
| |
| def wifi_enabled(): |
| return 'Wi-Fi is enabled' in self.RunShellCommand(['dumpsys', 'wifi'], |
| check_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) |
| |
| 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): |
| """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. |
| timeout: timeout in seconds |
| retries: number of retries |
| |
| Raises: |
| CommandTimeoutError on timeout. |
| DeviceUnreachableError on missing device. |
| """ |
| def device_offline(): |
| return not self.IsOnline() |
| |
| self.adb.Reboot() |
| self._ClearCache() |
| timeout_retry.WaitFor(device_offline, wait_period=1) |
| if block: |
| self.WaitUntilFullyBooted(wifi=wifi) |
| |
| INSTALL_DEFAULT_TIMEOUT = 4 * _DEFAULT_TIMEOUT |
| |
| @decorators.WithTimeoutAndRetriesFromInstance( |
| min_default_timeout=INSTALL_DEFAULT_TIMEOUT) |
| def Install(self, apk, allow_downgrade=False, reinstall=False, |
| permissions=None, timeout=None, retries=None): |
| """Install an APK. |
| |
| Noop if an identical APK is already installed. |
| |
| Args: |
| apk: An ApkHelper instance or string containing the path to the APK. |
| allow_downgrade: A boolean indicating if we should allow downgrades. |
| reinstall: A boolean indicating if we should keep any existing app data. |
| permissions: Set of permissions to set. If not set, finds permissions with |
| apk helper. To set no permissions, pass []. |
| timeout: timeout in seconds |
| retries: number of retries |
| |
| 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) |
| |
| @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): |
| """Install a split APK. |
| |
| Noop if all of the APK splits are already installed. |
| |
| Args: |
| base_apk: An ApkHelper instance or string containing the path to the base |
| APK. |
| split_apks: A list of strings of paths of all of the APK splits. |
| allow_downgrade: A boolean indicating if we should allow downgrades. |
| reinstall: A boolean indicating if we should keep any existing app data. |
| allow_cached_props: Whether to use cached values for device properties. |
| permissions: Set of permissions to set. If not set, finds permissions with |
| apk helper. To set no permissions, pass []. |
| timeout: timeout in seconds |
| retries: number of retries |
| |
| Raises: |
| CommandFailedError if the installation fails. |
| CommandTimeoutError if the installation times out. |
| 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): |
| if split_apks: |
| self._CheckSdkLevel(version_codes.LOLLIPOP) |
| |
| base_apk = apk_helper.ToHelper(base_apk) |
| |
| 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)] |
| if missing_apks: |
| raise device_errors.CommandFailedError( |
| 'Attempted to install non-existent apks: %s' |
| % pprint.pformat(missing_apks)) |
| |
| package_name = base_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: |
| 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: |
| logger.warning( |
| 'Installing split APK when non-split APK was previously installed') |
| apks_to_install = all_apks |
| else: |
| try: |
| apks_to_install, host_checksums = ( |
| self._ComputeStaleApks(package_name, all_apks)) |
| except EnvironmentError as e: |
| logger.warning('Error calculating md5: %s', e) |
| apks_to_install, host_checksums = all_apks, None |
| if apks_to_install and not reinstall: |
| self.Uninstall(package_name) |
| apks_to_install = all_apks |
| |
| if apks_to_install: |
| # 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 |
| self.adb.InstallMultiple( |
| apks_to_install, partial=partial, reinstall=reinstall, |
| allow_downgrade=allow_downgrade) |
| else: |
| self.adb.Install( |
| base_apk.path, reinstall=reinstall, 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. |
| self.ForceStop(package_name) |
| |
| if (permissions is None |
| and self.build_version_sdk >= version_codes.MARSHMALLOW): |
| permissions = base_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 |
| |
| @decorators.WithTimeoutAndRetriesFromInstance() |
| def Uninstall(self, package_name, keep_data=False, timeout=None, |
| retries=None): |
| """Remove the app |package_name| from the device. |
| |
| This is a no-op if the app is not already installed. |
| |
| Args: |
| package_name: The package to uninstall. |
| keep_data: (optional) Whether to keep the data and cache directories. |
| timeout: Timeout in seconds. |
| retries: Number of retries. |
| |
| Raises: |
| CommandFailedError if the uninstallation fails. |
| CommandTimeoutError if the uninstallation times out. |
| DeviceUnreachableError on missing device. |
| """ |
| installed = self._GetApplicationPathsInternal(package_name) |
| if not installed: |
| return |
| try: |
| self.adb.Uninstall(package_name, keep_data) |
| self._cache['package_apk_paths'][package_name] = [] |
| self._cache['package_apk_checksums'][package_name] = set() |
| except: |
| # Clear cache since we can't be sure of the state. |
| self._cache['package_apk_paths'].pop(package_name, 0) |
| self._cache['package_apk_checksums'].pop(package_name, 0) |
| raise |
| |
| def _CheckSdkLevel(self, required_sdk_level): |
| """Raises an exception if the device does not have the required SDK level. |
| """ |
| if self.build_version_sdk < required_sdk_level: |
| raise device_errors.DeviceVersionError( |
| ('Requires SDK level %s, device is SDK level %s' % |
| (required_sdk_level, self.build_version_sdk)), |
| 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, |
| retries=None): |
| """Run an ADB shell command. |
| |
| The command to run |cmd| should be a sequence of program arguments |
| (preferred) or a single string with a shell script to run. |
| |
| When |cmd| is a sequence, it is assumed to contain the name of the command |
| to run followed by its arguments. In this case, arguments are passed to the |
| command exactly as given, preventing any further processing by the shell. |
| This allows callers to easily pass arguments with spaces or special |
| characters without having to worry about quoting rules. Whenever possible, |
| it is recomended to pass |cmd| as a sequence. |
| |
| When |cmd| is passed as a single string, |shell| should be set to True. |
| The command will be interpreted and run by the shell on the device, |
| allowing the use of shell features such as pipes, wildcards, or variables. |
| Failing to set shell=True will issue a warning, but this will be changed |
| to a hard failure in the future (see: catapult:#3242). |
| |
| 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. |
| |
| Args: |
| cmd: A sequence containing the command to run and its arguments, or a |
| string with a shell script to run (should also set shell=True). |
| shell: A boolean indicating whether shell features may be used in |cmd|. |
| check_return: A boolean indicating whether or not the return code should |
| be checked. |
| cwd: The device directory in which the command should be run. |
| env: The environment variables with which the command should be run. |
| run_as: A string containing the package as which the command should be |
| run. |
| as_root: A boolean indicating whether the shell command should be run |
| with root privileges. |
| single_line: A boolean indicating if only a single line of output is |
| expected. |
| large_output: Uses a work-around for large shell command output. Without |
| this large output will be truncated. |
| raw_output: Whether to only return the raw output |
| (no splitting into lines). |
| timeout: timeout in seconds |
| retries: number of retries |
| |
| Returns: |
| If single_line is False, the output of the command as a list of lines, |
| otherwise, a string with the unique line of output emmited by the command |
| (with the optional newline at the end stripped). |
| |
| Raises: |
| AdbCommandFailedError if check_return is True and the exit code of |
| the command run on the device is non-zero. |
| CommandFailedError if single_line is True but the output contains two or |
| more lines. |
| 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) |
| # using double quotes here to allow interpolation of shell variables |
| return '%s=%s' % (key, cmd_helper.DoubleQuote(value)) |
| |
| def run(cmd): |
| return self.adb.Shell(cmd) |
| |
| def handle_check_return(cmd): |
| try: |
| return run(cmd) |
| except device_errors.AdbCommandFailedError as exc: |
| if check_return: |
| raise |
| else: |
| return exc.output |
| |
| def handle_large_command(cmd): |
| if len(cmd) < self._MAX_ADB_COMMAND_LENGTH: |
| return handle_check_return(cmd) |
| 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]) |
| return handle_check_return('sh %s' % script.name_quoted) |
| |
| def handle_large_output(cmd, large_output_mode): |
| if large_output_mode: |
| with device_temp_file.DeviceTempFile(self.adb) as large_output_file: |
| cmd = '( %s )>%s 2>&1' % (cmd, large_output_file.name) |
| logger.debug('Large output mode enabled. Will write output to ' |
| 'device and read results from file.') |
| handle_large_command(cmd) |
| return self.ReadFile(large_output_file.name, force_pull=True) |
| else: |
| try: |
| return handle_large_command(cmd) |
| except device_errors.AdbCommandFailedError as exc: |
| if exc.status is None: |
| logger.error(_FormatPartialOutputError(exc.output)) |
| logger.warning('Attempting to run in large_output mode.') |
| logger.warning('Use RunShellCommand(..., large_output=True) for ' |
| 'shell commands that expect a lot of output.') |
| return handle_large_output(cmd, True) |
| else: |
| raise |
| |
| if isinstance(cmd, basestring): |
| if not shell: |
| logger.warning( |
| 'The command to run should preferably be passed as a sequence of' |
| ' args. If shell features are needed (pipes, wildcards, variables)' |
| ' clients should explicitly set shell=True.') |
| else: |
| cmd = ' '.join(cmd_helper.SingleQuote(s) for s in cmd) |
| if env: |
| env = ' '.join(env_quote(k, v) for k, v in env.iteritems()) |
| cmd = '%s %s' % (env, cmd) |
| if cwd: |
| cmd = 'cd %s && %s' % (cmd_helper.SingleQuote(cwd), cmd) |
| if run_as: |
| cmd = 'run-as %s sh -c %s' % (cmd_helper.SingleQuote(run_as), |
| cmd_helper.SingleQuote(cmd)) |
| if as_root and self.NeedsSU(): |
| # "su -c sh -c" allows using shell features in |cmd| |
| cmd = self._Su('sh -c %s' % cmd_helper.SingleQuote(cmd)) |
| |
| output = handle_large_output(cmd, large_output) |
| |
| if raw_output: |
| return output |
| |
| output = output.splitlines() |
| if single_line: |
| if not output: |
| return '' |
| elif len(output) == 1: |
| return output[0] |
| else: |
| msg = 'one line of output was expected, but got: %s' |
| raise device_errors.CommandFailedError(msg % output, str(self)) |
| else: |
| return output |
| |
| def _RunPipedShellCommand(self, script, **kwargs): |
| PIPESTATUS_LEADER = 'PIPESTATUS: ' |
| |
| script += '; echo "%s${PIPESTATUS[@]}"' % PIPESTATUS_LEADER |
| kwargs.update(shell=True, check_return=True) |
| output = self.RunShellCommand(script, **kwargs) |
| pipestatus_line = output[-1] |
| |
| 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) |
| |
| output = output[:-1] |
| statuses = [ |
| 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) |
| 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): |
| """Kill all processes with the given name on the device. |
| |
| Args: |
| process_name: A string containing the name of the process to kill. |
| exact: A boolean indicating whether to kill all processes matching |
| the string |process_name| exactly, or all of those which contain |
| |process_name| as a substring. Defaults to False. |
| signum: An integer containing the signal number to send to kill. Defaults |
| to SIGKILL (9). |
| as_root: A boolean indicating whether the kill should be executed with |
| root privileges. |
| blocking: A boolean indicating whether we should wait until all processes |
| with the given |process_name| are dead. |
| quiet: A boolean indicating whether to ignore the fact that no processes |
| to kill were found. |
| timeout: timeout in seconds |
| retries: number of retries |
| |
| Returns: |
| The number of processes attempted to kill. |
| |
| Raises: |
| CommandFailedError if no process was killed and |quiet| is False. |
| CommandTimeoutError on timeout. |
| DeviceUnreachableError on missing device. |
| """ |
| processes = self.ListProcesses(process_name) |
| if exact: |
| processes = [p for p in processes if p.name == process_name] |
| if not processes: |
| if quiet: |
| return 0 |
| else: |
| raise device_errors.CommandFailedError( |
| 'No processes matching %r (exact=%r)' % (process_name, exact), |
| str(self)) |
| |
| logger.info( |
| 'KillAll(%r, ...) attempting to kill the following:', process_name) |
| for p in processes: |
| logger.info(' %05d %s', p.pid, p.name) |
| |
| pids = set(p.pid for p in processes) |
| cmd = ['kill', '-%d' % signum] + sorted(str(p) for p in pids) |
| self.RunShellCommand(cmd, as_root=as_root, check_return=True) |
| |
| def all_pids_killed(): |
| pids_left = (p.pid for p in self.ListProcesses(process_name)) |
| return not pids.intersection(pids_left) |
| |
| if blocking: |
| timeout_retry.WaitFor(all_pids_killed, wait_period=0.1) |
| |
| return len(pids) |
| |
| @decorators.WithTimeoutAndRetriesFromInstance() |
| 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: |
| intent_obj: An Intent object to send. |
| blocking: A boolean indicating whether we should wait for the activity to |
| finish launching. |
| trace_file_name: If present, a string that both indicates that we want to |
| profile the activity and contains the path to which the |
| trace should be saved. |
| force_stop: A boolean indicating whether we should stop the activity |
| before starting it. |
| timeout: timeout in seconds |
| retries: number of retries |
| |
| Raises: |
| CommandFailedError if the activity could not be started. |
| CommandTimeoutError on timeout. |
| DeviceUnreachableError on missing device. |
| """ |
| cmd = ['am', 'start'] |
| if blocking: |
| cmd.append('-W') |
| if trace_file_name: |
| cmd.extend(['--start-profiler', trace_file_name]) |
| if force_stop: |
| cmd.append('-S') |
| cmd.extend(intent_obj.am_args) |
| for line in self.RunShellCommand(cmd, check_return=True): |
| if line.startswith('Error:'): |
| raise device_errors.CommandFailedError(line, str(self)) |
| |
| @decorators.WithTimeoutAndRetriesFromInstance() |
| def StartInstrumentation(self, component, finish=True, raw=False, |
| extras=None, timeout=None, retries=None): |
| if extras is None: |
| extras = {} |
| |
| cmd = ['am', 'instrument'] |
| if finish: |
| cmd.append('-w') |
| if raw: |
| cmd.append('-r') |
| for k, v in extras.iteritems(): |
| cmd.extend(['-e', str(k), str(v)]) |
| cmd.append(component) |
| |
| # Store the package name in a shell variable to help the command stay under |
| # the _MAX_ADB_COMMAND_LENGTH limit. |
| 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) |
| |
| @decorators.WithTimeoutAndRetriesFromInstance() |
| def BroadcastIntent(self, intent_obj, timeout=None, retries=None): |
| """Send a broadcast intent. |
| |
| Args: |
| intent: An Intent to broadcast. |
| timeout: timeout in seconds |
| retries: number of retries |
| |
| Raises: |
| CommandTimeoutError on timeout. |
| DeviceUnreachableError on missing device. |
| """ |
| cmd = ['am', 'broadcast'] + intent_obj.am_args |
| self.RunShellCommand(cmd, check_return=True) |
| |
| @decorators.WithTimeoutAndRetriesFromInstance() |
| def GoHome(self, timeout=None, retries=None): |
| """Return to the home screen and obtain launcher focus. |
| |
| This command launches the home screen and attempts to obtain |
| launcher focus until the timeout is reached. |
| |
| Args: |
| timeout: timeout in seconds |
| retries: number of retries |
| |
| Raises: |
| CommandTimeoutError on timeout. |
| DeviceUnreachableError on missing device. |
| """ |
| def is_launcher_focused(): |
| output = self.RunShellCommand(['dumpsys', 'window', 'windows'], |
| check_return=True, large_output=True) |
| return any(self._LAUNCHER_FOCUSED_RE.match(l) for l in output) |
| |
| def dismiss_popups(): |
| # There is a dialog present; attempt to get rid of it. |
| # Not all dialogs can be dismissed with back. |
| self.SendKeyEvent(keyevent.KEYCODE_ENTER) |
| self.SendKeyEvent(keyevent.KEYCODE_BACK) |
| return is_launcher_focused() |
| |
| # If Home is already focused, return early to avoid unnecessary work. |
| if is_launcher_focused(): |
| return |
| |
| self.StartActivity( |
| intent.Intent(action='android.intent.action.MAIN', |
| category='android.intent.category.HOME'), |
| blocking=True) |
| |
| if not is_launcher_focused(): |
| timeout_retry.WaitFor(dismiss_popups, wait_period=1) |
| |
| @decorators.WithTimeoutAndRetriesFromInstance() |
| def ForceStop(self, package, timeout=None, retries=None): |
| """Close the application. |
| |
| Args: |
| package: A string containing the name of the package to stop. |
| timeout: timeout in seconds |
| retries: number of retries |
| |
| Raises: |
| CommandTimeoutError on timeout. |
| DeviceUnreachableError on missing device. |
| """ |
| if self.GetApplicationPids(package): |
| self.RunShellCommand(['am', 'force-stop', package], check_return=True) |
| |
| @decorators.WithTimeoutAndRetriesFromInstance() |
| def ClearApplicationState( |
| self, package, permissions=None, timeout=None, retries=None): |
| """Clear all state for the given package. |
| |
| Args: |
| package: A string containing the name of the package to stop. |
| permissions: List of permissions to set after clearing data. |
| timeout: timeout in seconds |
| retries: number of retries |
| |
| Raises: |
| CommandTimeoutError on timeout. |
| DeviceUnreachableError on missing device. |
| """ |
| # Check that the package exists before clearing it for android builds below |
| # JB MR2. Necessary because calling pm clear on a package that doesn't exist |
| # may never return. |
| if ((self.build_version_sdk >= version_codes.JELLY_BEAN_MR2) |
| or self._GetApplicationPathsInternal(package)): |
| self.RunShellCommand(['pm', 'clear', package], check_return=True) |
| self.GrantPermissions(package, permissions) |
| |
| @decorators.WithTimeoutAndRetriesFromInstance() |
| def SendKeyEvent(self, keycode, timeout=None, retries=None): |
| """Sends a keycode to the device. |
| |
| See the devil.android.sdk.keyevent module for suitable keycode values. |
| |
| Args: |
| keycode: A integer keycode to send to the device. |
| timeout: timeout in seconds |
| retries: number of retries |
| |
| Raises: |
| CommandTimeoutError on timeout. |
| DeviceUnreachableError on missing device. |
| """ |
| 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): |
| """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 |
| all files in it are pushed to the device as needed. |
| Additionally, if delete_device_stale option is True, |
| files that exist on the device but don't exist on the host are deleted. |
| |
| Args: |
| host_device_tuples: A list of (host_path, device_path) tuples, where |
| |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. |
| 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. |
| """ |
| |
| 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 delete_device_stale and all_stale_files: |
| self.RunShellCommand(['rm', '-f'] + all_stale_files, check_return=True) |
| |
| if all_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() |
| |
| def _GetChangedAndStaleFiles(self, host_path, device_path, track_stale=False): |
| """Get files to push and delete |
| |
| 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) |
| |
| Returns: |
| a four-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. |
| """ |
| 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 = [] |
| 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) |
| |
| def calculate_host_checksums(): |
| return md5sum.CalculateHostMd5Sums([host_path]) |
| |
| 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]) |
| |
| sums = md5sum.CalculateDeviceMd5Sums(specific_device_paths, self) |
| |
| cache_entry = [ignore_other_files, sums] |
| self._cache['device_path_checksums'][device_path] = cache_entry |
| return dict(sums) |
| |
| 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) |
| 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() |
| |
| def cache_commit_func(): |
| 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 |
| |
| return (to_push, up_to_date, to_delete, cache_commit_func) |
| |
| def _ComputeDeviceChecksumsForApks(self, package_name): |
| ret = self._cache['package_apk_checksums'].get(package_name) |
| if ret is None: |
| device_paths = self._GetApplicationPathsInternal(package_name) |
| file_to_checksums = md5sum.CalculateDeviceMd5Sums(device_paths, self) |
| ret = set(file_to_checksums.values()) |
| self._cache['package_apk_checksums'][package_name] = ret |
| return ret |
| |
| def _ComputeStaleApks(self, package_name, host_apk_paths): |
| def calculate_host_checksums(): |
| return md5sum.CalculateHostMd5Sums(host_apk_paths) |
| |
| 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] |
| return stale_apks, set(host_checksums.values()) |
| |
| def _PushFilesImpl(self, host_device_tuples, files): |
| if not files: |
| return |
| |
| 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_file_count = 0 |
| for h, _ in host_device_tuples: |
| if os.path.isdir(h): |
| dir_file_count += sum(len(f) for _r, _d, f in os.walk(h)) |
| else: |
| dir_file_count += 1 |
| |
| 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) |
| |
| if (dir_push_duration < push_duration and dir_push_duration < zip_duration |
| # TODO(jbudorick): Resume directory pushing once clients have switched |
| # to 1.0.36-compatible syntax. |
| and False): |
| self._PushChangedFilesIndividually(host_device_tuples) |
| elif push_duration < zip_duration: |
| self._PushChangedFilesIndividually(files) |
| 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]): |
| self._PushChangedFilesIndividually(files) |
| |
| def _MaybeInstallCommands(self): |
| if self._commands_installed is None: |
| try: |
| if not install_commands.Installed(self): |
| install_commands.InstallCommands(self) |
| self._commands_installed = True |
| except device_errors.CommandFailedError as e: |
| logger.warning('unzip not available: %s', str(e)) |
| self._commands_installed = False |
| return self._commands_installed |
| |
| @staticmethod |
| def _ApproximateDuration(adb_calls, file_count, byte_count, is_zipping): |
| # We approximate the time to push a set of files to a device as: |
| # t = c1 * a + c2 * f + c3 + b / c4 + b / (c5 * c6), where |
| # t: total time (sec) |
| # c1: adb call time delay (sec) |
| # a: number of times adb is called (unitless) |
| # c2: push time delay (sec) |
| # f: number of files pushed via adb (unitless) |
| # c3: zip time delay (sec) |
| # c4: zip rate (bytes/sec) |
| # b: total number of bytes (bytes) |
| # c5: transfer rate (bytes/sec) |
| # c6: compression ratio (unitless) |
| |
| # All of these are approximations. |
| ADB_CALL_PENALTY = 0.1 # seconds |
| ADB_PUSH_PENALTY = 0.01 # seconds |
| ZIP_PENALTY = 2.0 # seconds |
| ZIP_RATE = 10000000.0 # bytes / second |
| TRANSFER_RATE = 2000000.0 # bytes / second |
| COMPRESSION_RATIO = 2.0 # unitless |
| |
| adb_call_time = ADB_CALL_PENALTY * adb_calls |
| adb_push_setup_time = ADB_PUSH_PENALTY * file_count |
| if is_zipping: |
| zip_time = ZIP_PENALTY + byte_count / ZIP_RATE |
| transfer_time = byte_count / (TRANSFER_RATE * COMPRESSION_RATIO) |
| else: |
| zip_time = 0 |
| transfer_time = byte_count / TRANSFER_RATE |
| return adb_call_time + adb_push_setup_time + zip_time + transfer_time |
| |
| def _PushChangedFilesIndividually(self, files): |
| for h, d in files: |
| self.adb.Push(h, d) |
| |
| def _PushChangedFilesZipped(self, files, dirs): |
| if not self._MaybeInstallCommands(): |
| return False |
| |
| with tempfile_ext.NamedTemporaryDirectory() as working_dir: |
| zip_path = os.path.join(working_dir, 'tmp.zip') |
| try: |
| zip_utils.WriteZipFile(zip_path, files) |
| except zip_utils.ZipFailedError: |
| return False |
| |
| self.NeedsSU() |
| with device_temp_file.DeviceTempFile( |
| self.adb, suffix='.zip') as device_temp: |
| self.adb.Push(zip_path, device_temp.name) |
| |
| 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, |
| env={'PATH': '%s:$PATH' % install_commands.BIN_DIR}, |
| check_return=True) |
| |
| return True |
| |
| # TODO(nednguyen): 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. |
| |
| Arguments are the same as PathExists. |
| """ |
| return self.PathExists(device_path, timeout=timeout, retries=retries) |
| |
| @decorators.WithTimeoutAndRetriesFromInstance() |
| def PathExists(self, device_paths, as_root=False, timeout=None, retries=None): |
| """Checks whether the given path(s) exists on the device. |
| |
| Args: |
| device_path: A string containing the absolute path to the file on the |
| device, or an iterable of paths to check. |
| as_root: Whether root permissions should be use to check for the existence |
| of the given path(s). |
| timeout: timeout in seconds |
| retries: number of retries |
| |
| Returns: |
| True if the all given paths exist on the device, False otherwise. |
| |
| Raises: |
| CommandTimeoutError on timeout. |
| DeviceUnreachableError on missing device. |
| """ |
| paths = device_paths |
| if isinstance(paths, basestring): |
| 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) |
| 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): |
| """Removes the given path(s) from the device. |
| |
| Args: |
| device_path: A string containing the absolute path to the file on the |
| device, or an iterable of paths to check. |
| force: Whether to remove the path(s) with force (-f). |
| recursive: Whether to remove any directories in the path(s) recursively. |
| as_root: Whether root permissions should be use to remove the given |
| path(s). |
| rename: Whether to rename the path(s) before removing to help avoid |
| filesystem errors. See https://stackoverflow.com/questions/11539657 |
| timeout: timeout in seconds |
| retries: number of retries |
| """ |
| def _RenamePath(path): |
| 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) |
| 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') |
| if recursive: |
| args.append('-r') |
| if isinstance(device_path, basestring): |
| args.append(device_path if not rename else _RenamePath(device_path)) |
| else: |
| args.extend( |
| device_path if not rename else [_RenamePath(p) for p in device_path]) |
| self.RunShellCommand(args, as_root=as_root, check_return=True) |
| |
| @decorators.WithTimeoutAndRetriesFromInstance() |
| def PullFile(self, device_path, host_path, timeout=None, retries=None): |
| """Pull a file from the device. |
| |
| Args: |
| device_path: A string containing the absolute path of the file to pull |
| from the device. |
| host_path: A string containing the absolute path of the destination on |
| the host. |
| timeout: timeout in seconds |
| retries: number of retries |
| |
| Raises: |
| CommandFailedError on failure. |
| CommandTimeoutError on timeout. |
| """ |
| # Create the base dir if it doesn't exist already |
| dirname = os.path.dirname(host_path) |
| if dirname and not os.path.exists(dirname): |
| os.makedirs(dirname) |
| self.adb.Pull(device_path, host_path) |
| |
| def _ReadFileWithPull(self, device_path): |
| try: |
| d = tempfile.mkdtemp() |
| host_temp_path = os.path.join(d, 'tmp_ReadFileWithPull') |
| self.adb.Pull(device_path, host_temp_path) |
| with open(host_temp_path, 'r') as host_temp: |
| return host_temp.read() |
| finally: |
| if os.path.exists(d): |
| shutil.rmtree(d) |
| |
| @decorators.WithTimeoutAndRetriesFromInstance() |
| 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: |
| device_path: A string containing the absolute path of the file to read |
| from the device. |
| as_root: A boolean indicating whether the read should be executed with |
| root privileges. |
| force_pull: A boolean indicating whether to force the operation to be |
| performed by pulling a file from the device. The default is, when the |
| contents are short, to retrieve the contents using cat instead. |
| timeout: timeout in seconds |
| retries: number of retries |
| |
| Returns: |
| The contents of |device_path| as a string. Contents are intepreted using |
| universal newlines, so the caller will see them encoded as '\n'. Also, |
| all lines will be terminated. |
| |
| Raises: |
| AdbCommandFailedError if the file can't be read. |
| 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 device_temp_file.DeviceTempFile(self.adb) as device_temp: |
| cmd = 'SRC=%s DEST=%s;cp "$SRC" "$DEST" && chmod 666 "$DEST"' % ( |
| cmd_helper.SingleQuote(device_path), |
| cmd_helper.SingleQuote(device_temp.name)) |
| self.RunShellCommand(cmd, shell=True, as_root=True, check_return=True) |
| return self._ReadFileWithPull(device_temp.name) |
| else: |
| return self._ReadFileWithPull(device_path) |
| |
| def _WriteFileWithPush(self, device_path, contents): |
| with tempfile.NamedTemporaryFile() as host_temp: |
| host_temp.write(contents) |
| host_temp.flush() |
| 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): |
| """Writes |contents| to a file on the device. |
| |
| Args: |
| device_path: A string containing the absolute path to the file to write |
| on the device. |
| contents: A string containing the data to write to the device. |
| as_root: A boolean indicating whether the write should be executed with |
| root privileges (if available). |
| force_push: A boolean indicating whether to force the operation to be |
| performed by pushing a file to the device. The default is, when the |
| contents are short, to pass the contents using a shell script instead. |
| timeout: timeout in seconds |
| retries: number of retries |
| |
| Raises: |
| CommandFailedError if the file could not be written on the device. |
| CommandTimeoutError on timeout. |
| DeviceUnreachableError on missing device. |
| """ |
| if not force_push and len(contents) < self._MAX_ADB_COMMAND_LENGTH: |
| # If the contents are small, for efficieny we write the contents with |
| # a shell command rather than pushing a file. |
| cmd = 'echo -n %s > %s' % (cmd_helper.SingleQuote(contents), |
| cmd_helper.SingleQuote(device_path)) |
| self.RunShellCommand(cmd, shell=True, as_root=as_root, check_return=True) |
| elif as_root and self.NeedsSU(): |
| # Adb does not allow to "push with su", so we first push to a temp file |
| # on a safe location, and then copy it to the desired location with su. |
| with device_temp_file.DeviceTempFile(self.adb) as device_temp: |
| self._WriteFileWithPush(device_temp.name, contents) |
| # Here we need 'cp' rather than 'mv' because the temp and |
| # 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) |
| else: |
| # If root is not needed, we can push directly to the desired location. |
| self._WriteFileWithPush(device_path, contents) |
| |
| 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) |
| if output and output[0].startswith('total '): |
| output.pop(0) # pylint: disable=maybe-no-member |
| |
| entries = [] |
| for line in output: |
| m = _LONG_LS_OUTPUT_RE.match(line) |
| if m: |
| if m.group('filename') not in ['.', '..']: |
| item = m.groupdict() |
| # A change in toybox is causing recent Android versions to escape |
| # spaces in file names. Here we just unquote those spaces. If we |
| # later find more essoteric characters in file names, a more careful |
| # unquoting mechanism may be needed. But hopefully not. |
| # See: https://goo.gl/JAebZj |
| item['filename'] = item['filename'].replace('\\ ', ' ') |
| entries.append(item) |
| else: |
| logger.info('Skipping: %s', line) |
| |
| return entries |
| |
| def ListDirectory(self, device_path, as_root=False, **kwargs): |
| """List all files on a device directory. |
| |
| Mirroring os.listdir (and most client expectations) the resulting list |
| does not include the special entries '.' and '..' even if they are present |
| in the directory. |
| |
| Args: |
| device_path: A string containing the path of the directory on the device |
| to list. |
| as_root: A boolean indicating whether the to use root privileges to list |
| the directory contents. |
| timeout: timeout in seconds |
| retries: number of retries |
| |
| Returns: |
| A list of filenames for all entries contained in the directory. |
| |
| Raises: |
| AdbCommandFailedError if |device_path| does not specify a valid and |
| accessible directory in the device. |
| CommandTimeoutError on timeout. |
| DeviceUnreachableError on missing device. |
| """ |
| entries = self._ParseLongLsOutput(device_path, as_root=as_root, **kwargs) |
| return [d['filename'] for d in entries] |
| |
| def StatDirectory(self, device_path, as_root=False, **kwargs): |
| """List file and stat info for all entries on a device directory. |
| |
| Implementation notes: this is currently implemented by parsing the output |
| of 'ls -a -l' on the device. Whether possible and convenient, we attempt to |
| make parsing strict and return values mirroring those of the standard |os| |
| and |stat| Python modules. |
| |
| Mirroring os.listdir (and most client expectations) the resulting list |
| does not include the special entries '.' and '..' even if they are present |
| in the directory. |
| |
| Args: |
| device_path: A string containing the path of the directory on the device |
| to list. |
| as_root: A boolean indicating whether the to use root privileges to list |
| the directory contents. |
| timeout: timeout in seconds |
| retries: number of retries |
| |
| Returns: |
| A list of dictionaries, each containing the following keys: |
| filename: A string with the file name. |
| st_mode: File permissions, use the stat module to interpret these. |
| st_nlink: Number of hard links (may be missing). |
| st_owner: A string with the user name of the owner. |
| st_group: A string with the group name of the owner. |
| st_rdev_pair: Device type as (major, minior) (only if inode device). |
| st_size: Size of file, in bytes (may be missing for non-regular files). |
| st_mtime: Time of most recent modification, in seconds since epoch |
| (although resolution is in minutes). |
| symbolic_link_to: If entry is a symbolic link, path where it points to; |
| missing otherwise. |
| |
| Raises: |
| AdbCommandFailedError if |device_path| does not specify a valid and |
| accessible directory in the device. |
| CommandTimeoutError on timeout. |
| DeviceUnreachableError on missing device. |
| """ |
| entries = self._ParseLongLsOutput(device_path, as_root=as_root, **kwargs) |
| for d in entries: |
| for key, value in d.items(): |
| if value is None: |
| del d[key] # Remove missing fields. |
| d['st_mode'] = _ParseModeString(d['st_mode']) |
| d['st_mtime'] = calendar.timegm( |
| time.strptime(d['st_mtime'], _LS_DATE_FORMAT)) |
| for key in ['st_nlink', 'st_size', 'st_rdev_major', 'st_rdev_minor']: |
| if key in d: |
| d[key] = int(d[key]) |
| if 'st_rdev_major' in d and 'st_rdev_minor' in d: |
| d['st_rdev_pair'] = (d.pop('st_rdev_major'), d.pop('st_rdev_minor')) |
| return entries |
| |
| def StatPath(self, device_path, as_root=False, **kwargs): |
| """Get the stat attributes of a file or directory on the device. |
| |
| Args: |
| device_path: A string containing the path of a file or directory from |
| which to get attributes. |
| as_root: A boolean indicating whether the to use root privileges to |
| access the file information. |
| timeout: timeout in seconds |
| retries: number of retries |
| |
| Returns: |
| A dictionary with the stat info collected; see StatDirectory for details. |
| |
| Raises: |
| CommandFailedError if device_path cannot be found on the device. |
| CommandTimeoutError on timeout. |
| DeviceUnreachableError on missing device. |
| """ |
| dirname, filename = posixpath.split(posixpath.normpath(device_path)) |
| for entry in self.StatDirectory(dirname, as_root=as_root, **kwargs): |
| if entry['filename'] == filename: |
| return entry |
| raise device_errors.CommandFailedError( |
| 'Cannot find file or directory: %r' % device_path, str(self)) |
| |
| def FileSize(self, device_path, as_root=False, **kwargs): |
| """Get the size of a file on the device. |
| |
| Note: This is implemented by parsing the output of the 'ls' command on |
| the device. On some Android versions, when passing a directory or special |
| file, the size is *not* reported and this function will throw an exception. |
| |
| Args: |
| device_path: A string containing the path of a file on the device. |
| as_root: A boolean indicating whether the to use root privileges to |
| access the file information. |
| timeout: timeout in seconds |
| retries: number of retries |
| |
| Returns: |
| The size of the file in bytes. |
| |
| Raises: |
| CommandFailedError if device_path cannot be found on the device, or |
| its size cannot be determited for some reason. |
| CommandTimeoutError on timeout. |
| DeviceUnreachableError on missing device. |
| """ |
| entry = self.StatPath(device_path, as_root=as_root, **kwargs) |
| try: |
| return entry['st_size'] |
| except KeyError: |
| raise device_errors.CommandFailedError( |
| 'Could not determine the size of: %s' % device_path, str(self)) |
| |
| @decorators.WithTimeoutAndRetriesFromInstance() |
| def SetJavaAsserts(self, enabled, timeout=None, retries=None): |
| """Enables or disables Java asserts. |
| |
| Args: |
| enabled: A boolean indicating whether Java asserts should be enabled |
| or disabled. |
| timeout: timeout in seconds |
| retries: number of retries |
| |
| Returns: |
| True if the device-side property changed and a restart is required as a |
| result, False otherwise. |
| |
| Raises: |
| CommandTimeoutError on timeout. |
| """ |
| def find_property(lines, property_name): |
| for index, line in enumerate(lines): |
| if line.strip() == '': |
| continue |
| key_value = tuple(s.strip() for s in line.split('=', 1)) |
| if len(key_value) != 2: |
| continue |
| key, value = key_value |
| if key == property_name: |
| return index, value |
| return None, '' |
| |
| new_value = 'all' if enabled else '' |
| |
| # First ensure the desired property is persisted. |
| try: |
| properties = self.ReadFile(self.LOCAL_PROPERTIES_PATH).splitlines() |
| except device_errors.CommandFailedError: |
| properties = [] |
| index, value = find_property(properties, self.JAVA_ASSERT_PROPERTY) |
| if new_value != value: |
| if new_value: |
| new_line = '%s=%s' % (self.JAVA_ASSERT_PROPERTY, new_value) |
| if index is None: |
| properties.append(new_line) |
| else: |
| properties[index] = new_line |
| else: |
| assert index is not None # since new_value == '' and new_value != value |
| properties.pop(index) |
| self.WriteFile(self.LOCAL_PROPERTIES_PATH, _JoinLines(properties)) |
| |
| # Next, check the current runtime value is what we need, and |
| # if not, set it and report that a reboot is required. |
| value = self.GetProp(self.JAVA_ASSERT_PROPERTY) |
| if new_value != value: |
| self.SetProp(self.JAVA_ASSERT_PROPERTY, new_value) |
| return True |
| else: |
| return False |
| |
| def GetLanguage(self, cache=False): |
| """Returns the language setting on the device. |
| Args: |
| cache: Whether to use cached properties when available. |
| """ |
| return self.GetProp('persist.sys.language', cache=cache) |
| |
| def GetCountry(self, cache=False): |
| """Returns the country setting on the device. |
| |
| Args: |
| cache: Whether to use cached properties when available. |
| """ |
| return self.GetProp('persist.sys.country', cache=cache) |
| |
| @property |
| 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', |
| } |
| return DPI_TO_DENSITY.get(self.pixel_density, 'tvdpi') |
| |
| @property |
| def pixel_density(self): |
| return int(self.GetProp('ro.sf.lcd_density', cache=True)) |
| |
| @property |
| def build_description(self): |
| """Returns the build description of the system. |
| |
| For example: |
| nakasi-user 4.4.4 KTU84P 1227136 release-keys |
| """ |
| return self.GetProp('ro.build.description', cache=True) |
| |
| @property |
| def build_fingerprint(self): |
| """Returns the build fingerprint of the system. |
| |
| For example: |
| google/nakasi/grouper:4.4.4/KTU84P/1227136:user/release-keys |
| """ |
| return self.GetProp('ro.build.fingerprint', cache=True) |
| |
| @property |
| def build_id(self): |
| """Returns the build ID of the system (e.g. 'KTU84P').""" |
| return self.GetProp('ro.build.id', cache=True) |
| |
| @property |
| def build_product(self): |
| """Returns the build product of the system (e.g. 'grouper').""" |
| return self.GetProp('ro.build.product', 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) |
| |
| @property |
| def build_version_sdk(self): |
| """Returns the build version sdk of the system as a number (e.g. 19). |
| |
| For version code numbers see: |
| http://developer.android.com/reference/android/os/Build.VERSION_CODES.html |
| |
| For named constants see devil.android.sdk.version_codes |
| |
| Raises: |
| CommandFailedError if the build version sdk is not a number. |
| """ |
| value = self.GetProp('ro.build.version.sdk', cache=True) |
| try: |
| return int(value) |
| except ValueError: |
| raise device_errors.CommandFailedError( |
| 'Invalid build version sdk: %r' % value) |
| |
| @property |
| def product_cpu_abi(self): |
| """Returns the product cpu abi of the device (e.g. 'armeabi-v7a').""" |
| return self.GetProp('ro.product.cpu.abi', cache=True) |
| |
| @property |
| def product_model(self): |
| """Returns the name of the product model (e.g. 'Nexus 7').""" |
| return self.GetProp('ro.product.model', cache=True) |
| |
| @property |
| def product_name(self): |
| """Returns the product name of the device (e.g. 'nakasi').""" |
| return self.GetProp('ro.product.name', cache=True) |
| |
| @property |
| def product_board(self): |
| """Returns the product board name of the device (e.g. 'shamu').""" |
| return self.GetProp('ro.product.board', cache=True) |
| |
| def _EnsureCacheInitialized(self): |
| """Populates cache token, runs getprop and fetches $EXTERNAL_STORAGE.""" |
| if self._cache['token']: |
| return |
| with self._cache_lock: |
| if self._cache['token']: |
| return |
| # 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' |
| ) |
| output = self.RunShellCommand( |
| cmd, shell=True, check_return=True, large_output=True) |
| # Error-checking for this existing is done in GetExternalStoragePath(). |
| self._cache['external_storage'] = output[0] |
| self._cache['prev_token'] = output[1] |
| output = output[2:] |
| |
| prop_cache = self._cache['getprop'] |
| prop_cache.clear() |
| for key, value in _GETPROP_RE.findall(''.join(output)): |
| prop_cache[key] = value |
| self._cache['token'] = token |
| |
| @decorators.WithTimeoutAndRetriesFromInstance() |
| def GetProp(self, property_name, cache=False, timeout=None, retries=None): |
| """Gets a property from the device. |
| |
| Args: |
| property_name: A string containing the name of the property to get from |
| the device. |
| cache: Whether to use cached properties when available. |
| timeout: timeout in seconds |
| retries: number of retries |
| |
| Returns: |
| The value of the device's |property_name| property. |
| |
| Raises: |
| CommandTimeoutError on timeout. |
| """ |
| 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 |
| # properties. So, when caching we always query all properties. |
| self._EnsureCacheInitialized() |
| 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) |
| 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, |
| retries=None): |
| """Sets a property on the device. |
| |
| Args: |
| property_name: A string containing the name of the property to set on |
| the device. |
| value: A string containing the value to set to the property on the |
| device. |
| check: A boolean indicating whether to check that the property was |
| successfully set on the device. |
| timeout: timeout in seconds |
| retries: number of retries |
| |
| Raises: |
| CommandFailedError if check is true and the property was not correctly |
| 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(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. |
| 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)) |
| |
| @decorators.WithTimeoutAndRetriesFromInstance() |
| def GetABI(self, timeout=None, retries=None): |
| """Gets the device main ABI. |
| |
| Args: |
| timeout: timeout in seconds |
| retries: number of retries |
| |
| Returns: |
| The device's main ABI name. |
| |
| Raises: |
| CommandTimeoutError on timeout. |
| """ |
| return self.GetProp('ro.product.cpu.abi', cache=True) |
| |
| def _GetPsOutput(self, pattern): |
| """Runs |ps| command on the device and returns its output, |
| |
| This private method abstracts away differences between Android verions for |
| calling |ps|, and implements support for filtering the output by a given |
| |pattern|, but does not do any output parsing. |
| """ |
| try: |
| ps_cmd = 'ps' |
| # ps behavior was changed in Android above N, http://crbug.com/686716 |
| if (self.build_version_sdk >= version_codes.NOUGAT_MR1 |
| and self.build_id[0] > 'N'): |
| ps_cmd = 'ps -e' |
| if pattern: |
| return self._RunPipedShellCommand( |
| '%s | grep -F %s' % (ps_cmd, cmd_helper.SingleQuote(pattern))) |
| else: |
| return self.RunShellCommand( |
| ps_cmd.split(), check_return=True, large_output=True) |
| except device_errors.AdbShellCommandFailedError as e: |
| if e.status and isinstance(e.status, list) and not e.status[0]: |
| # If ps succeeded but grep failed, there were no processes with the |
| # given name. |
| return [] |
| else: |
| raise |
| |
| @decorators.WithTimeoutAndRetriesFromInstance() |
| def ListProcesses(self, process_name=None, timeout=None, retries=None): |
| """Returns a list of tuples with info about processes on the device. |
| |
| This essentially parses the output of the |ps| command into convenient |
| ProcessInfo tuples. |
| |
| Args: |
| process_name: A string used to filter the returned processes. If given, |
| only processes whose name have this value as a substring |
| will be returned. |
| timeout: timeout in seconds |
| retries: number of retries |
| |
| Returns: |
| A list of ProcessInfo tuples with |name|, |pid|, and |ppid| fields. |
| """ |
| process_name = process_name or '' |
| processes = [] |
| for line in self._GetPsOutput(process_name): |
| row = line.split() |
| try: |
| row = {k: row[i] for k, i in _PS_COLUMNS.iteritems()} |
| if row['pid'] == 'PID' or process_name not in row['name']: |
| # Skip over header and non-matching processes. |
| continue |
| row['pid'] = int(row['pid']) |
| row['ppid'] = int(row['ppid']) |
| except StandardError: # e.g. IndexError, TypeError, ValueError. |
| logging.warning('failed to parse ps line: %r', line) |
| continue |
| processes.append(ProcessInfo(**row)) |
| return processes |
| |
| # TODO(#4103): Remove after migrating clients to ListProcesses. |
| @decorators.WithTimeoutAndRetriesFromInstance() |
| def GetPids(self, process_name=None, timeout=None, retries=None): |
| """Returns the PIDs of processes containing the given name as substring. |
| |
| DEPRECATED |
| |
| Note that the |process_name| is often the package name. |
| |
| Args: |
| process_name: A string containing the process name to get the PIDs for. |
| If missing returns PIDs for all processes. |
| timeout: timeout in seconds |
| retries: number of retries |
| |
| Returns: |
| A dict mapping process name to a list of PIDs for each process that |
| contained the provided |process_name|. |
| |
| Raises: |
| CommandTimeoutError on timeout. |
| DeviceUnreachableError on missing device. |
| """ |
| procs_pids = collections.defaultdict(list) |
| for p in self.ListProcesses(process_name): |
| procs_pids[p.name].append(str(p.pid)) |
| return procs_pids |
| |
| @decorators.WithTimeoutAndRetriesFromInstance() |
| 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. |
| |
| Args: |
| process_name: A string containing the process name to get the PIDs for. |
| at_most_one: A boolean indicating that at most one PID is expected to |
| be found. |
| timeout: timeout in seconds |
| retries: number of retries |
| |
| Returns: |
| A list of the PIDs for the named process. If at_most_one=True returns |
| the single PID found or None otherwise. |
| |
| Raises: |
| CommandFailedError if at_most_one=True and more than one PID is found |
| for the named process. |
| CommandTimeoutError on timeout. |
| DeviceUnreachableError on missing device. |
| """ |
| 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), |
| device_serial=str(self)) |
| return pids[0] if pids else None |
| else: |
| return pids |
| |
| @decorators.WithTimeoutAndRetriesFromInstance() |
| def GetEnforce(self, timeout=None, retries=None): |
| """Get the current mode of SELinux. |
| |
| Args: |
| timeout: timeout in seconds |
| retries: number of retries |
| |
| Returns: |
| True (enforcing), False (permissive), or None (disabled). |
| |
| Raises: |
| CommandFailedError on failure. |
| CommandTimeoutError on timeout. |
| DeviceUnreachableError on missing device. |
| """ |
| 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) |
| return _SELINUX_MODE[output] |
| |
| @decorators.WithTimeoutAndRetriesFromInstance() |
| def SetEnforce(self, enabled, timeout=None, retries=None): |
| """Modify the mode SELinux is running in. |
| |
| Args: |
| enabled: a boolean indicating whether to put SELinux in encorcing mode |
| (if True), or permissive mode (otherwise). |
| timeout: timeout in seconds |
| retries: number of retries |
| |
| Raises: |
| CommandFailedError on failure. |
| CommandTimeoutError on timeout. |
| DeviceUnreachableError on missing device. |
| """ |
| self.RunShellCommand( |
| ['setenforce', '1' if int(enabled) else '0'], as_root=True, |
| check_return=True) |
| |
| @decorators.WithTimeoutAndRetriesFromInstance() |
| def TakeScreenshot(self, host_path=None, timeout=None, retries=None): |
| """Takes a screenshot of the device. |
| |
| Args: |
| host_path: A string containing the path on the host to save the |
| screenshot to. If None, a file name in the current |
| directory will be generated. |
| timeout: timeout in seconds |
| retries: number of retries |
| |
| Returns: |
| The name of the file on the host to which the screenshot was saved. |
| |
| Raises: |
| CommandFailedError on failure. |
| CommandTimeoutError on timeout. |
| DeviceUnreachableError on missing device. |
| """ |
| if not host_path: |
| 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) |
| self.PullFile(device_tmp.name, host_path) |
| return host_path |
| |
| @decorators.WithTimeoutAndRetriesFromInstance() |
| def DismissCrashDialogIfNeeded(self, timeout=None, retries=None): |
| """Dismiss the error/ANR dialog if present. |
| |
| 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): |
| match = re.match(_CURRENT_FOCUS_CRASH_RE, line) |
| if match: |
| break |
| return match |
| |
| match = _FindFocusedWindow() |
| if not match: |
| return None |
| package = match.group(2) |
| logger.warning('Trying to dismiss %s dialog for %s', *match.groups()) |
| self.SendKeyEvent(keyevent.KEYCODE_DPAD_RIGHT) |
| self.SendKeyEvent(keyevent.KEYCODE_DPAD_RIGHT) |
| self.SendKeyEvent(keyevent.KEYCODE_ENTER) |
| match = _FindFocusedWindow() |
| if match: |
| logger.error('Still showing a %s dialog for %s', *match.groups()) |
| return package |
| |
| def GetLogcatMonitor(self, *args, **kwargs): |
| """Returns a new LogcatMonitor associated with this device. |
| |
| Parameters passed to this function are passed directly to |
| |logcat_monitor.LogcatMonitor| and are documented there. |
| """ |
| return logcat_monitor.LogcatMonitor(self.adb, *args, **kwargs) |
| |
| def GetClientCache(self, client_name): |
| """Returns client cache.""" |
| if client_name not in self._client_caches: |
| self._client_caches[client_name] = {} |
| return self._client_caches[client_name] |
| |
| def _ClearCache(self): |
| """Clears all caches.""" |
| for client in self._client_caches: |
| self._client_caches[client].clear() |
| self._cache = { |
| # Map of packageId -> list of on-device .apk paths |
| 'package_apk_paths': {}, |
| # Set of packageId that were loaded from LoadCacheData and not yet |
| # verified. |
| 'package_apk_paths_to_verify': set(), |
| # Map of packageId -> set of on-device .apk checksums |
| 'package_apk_checksums': {}, |
| # Map of property_name -> value |
| 'getprop': {}, |
| # Map of device_path -> [ignore_other_files, map of 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, |
| } |
| |
| @decorators.WithTimeoutAndRetriesFromInstance() |
| def LoadCacheData(self, data, timeout=None, retries=None): |
| """Initializes the cache from data created using DumpCacheData. |
| |
| The cache is used only if its token matches the one found on the device. |
| This prevents a stale cache from being used (which can happen when sharing |
| devices). |
| |
| Args: |
| data: A previously serialized cache (string). |
| timeout: timeout in seconds |
| retries: number of retries |
| |
| Returns: |
| Whether the cache was loaded. |
| """ |
| obj = json.loads(data) |
| self._EnsureCacheInitialized() |
| given_token = obj.get('token') |
| if not given_token or self._cache['prev_token'] != given_token: |
| logger.warning('Stale cache detected. Not using it.') |
| return False |
| |
| self._cache['package_apk_paths'] = obj.get('package_apk_paths', {}) |
| # When using a cache across script invokations, verify that apps have |
| # not been uninstalled. |
| self._cache['package_apk_paths_to_verify'] = set( |
| self._cache['package_apk_paths'].iterkeys()) |
| |
| package_apk_checksums = obj.get('package_apk_checksums', {}) |
| for k, v in package_apk_checksums.iteritems(): |
| package_apk_checksums[k] = set(v) |
| self._cache['package_apk_checksums'] = package_apk_checksums |
| device_path_checksums = obj.get('device_path_checksums', {}) |
| self._cache['device_path_checksums'] = device_path_checksums |
| return True |
| |
| @decorators.WithTimeoutAndRetriesFromInstance() |
| def DumpCacheData(self, timeout=None, retries=None): |
| """Dumps the current cache state to a string. |
| |
| Args: |
| timeout: timeout in seconds |
| retries: number of retries |
| |
| Returns: |
| A serialized cache as a string. |
| """ |
| self._EnsureCacheInitialized() |
| obj = {} |
| obj['token'] = self._cache['token'] |
| obj['package_apk_paths'] = self._cache['package_apk_paths'] |
| obj['package_apk_checksums'] = self._cache['package_apk_checksums'] |
| # JSON can't handle sets. |
| for k, v in obj['package_apk_checksums'].iteritems(): |
| obj['package_apk_checksums'][k] = list(v) |
| obj['device_path_checksums'] = self._cache['device_path_checksums'] |
| return json.dumps(obj, separators=(',', ':')) |
| |
| @classmethod |
| def parallel(cls, devices, async=False): |
| """Creates a Parallelizer to operate over the provided list of devices. |
| |
| Args: |
| devices: A list of either DeviceUtils instances or objects from |
| from which DeviceUtils instances can be constructed. If None, |
| all attached devices will be used. |
| async: If true, returns a Parallelizer that runs operations |
| asynchronously. |
| |
| Returns: |
| A Parallelizer operating over |devices|. |
| """ |
| devices = [d if isinstance(d, cls) else cls(d) for d in devices] |
| if async: |
| return parallelizer.Parallelizer(devices) |
| else: |
| return parallelizer.SyncParallelizer(devices) |
| |
| @classmethod |
| def HealthyDevices(cls, blacklist=None, device_arg='default', retry=True, |
| **kwargs): |
| """Returns a list of DeviceUtils instances. |
| |
| Returns a list of DeviceUtils instances that are attached, not blacklisted, |
| 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 |
| 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 |
| NoDevicesError. |
| [] -> Returns all devices, unless $ANDROID_SERIAL is set. |
| None -> Use $ANDROID_SERIAL if set, otherwise looks for a single |
| attached device. Raises an exception if multiple devices are |
| attached. |
| 'serial' -> Returns an instance for the given serial, if not |
| blacklisted. |
| ['A', 'B', ...] -> Returns instances for the subset that is not |
| blacklisted. |
| retry: If true, will attempt to restart adb server and query it again if |
| no devices are found. |
| A device serial, or a list of device serials (optional). |
| |
| Returns: |
| A list of DeviceUtils instances. |
| |
| Raises: |
| NoDevicesError: Raised when no non-blacklisted devices exist and |
| device_arg is passed. |
| MultipleDevicesError: Raise when multiple devices exist, but |device_arg| |
| is None. |
| """ |
| allow_no_devices = False |
| if device_arg == 'default': |
| allow_no_devices = True |
| device_arg = () |
| |
| select_multiple = True |
| if not (isinstance(device_arg, tuple) or isinstance(device_arg, list)): |
| select_multiple = False |
| if device_arg: |
| device_arg = (device_arg,) |
| |
| blacklisted_devices = blacklist.Read() if blacklist 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,) |
| |
| def blacklisted(serial): |
| if serial in blacklisted_devices: |
| logger.warning('Device %s is blacklisted.', serial) |
| return True |
| return False |
| |
| def _get_devices(): |
| if device_arg: |
| devices = [cls(x, **kwargs) for x in device_arg if not blacklisted(x)] |
| else: |
| devices = [] |
| for adb in adb_wrapper.AdbWrapper.Devices(): |
| if not blacklisted(adb.GetDeviceSerial()): |
| devices.append(cls(_CreateAdbWrapper(adb), **kwargs)) |
| |
| if len(devices) == 0 and not allow_no_devices: |
| raise device_errors.NoDevicesError() |
| if len(devices) > 1 and not select_multiple: |
| raise device_errors.MultipleDevicesError(devices) |
| return sorted(devices) |
| |
| try: |
| return _get_devices() |
| except device_errors.NoDevicesError: |
| if not retry: |
| raise |
| logger.warning( |
| 'No devices found. Will try again after restarting adb server.') |
| RestartServer() |
| return _get_devices() |
| |
| @decorators.WithTimeoutAndRetriesFromInstance() |
| def RestartAdbd(self, timeout=None, retries=None): |
| 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.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: |
| return |
| |
| permissions = set( |
| p for p in permissions if not _PERMISSIONS_BLACKLIST_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' |
| ]).format( |
| package=cmd_helper.SingleQuote(package), |
| permissions=' '.join( |
| cmd_helper.SingleQuote(p) for p in sorted(permissions)), |
| sep=_SHELL_OUTPUT_SEPARATOR) |
| |
| logger.info('Setting permissions for %s.', package) |
| res = self.RunShellCommand( |
| 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)] |
| |
| if failures: |
| logger.warning( |
| 'Failed to grant some permissions. Blacklist 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) |
| if m: |
| error_msg = m.group(0) |
| elif len(output) > 200: |
| error_msg = repr(output[:200]) + ' (truncated)' |
| else: |
| error_msg = repr(output) |
| logger.warning('- %s: %s', permission, error_msg) |
| |
| @decorators.WithTimeoutAndRetriesFromInstance() |
| def IsScreenOn(self, timeout=None, retries=None): |
| """Determines if screen is on. |
| |
| Dumpsys input_method exposes screen on/off state. Below is an explination of |
| the states. |
| |
| Pre-L: |
| On: mScreenOn=true |
| Off: mScreenOn=false |
| L+: |
| On: mInteractive=true |
| Off: mInteractive=false |
| |
| Returns: |
| True if screen is on, false if it is off. |
| |
| Raises: |
| device_errors.CommandFailedError: If screen state cannot be found. |
| """ |
| if self.build_version_sdk < version_codes.LOLLIPOP: |
| input_check = 'mScreenOn' |
| check_value = 'mScreenOn=true' |
| else: |
| input_check = 'mInteractive' |
| check_value = 'mInteractive=true' |
| 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)) |
| return check_value in dumpsys_out[0] |
| |
| @decorators.WithTimeoutAndRetriesFromInstance() |
| def SetScreen(self, on, timeout=None, retries=None): |
| """Turns screen on and off. |
| |
| Args: |
| on: bool to decide state to switch to. True = on False = off. |
| """ |
| def screen_test(): |
| return self.IsScreenOn() == on |
| |
| if screen_test(): |
| logger.info('Screen already in expected state.') |
| return |
| self.SendKeyEvent(keyevent.KEYCODE_POWER) |
| timeout_retry.WaitFor(screen_test, wait_period=1) |