aboutsummaryrefslogtreecommitdiff
path: root/catapult/devil/devil/android/tools/system_app.py
diff options
context:
space:
mode:
Diffstat (limited to 'catapult/devil/devil/android/tools/system_app.py')
-rwxr-xr-xcatapult/devil/devil/android/tools/system_app.py218
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:]))