1 #!/usr/bin/env python 2 # Copyright 2017 The Chromium Authors. All rights reserved. 3 # Use of this source code is governed by a BSD-style license that can be 4 # found in the LICENSE file. 5 6 """A script to replace a system app while running a command.""" 7 8 import argparse 9 import contextlib 10 import logging 11 import os 12 import posixpath 13 import sys 14 15 16 if __name__ == '__main__': 17 sys.path.append( 18 os.path.abspath(os.path.join(os.path.dirname(__file__), 19 '..', '..', '..'))) 20 21 22 from devil.android import apk_helper 23 from devil.android import device_errors 24 from devil.android import device_temp_file 25 from devil.android.sdk import version_codes 26 from devil.android.tools import script_common 27 from devil.utils import cmd_helper 28 from devil.utils import parallelizer 29 from devil.utils import run_tests_helper 30 31 logger = logging.getLogger(__name__) 32 33 34 def RemoveSystemApps(device, package_names): 35 """Removes the given system apps. 36 37 Args: 38 device: (device_utils.DeviceUtils) the device for which the given 39 system app should be removed. 40 package_name: (iterable of strs) the names of the packages to remove. 41 """ 42 system_package_paths = _FindSystemPackagePaths(device, package_names) 43 if system_package_paths: 44 with EnableSystemAppModification(device): 45 device.RemovePath(system_package_paths, force=True, recursive=True) 46 47 48 @contextlib.contextmanager 49 def ReplaceSystemApp(device, package_name, replacement_apk): 50 """A context manager that replaces the given system app while in scope. 51 52 Args: 53 device: (device_utils.DeviceUtils) the device for which the given 54 system app should be replaced. 55 package_name: (str) the name of the package to replace. 56 replacement_apk: (str) the path to the APK to use as a replacement. 57 """ 58 storage_dir = device_temp_file.NamedDeviceTemporaryDirectory(device.adb) 59 relocate_app = _RelocateApp(device, package_name, storage_dir.name) 60 install_app = _TemporarilyInstallApp(device, replacement_apk) 61 with storage_dir, relocate_app, install_app: 62 yield 63 64 65 def _FindSystemPackagePaths(device, system_package_list): 66 """Finds all system paths for the given packages.""" 67 found_paths = [] 68 for system_package in system_package_list: 69 found_paths.extend(device.GetApplicationPaths(system_package)) 70 return [p for p in found_paths if p.startswith('/system/')] 71 72 73 _ENABLE_MODIFICATION_PROP = 'devil.modify_sys_apps' 74 75 76 @contextlib.contextmanager 77 def EnableSystemAppModification(device): 78 """A context manager that allows system apps to be modified while in scope. 79 80 Args: 81 device: (device_utils.DeviceUtils) the device 82 """ 83 if device.GetProp(_ENABLE_MODIFICATION_PROP) == '1': 84 yield 85 return 86 87 device.EnableRoot() 88 if not device.HasRoot(): 89 raise device_errors.CommandFailedError( 90 'Failed to enable modification of system apps on non-rooted device', 91 str(device)) 92 93 try: 94 # Disable Marshmallow's Verity security feature 95 if device.build_version_sdk >= version_codes.MARSHMALLOW: 96 logger.info('Disabling Verity on %s', device.serial) 97 device.adb.DisableVerity() 98 device.Reboot() 99 device.WaitUntilFullyBooted() 100 device.EnableRoot() 101 102 device.adb.Remount() 103 device.RunShellCommand(['stop'], check_return=True) 104 device.SetProp(_ENABLE_MODIFICATION_PROP, '1') 105 yield 106 finally: 107 device.SetProp(_ENABLE_MODIFICATION_PROP, '0') 108 device.Reboot() 109 device.WaitUntilFullyBooted() 110 111 112 @contextlib.contextmanager 113 def _RelocateApp(device, package_name, relocate_to): 114 """A context manager that relocates an app while in scope.""" 115 relocation_map = {} 116 system_package_paths = _FindSystemPackagePaths(device, [package_name]) 117 if system_package_paths: 118 relocation_map = { 119 p: posixpath.join(relocate_to, posixpath.relpath(p, '/')) 120 for p in system_package_paths 121 } 122 relocation_dirs = [ 123 posixpath.dirname(d) 124 for _, d in relocation_map.iteritems() 125 ] 126 device.RunShellCommand(['mkdir', '-p'] + relocation_dirs, 127 check_return=True) 128 _MoveApp(device, relocation_map) 129 else: 130 logger.info('No system package "%s"', package_name) 131 132 try: 133 yield 134 finally: 135 _MoveApp(device, {v: k for k, v in relocation_map.iteritems()}) 136 137 138 @contextlib.contextmanager 139 def _TemporarilyInstallApp(device, apk): 140 """A context manager that installs an app while in scope.""" 141 device.adb.Install(apk, reinstall=True) 142 try: 143 yield 144 finally: 145 device.adb.Uninstall(apk_helper.GetPackageName(apk)) 146 147 148 def _MoveApp(device, relocation_map): 149 """Moves an app according to the provided relocation map. 150 151 Args: 152 device: (device_utils.DeviceUtils) 153 relocation_map: (dict) A dict that maps src to dest 154 """ 155 movements = [ 156 'mv %s %s' % (k, v) 157 for k, v in relocation_map.iteritems() 158 ] 159 cmd = ' && '.join(movements) 160 with EnableSystemAppModification(device): 161 device.RunShellCommand(cmd, as_root=True, check_return=True, shell=True) 162 163 164 def main(raw_args): 165 parser = argparse.ArgumentParser() 166 subparsers = parser.add_subparsers() 167 168 def add_common_arguments(p): 169 script_common.AddDeviceArguments(p) 170 script_common.AddEnvironmentArguments(p) 171 p.add_argument( 172 '-v', '--verbose', action='count', default=0, 173 help='Print more information.') 174 p.add_argument('command', nargs='*') 175 176 @contextlib.contextmanager 177 def remove_system_app(device, args): 178 RemoveSystemApps(device, args.packages) 179 yield 180 181 remove_parser = subparsers.add_parser('remove') 182 remove_parser.add_argument( 183 '--package', dest='packages', nargs='*', required=True, 184 help='The system package(s) to remove.') 185 add_common_arguments(remove_parser) 186 remove_parser.set_defaults(func=remove_system_app) 187 188 @contextlib.contextmanager 189 def replace_system_app(device, args): 190 with ReplaceSystemApp(device, args.package, args.replace_with): 191 yield 192 193 replace_parser = subparsers.add_parser('replace') 194 replace_parser.add_argument( 195 '--package', required=True, 196 help='The system package to replace.') 197 replace_parser.add_argument( 198 '--replace-with', metavar='APK', required=True, 199 help='The APK with which the existing system app should be replaced.') 200 add_common_arguments(replace_parser) 201 replace_parser.set_defaults(func=replace_system_app) 202 203 args = parser.parse_args(raw_args) 204 205 run_tests_helper.SetLogLevel(args.verbose) 206 script_common.InitializeEnvironment(args) 207 208 devices = script_common.GetDevices(args.devices, args.blacklist_file) 209 parallel_devices = parallelizer.SyncParallelizer( 210 [args.func(d, args) for d in devices]) 211 with parallel_devices: 212 if args.command: 213 return cmd_helper.Call(args.command) 214 return 0 215 216 217 if __name__ == '__main__': 218 sys.exit(main(sys.argv[1:])) 219