Home | History | Annotate | Download | only in tools
      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