diff options
Diffstat (limited to 'catapult/devil/devil/android/device_utils.py')
-rw-r--r-- | catapult/devil/devil/android/device_utils.py | 258 |
1 files changed, 248 insertions, 10 deletions
diff --git a/catapult/devil/devil/android/device_utils.py b/catapult/devil/devil/android/device_utils.py index 5a3db413..518e4393 100644 --- a/catapult/devil/devil/android/device_utils.py +++ b/catapult/devil/devil/android/device_utils.py @@ -59,6 +59,29 @@ _DEFAULT_RETRIES = 3 # the timeout_retry decorators. DEFAULT = object() +# A sentinel object to require that calls to RunShellCommand force running the +# command with su even if the device has been rooted. To use, pass into the +# as_root param. +_FORCE_SU = object() + +_RECURSIVE_DIRECTORY_LIST_SCRIPT = """ + function list_subdirs() { + for f in "$1"/* ; + do + if [ -d "$f" ] ; + then + if [ "$f" == "." ] || [ "$f" == ".." ] ; + then + continue ; + fi ; + echo "$f" ; + list_subdirs "$f" ; + fi ; + done ; + } ; + list_subdirs %s +""" + _RESTART_ADBD_SCRIPT = """ trap '' HUP trap '' TERM @@ -88,6 +111,7 @@ _PERMISSIONS_BLACKLIST_RE = re.compile('|'.join(fnmatch.translate(p) for p in [ 'android.permission.DISABLE_KEYGUARD', 'android.permission.DOWNLOAD_WITHOUT_NOTIFICATION', 'android.permission.EXPAND_STATUS_BAR', + 'android.permission.FOREGROUND_SERVICE', 'android.permission.GET_PACKAGE_SIZE', 'android.permission.INSTALL_SHORTCUT', 'android.permission.INJECT_EVENTS', @@ -649,6 +673,22 @@ class DeviceUtils(object): 'Version name for %s not found on dumpsys output' % package, str(self)) @decorators.WithTimeoutAndRetriesFromInstance() + def GetPackageArchitecture(self, package, timeout=None, retries=None): + """Get the architecture of a package installed on the device. + + Args: + package: Name of the package. + + Returns: + A string with the architecture, or None if the package is missing. + """ + lines = self._GetDumpsysOutput(['package', package], 'primaryCpuAbi') + if lines: + _, _, package_arch = lines[-1].partition('=') + return package_arch.strip() + return None + + @decorators.WithTimeoutAndRetriesFromInstance() def GetApplicationDataDirectory(self, package, timeout=None, retries=None): """Get the data directory on the device for the given package. @@ -670,6 +710,31 @@ class DeviceUtils(object): raise device_errors.CommandFailedError( 'Could not find data directory for %s', package) + @decorators.WithTimeoutAndRetriesFromInstance() + def GetSecurityContextForPackage(self, package, encrypted=False, timeout=None, + retries=None): + """Gets the SELinux security context for the given package. + + Args: + package: Name of the package. + encrypted: Whether to check in the encrypted data directory + (/data/user_de/0/) or the unencrypted data directory (/data/data/). + + Returns: + The package's security context as a string, or None if not found. + """ + directory = '/data/user_de/0/' if encrypted else '/data/data/' + for line in self.RunShellCommand(['ls', '-Z', directory], + as_root=True, check_return=True): + split_line = line.split() + # ls -Z output differs between Android versions, but the package is + # always last and the context always starts with "u:object" + if split_line[-1] == package: + for column in split_line: + if column.startswith('u:object'): + return column + return None + def TakeBugReport(self, path, timeout=60*5, retries=None): """Takes a bug report and dumps it to the specified path. @@ -1064,7 +1129,7 @@ class DeviceUtils(object): 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(): + if (as_root is _FORCE_SU) or (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)) @@ -1202,6 +1267,33 @@ class DeviceUtils(object): raise device_errors.CommandFailedError(line, str(self)) @decorators.WithTimeoutAndRetriesFromInstance() + def StartService(self, intent_obj, user_id=None, timeout=None, retries=None): + """Start a service on the device. + + Args: + intent_obj: An Intent object to send describing the service to start. + user_id: A specific user to start the service as, defaults to current. + timeout: Timeout in seconds. + retries: Number of retries + + Raises: + CommandFailedError if the service could not be started. + CommandTimeoutError on timeout. + DeviceUnreachableError on missing device. + """ + # For whatever reason, startservice was changed to start-service on O and + # above. + cmd = ['am', 'startservice'] + if self.build_version_sdk >= version_codes.OREO: + cmd[1] = 'start-service' + if user_id: + cmd.extend(['--user', str(user_id)]) + 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: @@ -1383,7 +1475,7 @@ class DeviceUtils(object): missing_dirs.add(posixpath.dirname(d)) if delete_device_stale and all_stale_files: - self.RunShellCommand(['rm', '-f'] + all_stale_files, check_return=True) + self.RemovePath(all_stale_files, force=True, recursive=True) if all_changed_files: if missing_dirs: @@ -1483,15 +1575,66 @@ class DeviceUtils(object): else: to_push.append((host_abs_path, device_abs_path)) to_delete = device_checksums.keys() + # We can't rely solely on the checksum approach since it does not catch + # stale directories, which can result in empty directories that cause issues + # during copying in efficient_android_directory_copy.sh. So, find any stale + # directories here so they can be removed in addition to stale files. + if track_stale: + to_delete.extend(self._GetStaleDirectories(host_path, device_path)) def cache_commit_func(): - new_sums = {posixpath.join(device_path, path[len(host_path) + 1:]): val - for path, val in host_checksums.iteritems()} + # When host_path is a not a directory, the path.join() call below would + # have an '' as the second argument, causing an unwanted / to be appended. + if os.path.isfile(host_path): + assert len(host_checksums) == 1 + new_sums = {device_path: host_checksums[host_path]} + else: + new_sums = {posixpath.join(device_path, path[len(host_path) + 1:]): val + for path, val in host_checksums.iteritems()} cache_entry = [ignore_other_files, new_sums] self._cache['device_path_checksums'][device_path] = cache_entry return (to_push, up_to_date, to_delete, cache_commit_func) + def _GetStaleDirectories(self, host_path, device_path): + """Gets a list of stale directories on the device. + + Args: + host_path: an absolute path of a directory on the host + device_path: an absolute path of a directory on the device + + Returns: + A list containing absolute paths to directories on the device that are + considered stale. + """ + def get_device_dirs(path): + directories = set() + command = _RECURSIVE_DIRECTORY_LIST_SCRIPT % cmd_helper.SingleQuote(path) + # We use shell=True to evaluate the command as a script through the shell, + # otherwise RunShellCommand tries to interpret it as the name of a (non + # existent) command to run. + for line in self.RunShellCommand( + command, shell=True, check_return=True): + directories.add(posixpath.relpath(posixpath.normpath(line), path)) + return directories + + def get_host_dirs(path): + directories = set() + if not os.path.isdir(path): + return directories + for root, _, _ in os.walk(path): + if root != path: + # Strip off the top level directory so we can compare the device and + # host. + directories.add( + os.path.relpath(root, path).replace(os.sep, posixpath.sep)) + return directories + + host_dirs = get_host_dirs(host_path) + device_dirs = get_device_dirs(device_path) + stale_dirs = device_dirs - host_dirs + return [posixpath.join(device_path, d) for d in stale_dirs] + def _ComputeDeviceChecksumsForApks(self, package_name): ret = self._cache['package_apk_checksums'].get(package_name) if ret is None: @@ -1608,6 +1751,8 @@ class DeviceUtils(object): except zip_utils.ZipFailedError: return False + logger.info('Pushing %d files via .zip of size %d', len(files), + os.path.getsize(zip_path)) self.NeedsSU() with device_temp_file.DeviceTempFile( self.adb, suffix='.zip') as device_temp: @@ -2279,9 +2424,8 @@ class DeviceUtils(object): """ 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 behavior was changed in Android O and above, http://crbug.com/686716 + if self.build_version_sdk >= version_codes.OREO: ps_cmd = 'ps -e' if pattern: return self._RunPipedShellCommand( @@ -2331,6 +2475,29 @@ class DeviceUtils(object): processes.append(ProcessInfo(**row)) return processes + def _GetDumpsysOutput(self, extra_args, pattern=None): + """Runs |dumpsys| command on the device and returns its output. + + This private method implements support for filtering the output by a given + |pattern|, but does not do any output parsing. + """ + try: + cmd = ['dumpsys'] + extra_args + if pattern: + cmd = ' '.join(cmd_helper.SingleQuote(s) for s in cmd) + return self._RunPipedShellCommand( + '%s | grep -F %s' % (cmd, cmd_helper.SingleQuote(pattern))) + else: + cmd = ['dumpsys'] + extra_args + return self.RunShellCommand(cmd, 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 dumpsys succeeded but grep failed, there were no lines matching + # the given pattern. + return [] + else: + raise + # TODO(#4103): Remove after migrating clients to ListProcesses. @decorators.WithTimeoutAndRetriesFromInstance() def GetPids(self, process_name=None, timeout=None, retries=None): @@ -2438,6 +2605,30 @@ class DeviceUtils(object): check_return=True) @decorators.WithTimeoutAndRetriesFromInstance() + def SetWebViewImplementation(self, package_name, timeout=None, retries=None): + """Select the WebView implementation to the specified package. + + Args: + package_name: The package name of a WebView implementation. The package + must be already installed on the device. + timeout: timeout in seconds + retries: number of retries + + Raises: + CommandFailedError on failure. + CommandTimeoutError on timeout. + DeviceUnreachableError on missing device. + """ + output = self.RunShellCommand( + ['cmd', 'webviewupdate', 'set-webview-implementation', package_name], + single_line=True, check_return=True) + if output == 'Success': + logging.info('WebView provider set to: %s', package_name) + else: + raise device_errors.CommandFailedError( + 'Error setting WebView provider: %s' % output, str(self)) + + @decorators.WithTimeoutAndRetriesFromInstance() def TakeScreenshot(self, host_path=None, timeout=None, retries=None): """Takes a screenshot of the device. @@ -2615,7 +2806,7 @@ class DeviceUtils(object): @classmethod def HealthyDevices(cls, blacklist=None, device_arg='default', retry=True, - **kwargs): + abis=None, **kwargs): """Returns a list of DeviceUtils instances. Returns a list of DeviceUtils instances that are attached, not blacklisted, @@ -2639,6 +2830,8 @@ class DeviceUtils(object): blacklisted. retry: If true, will attempt to restart adb server and query it again if no devices are found. + abis: A list of ABIs for which the device needs to support at least one of + (optional). A device serial, or a list of device serials (optional). Returns: @@ -2674,14 +2867,23 @@ class DeviceUtils(object): return True return False + def supports_abi(abi, serial): + if abis and abi not in abis: + logger.warning("Device %s doesn't support required ABIs.", serial) + return False + return True + 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)) + serial = adb.GetDeviceSerial() + if not blacklisted(serial): + device = cls(_CreateAdbWrapper(adb), **kwargs) + if supports_abi(device.GetABI(), serial): + devices.append(device) if len(devices) == 0 and not allow_no_devices: raise device_errors.NoDevicesError() @@ -2806,3 +3008,39 @@ class DeviceUtils(object): return self.SendKeyEvent(keyevent.KEYCODE_POWER) timeout_retry.WaitFor(screen_test, wait_period=1) + + @decorators.WithTimeoutAndRetriesFromInstance() + def ChangeOwner(self, owner_group, paths, timeout=None, retries=None): + """Changes file system ownership for permissions. + + Args: + owner_group: New owner and group to assign. Note that this should be a + string in the form user[.group] where the group is option. + paths: Paths to change ownership of. + + Note that the -R recursive option is not supported by all Android + versions. + """ + if not paths: + return + self.RunShellCommand(['chown', owner_group] + paths, check_return=True) + + @decorators.WithTimeoutAndRetriesFromInstance() + def ChangeSecurityContext(self, security_context, paths, timeout=None, + retries=None): + """Changes the SELinux security context for files. + + Args: + security_context: The new security context as a string + paths: Paths to change the security context of. + + Note that the -R recursive option is not supported by all Android + versions. + """ + if not paths: + return + command = ['chcon', security_context] + paths + + # Note, need to force su because chcon can fail with permission errors even + # if the device is rooted. + self.RunShellCommand(command, as_root=_FORCE_SU, check_return=True) |