Index: build/android/pylib/device/device_utils.py |
diff --git a/build/android/pylib/device/device_utils.py b/build/android/pylib/device/device_utils.py |
index c8b471831e62aec343c705afb12bd3095bd8ea24..3a6563e8442e7783751d3f52f6a2cdd19661afe5 100644 |
--- a/build/android/pylib/device/device_utils.py |
+++ b/build/android/pylib/device/device_utils.py |
@@ -26,6 +26,7 @@ import pylib.android_commands |
from pylib import cmd_helper |
from pylib import constants |
from pylib import device_signal |
+from pylib.constants import keyevent |
from pylib.device import adb_wrapper |
from pylib.device import decorators |
from pylib.device import device_blacklist |
@@ -33,6 +34,7 @@ from pylib.device import device_errors |
from pylib.device import intent |
from pylib.device import logcat_monitor |
from pylib.device.commands import install_commands |
+from pylib.sdk import split_select |
from pylib.utils import apk_helper |
from pylib.utils import base_error |
from pylib.utils import device_temp_file |
@@ -135,6 +137,8 @@ class DeviceUtils(object): |
_MAX_ADB_COMMAND_LENGTH = 512 |
_MAX_ADB_OUTPUT_LENGTH = 32768 |
+ _LAUNCHER_FOCUSED_RE = re.compile( |
+ '\s*mCurrentFocus.*(Launcher|launcher).*') |
_VALID_SHELL_VARIABLE = re.compile('^[a-zA-Z_][a-zA-Z0-9_]*$') |
# Property in /data/local.prop that controls Java assertions. |
@@ -339,14 +343,14 @@ class DeviceUtils(object): |
return value |
@decorators.WithTimeoutAndRetriesFromInstance() |
- def GetApplicationPath(self, package, timeout=None, retries=None): |
- """Get the path of the installed apk on the device for the given package. |
+ 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: |
- Path to the apk on the device if it exists, None otherwise. |
+ List of paths to the apks on the device for the given package. |
""" |
# 'pm path' is liable to incorrectly exit with a nonzero number starting |
# in Lollipop. |
@@ -354,14 +358,37 @@ class DeviceUtils(object): |
# released to put an upper bound on this. |
should_check_return = (self.build_version_sdk < |
constants.ANDROID_SDK_VERSION_CODES.LOLLIPOP) |
- output = self.RunShellCommand(['pm', 'path', package], single_line=True, |
- check_return=should_check_return) |
- if not output: |
- return None |
- if not output.startswith('package:'): |
- raise device_errors.CommandFailedError('pm path returned: %r' % output, |
- str(self)) |
- return output[len('package:'):] |
+ output = self.RunShellCommand( |
+ ['pm', 'path', package], check_return=should_check_return) |
+ apks = [] |
+ for line in output: |
+ if not line.startswith('package:'): |
+ raise device_errors.CommandFailedError( |
+ 'pm path returned: %r' % '\n'.join(output), str(self)) |
+ apks.append(line[len('package:'):]) |
+ return apks |
+ |
+ @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, or None if the package doesn't exist on the |
+ device. |
+ """ |
+ try: |
+ output = self._RunPipedShellCommand( |
+ 'pm dump %s | grep dataDir=' % cmd_helper.SingleQuote(package)) |
+ for line in output: |
+ _, _, dataDir = line.partition('dataDir=') |
+ if dataDir: |
+ return dataDir |
+ except device_errors.CommandFailedError: |
+ logging.exception('Could not find data directory for %s', package) |
+ return None |
@decorators.WithTimeoutAndRetriesFromInstance() |
def WaitUntilFullyBooted(self, wifi=False, timeout=None, retries=None): |
@@ -391,7 +418,7 @@ class DeviceUtils(object): |
def pm_ready(): |
try: |
- return self.GetApplicationPath('android') |
+ return self.GetApplicationPaths('android') |
except device_errors.CommandFailedError: |
return False |
@@ -461,9 +488,15 @@ class DeviceUtils(object): |
DeviceUnreachableError on missing device. |
""" |
package_name = apk_helper.GetPackageName(apk_path) |
- device_path = self.GetApplicationPath(package_name) |
- if device_path is not None: |
- should_install = bool(self._GetChangedFilesImpl(apk_path, device_path)) |
+ device_paths = self.GetApplicationPaths(package_name) |
+ if device_paths: |
+ if len(device_paths) > 1: |
+ logging.warning( |
+ 'Installing single APK (%s) when split APKs (%s) are currently ' |
+ 'installed.', apk_path, ' '.join(device_paths)) |
+ (files_to_push, _) = self._GetChangedAndStaleFiles( |
+ apk_path, device_paths[0]) |
+ should_install = bool(files_to_push) |
if should_install and not reinstall: |
self.adb.Uninstall(package_name) |
else: |
@@ -471,6 +504,62 @@ class DeviceUtils(object): |
if should_install: |
self.adb.Install(apk_path, reinstall=reinstall) |
+ @decorators.WithTimeoutAndRetriesDefaults( |
+ INSTALL_DEFAULT_TIMEOUT, |
+ INSTALL_DEFAULT_RETRIES) |
+ def InstallSplitApk(self, base_apk, split_apks, reinstall=False, |
+ timeout=None, retries=None): |
+ """Install a split APK. |
+ |
+ Noop if all of the APK splits are already installed. |
+ |
+ Args: |
+ base_apk: A string of the path to the base APK. |
+ split_apks: A list of strings of paths of all of the APK splits. |
+ reinstall: A boolean indicating if we should keep any existing app data. |
+ 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._CheckSdkLevel(constants.ANDROID_SDK_VERSION_CODES.LOLLIPOP) |
+ |
+ all_apks = [base_apk] + split_select.SelectSplits( |
+ self, base_apk, split_apks) |
+ package_name = apk_helper.GetPackageName(base_apk) |
+ device_apk_paths = self.GetApplicationPaths(package_name) |
+ |
+ if device_apk_paths: |
+ partial_install_package = package_name |
+ device_checksums = md5sum.CalculateDeviceMd5Sums(device_apk_paths, self) |
+ host_checksums = md5sum.CalculateHostMd5Sums(all_apks) |
+ apks_to_install = [k for (k, v) in host_checksums.iteritems() |
+ if v not in device_checksums.values()] |
+ if apks_to_install and not reinstall: |
+ self.adb.Uninstall(package_name) |
+ partial_install_package = None |
+ apks_to_install = all_apks |
+ else: |
+ partial_install_package = None |
+ apks_to_install = all_apks |
+ if apks_to_install: |
+ self.adb.InstallMultiple( |
+ apks_to_install, partial=partial_install_package, reinstall=reinstall) |
+ |
+ 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.adb.GetDeviceSerial()) |
+ |
+ |
@decorators.WithTimeoutAndRetriesFromInstance() |
def RunShellCommand(self, cmd, check_return=False, cwd=None, env=None, |
as_root=False, single_line=False, large_output=False, |
@@ -557,8 +646,8 @@ class DeviceUtils(object): |
if large_output_mode: |
with device_temp_file.DeviceTempFile(self.adb) as large_output_file: |
cmd = '%s > %s' % (cmd, large_output_file.name) |
- logging.info('Large output mode enabled. Will write output to device ' |
- 'and read results from file.') |
+ logging.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: |
@@ -714,7 +803,7 @@ class DeviceUtils(object): |
for k, v in extras.iteritems(): |
cmd.extend(['-e', str(k), str(v)]) |
cmd.append(component) |
- return self.RunShellCommand(cmd, check_return=True) |
+ return self.RunShellCommand(cmd, check_return=True, large_output=True) |
@decorators.WithTimeoutAndRetriesFromInstance() |
def BroadcastIntent(self, intent_obj, timeout=None, retries=None): |
@@ -734,7 +823,10 @@ class DeviceUtils(object): |
@decorators.WithTimeoutAndRetriesFromInstance() |
def GoHome(self, timeout=None, retries=None): |
- """Return to the home screen. |
+ """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 |
@@ -744,11 +836,30 @@ class DeviceUtils(object): |
CommandTimeoutError on timeout. |
DeviceUnreachableError on missing device. |
""" |
+ def is_launcher_focused(): |
+ output = self.RunShellCommand(['dumpsys', 'window', 'windows'], |
+ check_return=True, large_output=True) |
+ 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. |
@@ -782,7 +893,7 @@ class DeviceUtils(object): |
# may never return. |
if ((self.build_version_sdk >= |
constants.ANDROID_SDK_VERSION_CODES.JELLY_BEAN_MR2) |
- or self.GetApplicationPath(package)): |
+ or self.GetApplicationPaths(package)): |
self.RunShellCommand(['pm', 'clear', package], check_return=True) |
@decorators.WithTimeoutAndRetriesFromInstance() |
@@ -810,9 +921,14 @@ class DeviceUtils(object): |
PUSH_CHANGED_FILES_DEFAULT_TIMEOUT, |
PUSH_CHANGED_FILES_DEFAULT_RETRIES) |
def PushChangedFiles(self, host_device_tuples, timeout=None, |
- retries=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 |
@@ -820,6 +936,7 @@ class DeviceUtils(object): |
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. |
@@ -827,15 +944,72 @@ class DeviceUtils(object): |
DeviceUnreachableError on missing device. |
""" |
- files = [] |
+ all_changed_files = [] |
+ all_stale_files = [] |
for h, d in host_device_tuples: |
if os.path.isdir(h): |
self.RunShellCommand(['mkdir', '-p', d], check_return=True) |
- files += self._GetChangedFilesImpl(h, d) |
+ (changed_files, stale_files) = self._GetChangedAndStaleFiles(h, d) |
+ all_changed_files += changed_files |
+ all_stale_files += stale_files |
- if not files: |
+ if delete_device_stale: |
+ self.RunShellCommand(['rm', '-f'] + all_stale_files, |
+ check_return=True) |
+ |
+ if not all_changed_files: |
return |
+ self._PushFilesImpl(host_device_tuples, all_changed_files) |
+ |
+ def _GetChangedAndStaleFiles(self, host_path, device_path): |
+ """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 |
+ |
+ Returns: |
+ a two-element tuple |
+ 1st element: a list of (host_files_path, device_files_path) tuples to push |
+ 2nd element: a list of stale files under device_path |
+ """ |
+ real_host_path = os.path.realpath(host_path) |
+ try: |
+ real_device_path = self.RunShellCommand( |
+ ['realpath', device_path], single_line=True, check_return=True) |
+ except device_errors.CommandFailedError: |
+ real_device_path = None |
+ if not real_device_path: |
+ return ([(host_path, device_path)], []) |
+ |
+ try: |
+ host_checksums = md5sum.CalculateHostMd5Sums([real_host_path]) |
+ device_checksums = md5sum.CalculateDeviceMd5Sums( |
+ [real_device_path], self) |
+ except EnvironmentError as e: |
+ logging.warning('Error calculating md5: %s', e) |
+ return ([(host_path, device_path)], []) |
+ |
+ if os.path.isfile(host_path): |
+ host_checksum = host_checksums.get(real_host_path) |
+ device_checksum = device_checksums.get(real_device_path) |
+ if host_checksum != device_checksum: |
+ return ([(host_path, device_path)], []) |
+ else: |
+ return ([], []) |
+ else: |
+ to_push = [] |
+ for host_abs_path, host_checksum in host_checksums.iteritems(): |
+ device_abs_path = '%s/%s' % ( |
+ real_device_path, os.path.relpath(host_abs_path, real_host_path)) |
+ device_checksum = device_checksums.pop(device_abs_path, None) |
+ if device_checksum != host_checksum: |
+ to_push.append((host_abs_path, device_abs_path)) |
+ to_delete = device_checksums.keys() |
+ return (to_push, to_delete) |
+ |
+ def _PushFilesImpl(self, host_device_tuples, files): |
size = sum(host_utils.GetRecursiveDiskUsage(h) for h, _ in files) |
file_count = len(files) |
dir_size = sum(host_utils.GetRecursiveDiskUsage(h) |
@@ -866,44 +1040,6 @@ class DeviceUtils(object): |
['chmod', '-R', '777'] + [d for _, d in host_device_tuples], |
as_root=True, check_return=True) |
- def _GetChangedFilesImpl(self, host_path, device_path): |
- real_host_path = os.path.realpath(host_path) |
- try: |
- real_device_path = self.RunShellCommand( |
- ['realpath', device_path], single_line=True, check_return=True) |
- except device_errors.CommandFailedError: |
- real_device_path = None |
- if not real_device_path: |
- return [(host_path, device_path)] |
- |
- try: |
- host_checksums = md5sum.CalculateHostMd5Sums([real_host_path]) |
- device_paths_to_md5 = ( |
- real_device_path if os.path.isfile(real_host_path) |
- else ('%s/%s' % (real_device_path, os.path.relpath(p, real_host_path)) |
- for p in host_checksums.iterkeys())) |
- device_checksums = md5sum.CalculateDeviceMd5Sums( |
- device_paths_to_md5, self) |
- except EnvironmentError as e: |
- logging.warning('Error calculating md5: %s', e) |
- return [(host_path, device_path)] |
- |
- if os.path.isfile(host_path): |
- host_checksum = host_checksums.get(real_host_path) |
- device_checksum = device_checksums.get(real_device_path) |
- if host_checksum != device_checksum: |
- return [(host_path, device_path)] |
- else: |
- return [] |
- else: |
- to_push = [] |
- for host_abs_path, host_checksum in host_checksums.iteritems(): |
- device_abs_path = '%s/%s' % ( |
- real_device_path, os.path.relpath(host_abs_path, real_host_path)) |
- if (device_checksums.get(device_abs_path) != host_checksum): |
- to_push.append((host_abs_path, device_abs_path)) |
- return to_push |
- |
def _InstallCommands(self): |
if self._commands_installed is None: |
try: |
@@ -1244,6 +1380,29 @@ class DeviceUtils(object): |
else: |
return False |
+ @property |
+ def language(self): |
+ """Returns the language setting on the device.""" |
+ return self.GetProp('persist.sys.language', cache=False) |
+ |
+ @property |
+ def country(self): |
+ """Returns the country setting on the device.""" |
+ return self.GetProp('persist.sys.country', cache=False) |
+ |
+ @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', |
+ } |
+ dpi = int(self.GetProp('ro.sf.lcd_density', cache=True)) |
+ return DPI_TO_DENSITY.get(dpi, 'tvdpi') |
@property |
def build_description(self): |
@@ -1585,4 +1744,3 @@ class DeviceUtils(object): |
return [cls(adb) for adb in adb_wrapper.AdbWrapper.Devices() |
if not blacklisted(adb)] |
- |