# Copyright 2023 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Android module to prepare APKs and emulators to run webdriver-based tests.
"""
import re
import os
import subprocess
import sys
from pkg_resources import packaging
from typing import List, Optional
from chrome.test.variations.test_utils import SRC_DIR
# The root for the module pylib/android is under build/android.
sys.path.append(os.path.join(SRC_DIR, 'build', 'android'))
# This import adds `devil` to `sys.path`.
import devil_chromium
from devil.android import apk_helper
from devil.android import device_utils
from devil.android import forwarder
from devil.android.sdk import adb_wrapper
from pylib.local.emulator import avd
_INSTALLER_SCRIPT_PY = os.path.join(
SRC_DIR, 'clank', 'bin', 'utils', 'installer_script_wrapper.py')
def _package_name(channel: str):
if channel in ('beta', 'dev', 'canary'):
return f'com.chrome.{channel}'
return 'com.android.chrome'
def _is_require_signed(channel: str) -> bool:
"""Check if we need to install a signed build."""
# The stable build has the same package name as prebuilt one, in order
# to avoid the signature mismatch, we need to install the one with the
# same signed build.
return channel == 'stable'
def install_chrome(channel: str, device: device_utils.DeviceUtils) -> str:
"""Installs Chrome to the device and returns the package name."""
args = [
_INSTALLER_SCRIPT_PY, f'--product=chrome',
f'--channel={channel}', f'--serial={device.serial}',
f'--adb={adb_wrapper.AdbWrapper.GetAdbPath()}',
]
args.append('--signed' if _is_require_signed(channel) else '--unsigned')
subprocess.check_call(args=args)
return _package_name(channel)
def install_webview(
channel: str,
device: device_utils.DeviceUtils
) -> packaging.version.Version:
"""Installs Webview to the device and returns the installed version."""
args = [
_INSTALLER_SCRIPT_PY, f'--product=webview',
f'--channel={channel}', f'--serial={device.serial}',
f'--adb={adb_wrapper.AdbWrapper.GetAdbPath()}',
]
args.append('--signed' if _is_require_signed(channel) else '--unsigned')
subprocess.check_call(args=args)
version_regex = r'\s*Preferred WebView package[^:]*[^\d]*([^\)]+)'
version_output = device.RunShellCommand(['dumpsys' ,'webviewupdate'])
version = [
m.group(1)
for line in version_output if (m := re.match(version_regex, line))
]
return packaging.version.parse(version[0]) if version else None
def _forward_port(device: device_utils.DeviceUtils,
ports: Optional[List[int]] = None):
# Ideally, we would dynamically allocate ports from the device, and
# remember the mapping here, it requires how the client redirects ports.
# Since we currently only allocate ports from a user space whose value is
# always 3xxxx and above, there is a very rare case to cause issues here.
# It is possible that the port is already used on the device, however,
# the likelihood is small, and we will fix once it shows a problem.
if ports:
forwarder.Forwarder.Map([(port, port) for port in ports], device)
def launch_emulator(avd_config: str,
emulator_window: bool,
ports: Optional[List[int]] = None) -> avd._AvdInstance:
"""Launches the emulator and forwards ports from device to host."""
avd_config = avd.AvdConfig(avd_config)
avd_config.Install()
instance = avd_config.CreateInstance()
instance.Start(writable_system=True,
window=emulator_window,
require_fast_start=True)
_forward_port(instance.device, ports)
return instance