aboutsummaryrefslogtreecommitdiff
path: root/catapult/devil/devil/android/tools/webview_app.py
blob: 406de1cb9c5a22a296bbe822b9b41b012aed0070 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
#!/usr/bin/env python
# Copyright 2019 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 use a package as the WebView provider while running a command."""

import argparse
import contextlib
import logging
import os
import re
import sys

if __name__ == '__main__':
  sys.path.append(
      os.path.abspath(
          os.path.join(os.path.dirname(__file__), '..', '..', '..')))
  sys.path.append(
      os.path.abspath(
          os.path.join(
              os.path.dirname(__file__), '..', '..', '..', '..', 'common',
              'py_utils')))

from devil.android import apk_helper
from devil.android import device_errors
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 cmd_helper
from devil.utils import parallelizer
from devil.utils import run_tests_helper
from py_utils import tempfile_ext

logger = logging.getLogger(__name__)

_SYSTEM_PATH_RE = re.compile(r'^\s*\/(system|product)\/')
_WEBVIEW_INSTALL_TIMEOUT = 300


@contextlib.contextmanager
def UseWebViewProvider(device, apk, expected_package=''):
  """A context manager that uses the apk as the webview provider while in scope.

  Args:
    device: (device_utils.DeviceUtils) The device for which the webview apk
      should be used as the provider.
    apk: (str) The path to the webview APK to use.
    expected_package: (str) If non-empty, verify apk's package name matches
                      this value.
  """
  package_name = apk_helper.GetPackageName(apk)

  if expected_package:
    if package_name != expected_package:
      raise device_errors.CommandFailedError(
          'WebView Provider package %s does not match expected %s' %
          (package_name, expected_package), str(device))

  if (device.build_version_sdk in [
      version_codes.NOUGAT, version_codes.NOUGAT_MR1
  ]):
    logger.warning('Due to webviewupdate bug in Nougat, WebView Fallback Logic '
                   'will be disabled and WebView provider may be changed after '
                   'exit of UseWebViewProvider context manager scope.')

  webview_update = device.GetWebViewUpdateServiceDump()
  original_fallback_logic = webview_update.get('FallbackLogicEnabled', None)
  original_provider = webview_update.get('CurrentWebViewPackage', None)

  # This is only necessary if the provider is a fallback provider, but we can't
  # generally determine this, so we set this just in case.
  device.SetWebViewFallbackLogic(False)

  try:
    # If user installed versions of the package is present, they must be
    # uninstalled first, so that the system version of the package,
    # if any, can be found by the ReplaceSystemApp context manager
    with _UninstallNonSystemApp(device, package_name):
      all_paths = device.GetApplicationPaths(package_name)
      system_paths = _FilterPaths(all_paths, True)
      non_system_paths = _FilterPaths(all_paths, False)
      if non_system_paths:
        raise device_errors.CommandFailedError(
            'Non-System application paths found after uninstallation: ',
            str(non_system_paths))
      elif system_paths:
        # app is system app, use ReplaceSystemApp to install
        with system_app.ReplaceSystemApp(
            device, package_name, apk,
            install_timeout=_WEBVIEW_INSTALL_TIMEOUT):
          _SetWebViewProvider(device, package_name)
          yield
      else:
        # app is not present on device, can directly install
        with _InstallApp(device, apk):
          _SetWebViewProvider(device, package_name)
          yield
  finally:
    # restore the original provider only if it was known and not the current
    # provider
    if original_provider is not None:
      webview_update = device.GetWebViewUpdateServiceDump()
      new_provider = webview_update.get('CurrentWebViewPackage', None)
      if new_provider != original_provider:
        device.SetWebViewImplementation(original_provider)

    # enable the fallback logic only if it was known to be enabled
    if original_fallback_logic is True:
      device.SetWebViewFallbackLogic(True)


def _SetWebViewProvider(device, package_name):
  """ Set the WebView provider to the package_name if supported. """
  if device.build_version_sdk >= version_codes.NOUGAT:
    device.SetWebViewImplementation(package_name)


def _FilterPaths(path_list, is_system):
  """ Return paths in the path_list that are/aren't system paths. """
  return [
      p for p in path_list if is_system == bool(re.match(_SYSTEM_PATH_RE, p))
  ]


def _RebasePath(new_root, old_root):
  """ Graft old_root onto new_root and return the result. """
  return os.path.join(new_root, os.path.relpath(old_root, '/'))


@contextlib.contextmanager
def _UninstallNonSystemApp(device, package_name):
  """ Make package un-installed while in scope. """
  all_paths = device.GetApplicationPaths(package_name)
  user_paths = _FilterPaths(all_paths, False)
  host_paths = []
  if user_paths:
    with tempfile_ext.NamedTemporaryDirectory() as temp_dir:
      for user_path in user_paths:
        host_path = _RebasePath(temp_dir, user_path)
        # PullFile takes care of host_path creation if needed.
        device.PullFile(user_path, host_path, timeout=_WEBVIEW_INSTALL_TIMEOUT)
        host_paths.append(host_path)
      device.Uninstall(package_name)
      try:
        yield
      finally:
        for host_path in reversed(host_paths):
          device.Install(
              host_path, reinstall=True, timeout=_WEBVIEW_INSTALL_TIMEOUT)
  else:
    yield


@contextlib.contextmanager
def _InstallApp(device, apk):
  """ Make apk installed while in scope. """
  package_name = apk_helper.GetPackageName(apk)
  device.Install(apk, reinstall=True, timeout=_WEBVIEW_INSTALL_TIMEOUT)
  try:
    yield
  finally:
    device.Uninstall(package_name)


def main(raw_args):
  parser = argparse.ArgumentParser()

  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 use_webview_provider(device, args):
    with UseWebViewProvider(device, args.apk, args.expected_package):
      yield

  parser.add_argument(
      '--apk', required=True, help='The apk to use as the provider.')
  parser.add_argument(
      '--expected-package',
      default='',
      help="Verify apk's package name matches value, disabled by default.")
  add_common_arguments(parser)
  parser.set_defaults(func=use_webview_provider)

  args = parser.parse_args(raw_args)

  run_tests_helper.SetLogLevel(args.verbose)
  script_common.InitializeEnvironment(args)

  devices = script_common.GetDevices(args.devices, args.denylist_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:]))