chromium/chrome/test/variations/drivers/android.py

# 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.

import os
import logging
import posixpath

from contextlib import contextmanager
from typing import List, Optional

import attr

from chrome.test.variations.drivers import DriverFactory
# This import also adds `devil` and `build/android` to `sys.path`.
from chrome.test.variations.test_utils import android
from selenium import webdriver

from devil.android import device_temp_file
from devil.android import device_utils
from devil.android.sdk import intent


@attr.attrs()
class AndroidDriverFactory(DriverFactory):
  channel: str = attr.attrib()
  avd_config: Optional[str] = attr.attrib()
  enabled_emulator_window: bool = attr.attrib()
  ports: List[int] = attr.attrib()

  #override
  def __attrs_post_init__(self):
    super().__attrs_post_init__()
    self._instance = android.launch_emulator(
      avd_config=self.avd_config,
      emulator_window=self.enabled_emulator_window,
      ports=self.ports)
    self._device_temp_dir = device_temp_file.NamedDeviceTemporaryDirectory(
      self.device.adb)
    self._install_package()

  def _install_package(self):
    self._package_name = android.install_chrome(self.channel, self.device)
    self.device.ClearApplicationState(self.package_name)
    logging.info('Installed Chrome (%s)', self.package_name)

  #override
  @property
  def supports_startup_timeout(self) -> bool:
    # Android doesn't support browser startup timeout.
    return False

  @property
  def device_temp_dir(self) -> device_temp_file.NamedDeviceTemporaryDirectory:
    return self._device_temp_dir

  @property
  def package_name(self) -> str:
    return self._package_name

  @property
  def activity_name(self) -> Optional[str]:
    return None

  @property
  def device(self) -> device_utils.DeviceUtils:
    return self._instance.device

  def _push_seed(self, seed_file: str):
    local_seed_file = posixpath.join(
      self.device_temp_dir.name, os.path.basename(seed_file))
    self.device.adb.Push(seed_file, local_seed_file)

    uid = self.device.GetUidForPackage(self.package_name)
    self.device.RunShellCommand(
      ['chown', uid, local_seed_file], as_root=True)
    return local_seed_file

  #override
  @contextmanager
  def create_driver(
    self,
    seed_file: Optional[str] = None,
    options: Optional[webdriver.ChromeOptions] = None
    ) -> webdriver.Remote:
    options = options or self.default_options
    options.enable_mobile(
      android_package=self.package_name,
      android_activity=self.activity_name,
    )
    # We clean up the application dir and place several files there, so
    # we need to keep the data when running webdriver.
    options.mobile_options['androidKeepAppDataDir'] = True

    if seed_file:
      installed_seed_path = self._push_seed(seed_file)
      logging.info('Installed seed at (%s)', installed_seed_path)
      options.add_argument(
        f'variations-test-seed-path={installed_seed_path}')
      options.add_argument(f'--fake-variations-channel={self.channel}')

    driver = None
    try:
      yield (driver := webdriver.Chrome(service=self.get_driver_service(),
                                        options=options))
    finally:
      if driver:
        driver.quit()

  #override
  def close(self):
    self._instance.Stop()


@attr.attrs()
class WebviewDriverFactory(AndroidDriverFactory):

  #override
  @property
  def package_name(self):
    return 'org.chromium.webview_shell'

  #override
  @property
  def activity_name(self):
    return '.WebViewBrowserActivity'

  #override
  def _install_package(self):
    # Clear the system webview shell.
    self.device.ClearApplicationState(self.package_name)

    ver = android.install_webview(self.channel, self.device)
    logging.info('Installed webview (%s)', ver)

    # Launch shell once to create local state files.
    self.device.StartActivity(
        intent.Intent(
            action='android.intent.action.MAIN',
            package=self.package_name,
            activity='.WebViewBrowserActivity'),
        blocking=True)
    self.device.ForceStop(self.package_name)

  #override
  def _push_seed(self, seed_file: str):
    # Variation seeds for webview are always being loaded from app_webview.
    package_dir = self.device.GetApplicationDataDirectory(self.package_name)
    app_data_dir = posixpath.join(package_dir, 'app_webview')
    local_seed_file = super()._push_seed(seed_file)

    seed_path = posixpath.join(app_data_dir, 'variations_seed')
    seed_new_path = posixpath.join(app_data_dir, 'variations_seed_new')
    self.device.RunShellCommand(
      ['cp', local_seed_file, seed_path], check_return=True, as_root=True)
    self.device.RunShellCommand(
      ['cp', local_seed_file, seed_new_path], check_return=True, as_root=True)
    return local_seed_file