diff options
Diffstat (limited to 'catapult/devil/devil/android/tools/system_app.py')
-rwxr-xr-x | catapult/devil/devil/android/tools/system_app.py | 218 |
1 files changed, 218 insertions, 0 deletions
diff --git a/catapult/devil/devil/android/tools/system_app.py b/catapult/devil/devil/android/tools/system_app.py new file mode 100755 index 00000000..00ea312a --- /dev/null +++ b/catapult/devil/devil/android/tools/system_app.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python +# Copyright 2017 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. + +"""A script to replace a system app while running a command.""" + +import argparse +import contextlib +import logging +import os +import posixpath +import sys + + +if __name__ == '__main__': + sys.path.append( + os.path.abspath(os.path.join(os.path.dirname(__file__), + '..', '..', '..'))) + + +from devil.android import apk_helper +from devil.android import device_errors +from devil.android import device_temp_file +from devil.android.sdk import version_codes +from devil.android.tools import script_common +from devil.utils import cmd_helper +from devil.utils import parallelizer +from devil.utils import run_tests_helper + +logger = logging.getLogger(__name__) + + +def RemoveSystemApps(device, package_names): + """Removes the given system apps. + + Args: + device: (device_utils.DeviceUtils) the device for which the given + system app should be removed. + package_name: (iterable of strs) the names of the packages to remove. + """ + system_package_paths = _FindSystemPackagePaths(device, package_names) + if system_package_paths: + with EnableSystemAppModification(device): + device.RemovePath(system_package_paths, force=True, recursive=True) + + +@contextlib.contextmanager +def ReplaceSystemApp(device, package_name, replacement_apk): + """A context manager that replaces the given system app while in scope. + + Args: + device: (device_utils.DeviceUtils) the device for which the given + system app should be replaced. + package_name: (str) the name of the package to replace. + replacement_apk: (str) the path to the APK to use as a replacement. + """ + storage_dir = device_temp_file.NamedDeviceTemporaryDirectory(device.adb) + relocate_app = _RelocateApp(device, package_name, storage_dir.name) + install_app = _TemporarilyInstallApp(device, replacement_apk) + with storage_dir, relocate_app, install_app: + yield + + +def _FindSystemPackagePaths(device, system_package_list): + """Finds all system paths for the given packages.""" + found_paths = [] + for system_package in system_package_list: + found_paths.extend(device.GetApplicationPaths(system_package)) + return [p for p in found_paths if p.startswith('/system/')] + + +_ENABLE_MODIFICATION_PROP = 'devil.modify_sys_apps' + + +@contextlib.contextmanager +def EnableSystemAppModification(device): + """A context manager that allows system apps to be modified while in scope. + + Args: + device: (device_utils.DeviceUtils) the device + """ + if device.GetProp(_ENABLE_MODIFICATION_PROP) == '1': + yield + return + + device.EnableRoot() + if not device.HasRoot(): + raise device_errors.CommandFailedError( + 'Failed to enable modification of system apps on non-rooted device', + str(device)) + + try: + # Disable Marshmallow's Verity security feature + if device.build_version_sdk >= version_codes.MARSHMALLOW: + logger.info('Disabling Verity on %s', device.serial) + device.adb.DisableVerity() + device.Reboot() + device.WaitUntilFullyBooted() + device.EnableRoot() + + device.adb.Remount() + device.RunShellCommand(['stop'], check_return=True) + device.SetProp(_ENABLE_MODIFICATION_PROP, '1') + yield + finally: + device.SetProp(_ENABLE_MODIFICATION_PROP, '0') + device.Reboot() + device.WaitUntilFullyBooted() + + +@contextlib.contextmanager +def _RelocateApp(device, package_name, relocate_to): + """A context manager that relocates an app while in scope.""" + relocation_map = {} + system_package_paths = _FindSystemPackagePaths(device, [package_name]) + if system_package_paths: + relocation_map = { + p: posixpath.join(relocate_to, posixpath.relpath(p, '/')) + for p in system_package_paths + } + relocation_dirs = [ + posixpath.dirname(d) + for _, d in relocation_map.iteritems() + ] + device.RunShellCommand(['mkdir', '-p'] + relocation_dirs, + check_return=True) + _MoveApp(device, relocation_map) + else: + logger.info('No system package "%s"', package_name) + + try: + yield + finally: + _MoveApp(device, {v: k for k, v in relocation_map.iteritems()}) + + +@contextlib.contextmanager +def _TemporarilyInstallApp(device, apk): + """A context manager that installs an app while in scope.""" + device.adb.Install(apk, reinstall=True) + try: + yield + finally: + device.adb.Uninstall(apk_helper.GetPackageName(apk)) + + +def _MoveApp(device, relocation_map): + """Moves an app according to the provided relocation map. + + Args: + device: (device_utils.DeviceUtils) + relocation_map: (dict) A dict that maps src to dest + """ + movements = [ + 'mv %s %s' % (k, v) + for k, v in relocation_map.iteritems() + ] + cmd = ' && '.join(movements) + with EnableSystemAppModification(device): + device.RunShellCommand(cmd, as_root=True, check_return=True, shell=True) + + +def main(raw_args): + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers() + + def add_common_arguments(p): + script_common.AddDeviceArguments(p) + script_common.AddEnvironmentArguments(p) + p.add_argument( + '-v', '--verbose', action='count', default=0, + help='Print more information.') + p.add_argument('command', nargs='*') + + @contextlib.contextmanager + def remove_system_app(device, args): + RemoveSystemApps(device, args.packages) + yield + + remove_parser = subparsers.add_parser('remove') + remove_parser.add_argument( + '--package', dest='packages', nargs='*', required=True, + help='The system package(s) to remove.') + add_common_arguments(remove_parser) + remove_parser.set_defaults(func=remove_system_app) + + @contextlib.contextmanager + def replace_system_app(device, args): + with ReplaceSystemApp(device, args.package, args.replace_with): + yield + + replace_parser = subparsers.add_parser('replace') + replace_parser.add_argument( + '--package', required=True, + help='The system package to replace.') + replace_parser.add_argument( + '--replace-with', metavar='APK', required=True, + help='The APK with which the existing system app should be replaced.') + add_common_arguments(replace_parser) + replace_parser.set_defaults(func=replace_system_app) + + args = parser.parse_args(raw_args) + + run_tests_helper.SetLogLevel(args.verbose) + script_common.InitializeEnvironment(args) + + devices = script_common.GetDevices(args.devices, args.blacklist_file) + parallel_devices = parallelizer.SyncParallelizer( + [args.func(d, args) for d in devices]) + with parallel_devices: + if args.command: + return cmd_helper.Call(args.command) + return 0 + + +if __name__ == '__main__': + sys.exit(main(sys.argv[1:])) |