chromium/android_webview/tools/remove_preinstalled_webview.py

#!/usr/bin/env vpython3
#
# Copyright 2018 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Removes the preinstalled WebView on a device to avoid signature mismatches.

This should only be used by developers. This script will fail on actual user
devices (and this configuration is not recommended for user devices).

The recommended development configuration for Googlers is to satisfy all of the
below:
  1. The device has a Google-provided image.
  2. The device does not have an image based on AOSP.
  3. Set `use_signing_keys = true` in GN args.

If any of the above are not satisfied (or if you're external to Google), you can
use this script to remove the system-image WebView on your device, which will
allow you to install a local WebView build without triggering signature
mismatches (which would otherwise block installing the APK).

After running this script, you should be able to build and install
system_webview_apk.
  * If your device does *not* have an AOSP-based image, you will need to set
    `system_webview_package_name = "com.google.android.webview"` in GN args.
"""

from __future__ import print_function

import argparse
import logging
import os
import sys

sys.path.append(os.path.join(
    os.path.dirname(__file__), os.pardir, os.pardir, 'build', 'android'))
# pylint: disable=wrong-import-position,import-error
import devil_chromium
from devil.android import device_errors
from devil.android import device_utils
from devil.android.sdk import version_codes
from devil.android.tools import script_common
from devil.android.tools import system_app
from devil.utils import logging_common

WEBVIEW_PACKAGES = ['com.android.webview', 'com.google.android.webview']

TRICHROME_WEBVIEW_PACKAGE = 'com.google.android.webview'
TRICHROME_CHROME_PACKAGE = 'com.android.chrome'
TRICHROME_LIBRARY_PACKAGE = 'com.google.android.trichromelibrary'

ALREADY_UNINSTALLED_ERROR_MESSAGE = "DELETE_FAILED_INTERNAL_ERROR"


def FindFilePath(device, file_name):
  paths = device.RunShellCommand(['find', '/product', '-iname', file_name],
                                 check_return=True)
  assert len(paths) <= 1, ('Found multiple paths %s for %s' %
                           (str(paths), file_name))
  return paths


def FindSystemAPKFiles(device, apk_name):
  """The expected structure of WebViewGoogle system APK is one of the following:
    On most Q+ devices and emulators:
    /product/app/WebViewGoogle/WebViewGoogle.apk.gz
    /product/app/WebViewGoogle-Stub/WebViewGoogle-Stub.apk
    On Q and R emulators:
    /product/app/WebViewGoogle/WebViewGoogle.apk

    Others Trichrome system APKs follow a similar structure.
  """
  paths = []
  paths.extend(FindFilePath(device, apk_name + '.apk.gz'))
  paths.extend(FindFilePath(device, apk_name + '-Stub.apk'))
  paths.extend(FindFilePath(device, apk_name + '.apk'))
  if len(paths) == 0:
    logging.info('%s system APK not found or already removed', apk_name)
  return paths


def RemoveTrichromeSystemAPKs(device):
  """Removes Trichrome system APKs."""
  logging.info('Removing Trichrome system APKs from %s...', device.serial)
  paths = []
  with system_app.EnableSystemAppModification(device):
    for apk in ['WebViewGoogle', 'Chrome', 'TrichromeLibrary']:
      paths.extend(FindSystemAPKFiles(device, apk))
    device.RemovePath(paths, force=True, recursive=True)


def UninstallTrichromePackages(device):
  """Uninstalls Trichrome packages."""
  logging.info('Uninstalling Trichrome packages from %s...', device.serial)
  device.Uninstall(TRICHROME_WEBVIEW_PACKAGE)
  device.Uninstall(TRICHROME_CHROME_PACKAGE)
  # Keep uninstalling TRICHROME_LIBRARY_PACKAGE until we get
  # device_errors.AdbCommandFailedError as multiple versions maybe installed.
  # device.Uninstall doesn't work on shared libraries. We need to call Uninstall
  # on AdbWrapper directly.
  is_trichrome_library_installed = True
  try:
    # Limiting uninstalling to 10 times as a precaution.
    for _ in range(10):
      device.adb.Uninstall(TRICHROME_LIBRARY_PACKAGE)
  except device_errors.AdbCommandFailedError as e:
    if e.output and ALREADY_UNINSTALLED_ERROR_MESSAGE in e.output:
      # Trichrome library is already uninstalled.
      is_trichrome_library_installed = False
  if is_trichrome_library_installed:
    raise device_errors.CommandFailedError(
        '{} is still installed on the device'.format(TRICHROME_LIBRARY_PACKAGE),
        device)


def UninstallWebViewSystemImages(device):
  """Uninstalls system images for known WebView packages."""
  logging.info('Removing system images from %s...', device.serial)
  system_app.RemoveSystemApps(device, WEBVIEW_PACKAGES)
  # Removing system apps will reboot the device, so we try to unlock the device
  # once that's done.
  device.Unlock()


def UninstallWebViewUpdates(device):
  """Uninstalls updates for WebView packages, if updates exist."""
  logging.info('Uninstalling updates from %s...', device.serial)
  for webview_package in WEBVIEW_PACKAGES:
    try:
      device.Uninstall(webview_package)
    except device_errors.AdbCommandFailedError:
      # This can happen if the app is on the system image but there are no
      # updates installed on top of that.
      logging.info('No update to uninstall for %s on %s', webview_package,
                   device.serial)


def CheckWebViewIsUninstalled(device):
  """Throws if WebView is still installed."""
  for webview_package in WEBVIEW_PACKAGES:
    if device.IsApplicationInstalled(webview_package):
      raise device_errors.CommandFailedError(
          '{} is still installed on the device'.format(webview_package),
          device)


def RemovePreinstalledWebViews(device):
  device.EnableRoot()
  try:
    if device.build_version_sdk >= version_codes.Q:
      logging.warning('This is a Q+ device, so both WebView and Chrome will be '
                      'removed.')
      RemoveTrichromeSystemAPKs(device)
      UninstallTrichromePackages(device)
    else:
      UninstallWebViewUpdates(device)
      UninstallWebViewSystemImages(device)
    CheckWebViewIsUninstalled(device)
  except device_errors.CommandFailedError:
    if device.is_emulator:
      # Point the user to documentation, since there's a good chance they can
      # workaround this. Use lots of newlines to make sure this message doesn't
      # get lost.
      logging.error('Did you start the emulator with "-writable-system?"\n'
                    'See https://chromium.googlesource.com/chromium/src/+/'
                    'main/docs/android_emulator.md#writable-system-partition'
                    '\n')
    raise
  device.SetWebViewFallbackLogic(False)  # Allow standalone WebView on N+

def main():
  parser = argparse.ArgumentParser(description="""
Removes the preinstalled WebView APKs to avoid signature mismatches during
development.
""")

  script_common.AddEnvironmentArguments(parser)
  script_common.AddDeviceArguments(parser)
  logging_common.AddLoggingArguments(parser)

  args = parser.parse_args()
  logging_common.InitializeLogging(args)
  devil_chromium.Initialize(adb_path=args.adb_path)

  devices = device_utils.DeviceUtils.HealthyDevices(device_arg=args.devices)
  device_utils.DeviceUtils.parallel(devices).pMap(RemovePreinstalledWebViews)


if __name__ == '__main__':
  main()