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

from __future__ import absolute_import
import logging
import os
import posixpath
import shutil

from py_utils import exc_util

from telemetry.core import exceptions
from telemetry.internal.platform import android_platform_backend as \
  android_platform_backend_module
from telemetry.internal.backends.chrome import android_minidump_symbolizer
from telemetry.internal.backends.chrome import chrome_browser_backend
from telemetry.internal.backends.chrome import minidump_finder
from telemetry.internal.browser import user_agent
from telemetry.internal.results import artifact_logger

from devil.android import app_ui
from devil.android import device_signal
from devil.android.sdk import intent
from devil.android.sdk import version_codes


class AndroidBrowserBackend(chrome_browser_backend.ChromeBrowserBackend):
  """The backend for controlling a browser instance running on Android."""
  DEBUG_ARTIFACT_PREFIX = 'android_debug_info'

  def __init__(self, android_platform_backend, browser_options,
               browser_directory, profile_directory, backend_settings,
               build_dir=None):
    """
    Args:
      android_platform_backend: The
          android_platform_backend.AndroidPlatformBackend instance to use.
      browser_options: The browser_options.BrowserOptions instance to use.
      browser_directory: A string containing the path to the directory on the
          device where the browser is installed.
      profile_directory: A string containing a path to the directory on the
          device to store browser profile information in.
      backend_settings: The
          android_browser_backend_settings.AndroidBrowserBackendSettings
          instance to use.
      build_dir: A string containing a path to the directory on the host that
          the browser was built in, for finding debug artifacts. Can be None if
          the browser was not locally built, or the directory otherwise cannot
          be determined.
    """
    assert isinstance(android_platform_backend,
                      android_platform_backend_module.AndroidPlatformBackend)
    super(AndroidBrowserBackend, self).__init__(
        android_platform_backend,
        browser_options=browser_options,
        browser_directory=browser_directory,
        profile_directory=profile_directory,
        supports_extensions=False,
        supports_tab_control=backend_settings.supports_tab_control,
        build_dir=build_dir)
    self._backend_settings = backend_settings

    # Initialize fields so that an explosion during init doesn't break in Close.
    self._saved_sslflag = ''
    self._app_ui = None

    # Set the debug app if needed.
    self.platform_backend.SetDebugApp(self._backend_settings.package)

  @property
  def log_file_path(self):
    return None

  @property
  def device(self):
    return self.platform_backend.device

  @property
  def supports_app_ui_interactions(self):
    return True

  def GetAppUi(self):
    if self._app_ui is None:
      self._app_ui = app_ui.AppUi(self.device, package=self.package)
    return self._app_ui

  def _StopBrowser(self):
    # Note: it's important to stop and _not_ kill the browser app, since
    # stopping also clears the app state in Android's activity manager.
    self.platform_backend.StopApplication(self._backend_settings.package)

  def Start(self, startup_args):
    assert not startup_args, (
        'Startup arguments for Android should be set during '
        'possible_browser.SetUpEnvironment')
    self._dump_finder = minidump_finder.MinidumpFinder(
        self.browser.platform.GetOSName(), self.browser.platform.GetArchName())
    user_agent_dict = user_agent.GetChromeUserAgentDictFromType(
        self.browser_options.browser_user_agent_type)
    self.device.StartActivity(
        intent.Intent(package=self._backend_settings.package,
                      activity=self._backend_settings.activity,
                      action=None, data='about:blank', category=None,
                      extras=user_agent_dict),
        blocking=True)
    try:
      self.BindDevToolsClient()
    except:
      self.Close()
      raise

  def BindDevToolsClient(self):
    super(AndroidBrowserBackend, self).BindDevToolsClient()
    package = self.devtools_client.GetVersion().get('Android-Package')
    if package is None:
      logging.warning('Could not determine package name from DevTools client.')
    elif package == self._backend_settings.package:
      logging.info('Successfully connected to %s DevTools client', package)
    else:
      raise exceptions.BrowserGoneException(
          self.browser, 'Expected connection to %s but got %s.' % (
              self._backend_settings.package, package))

  def _FindDevToolsPortAndTarget(self):
    devtools_port = self._backend_settings.GetDevtoolsRemotePort(self.device)
    browser_target = None  # Use default
    return devtools_port, browser_target

  def Foreground(self):
    package = self._backend_settings.package
    activity = self._backend_settings.activity
    self.device.StartActivity(
        intent.Intent(package=package,
                      activity=activity,
                      action=None,
                      flags=[intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED]),
        blocking=False)
    # TODO(crbug.com/601052): The following waits for any UI node for the
    # package launched to appear on the screen. When the referenced bug is
    # fixed, remove this workaround and just switch blocking above to True.
    try:
      app_ui.AppUi(self.device).WaitForUiNode(package=package)
    except Exception:
      raise exceptions.BrowserGoneException(
          self.browser,
          'Timed out waiting for browser to come back foreground.')

  def Background(self):
    package = 'org.chromium.push_apps_to_background'
    activity = package + '.PushAppsToBackgroundActivity'
    self.device.StartActivity(
        intent.Intent(
            package=package,
            activity=activity,
            action=None,
            flags=[intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED]),
        blocking=True)

  def ForceJavaHeapGarbageCollection(self):
    # Send USR1 signal to force GC on Chrome processes forked from Zygote.
    # (c.f. crbug.com/724032)
    self.device.KillAll(
        self._backend_settings.package,
        exact=False,  # Send signal to children too.
        signum=device_signal.SIGUSR1,
        as_root=True)

  @property
  def processes(self):
    try:
      zygotes = self.device.ListProcesses('zygote')
      zygote_pids = set(p.pid for p in zygotes)
      assert zygote_pids, 'No Android zygote found'
      processes = self.device.ListProcesses(self._backend_settings.package)
      return [p for p in processes if p.ppid in zygote_pids]
    except Exception as exc:
      # Re-raise as an AppCrashException to get further diagnostic information.
      # In particular we also get the values of all local variables above.
      raise exceptions.AppCrashException(
          self.browser, 'Error getting browser PIDs: %s' % exc)

  def GetPid(self):
    browser_processes = self._GetBrowserProcesses()
    assert len(browser_processes) <= 1, (
        'Found too many browsers: %r' % browser_processes)
    if not browser_processes:
      raise exceptions.BrowserGoneException(self.browser)
    return browser_processes[0].pid

  def _GetBrowserProcesses(self):
    """Return all possible browser processes."""
    package = self._backend_settings.package
    return [p for p in self.processes if p.name == package]

  @property
  def package(self):
    return self._backend_settings.package

  @property
  def activity(self):
    return self._backend_settings.activity

  def __del__(self):
    self.Close()

  @exc_util.BestEffort
  def Close(self):
    super(AndroidBrowserBackend, self).Close()
    self._StopBrowser()
    if self._tmp_minidump_dir:
      shutil.rmtree(self._tmp_minidump_dir, ignore_errors=True)
      self._tmp_minidump_dir = None

  def IsBrowserRunning(self):
    return len(self._GetBrowserProcesses()) > 0

  def GetStandardOutput(self):
    return self.platform_backend.GetStandardOutput()

  def PullMinidumps(self):
    self._PullMinidumpsAndAdjustMtimes()

  def CollectDebugData(self, log_level):
    """Collects various information that may be useful for debugging.

    In addition to any data collected by parents' implementation, this also
    collects the following and stores it as artifacts:
      1. UI state of the device
      2. Logcat
      3. Symbolized logcat
      4. Tombstones

    Args:
      log_level: The logging level to use from the logging module, e.g.
          logging.ERROR.

    Returns:
      A debug_data.DebugData object containing the collected data.
    """
    # Store additional debug information as artifacts.
    # We do these in a mixed order so that higher priority ones are done first.
    # This is so that if an error occurs during debug data collection (e.g.
    # adb issues), we're more likely to end up with useful debug information.
    suffix = artifact_logger.GetTimestampSuffix()
    self._StoreLogcatAsArtifact(suffix)
    retval = super(AndroidBrowserBackend, self).CollectDebugData(log_level)
    self._StoreUiDumpAsArtifact(suffix)
    self._StoreTombstonesAsArtifact(suffix)
    return retval

  def SymbolizeMinidump(self, minidump_path):
    dump_symbolizer = android_minidump_symbolizer.AndroidMinidumpSymbolizer(
        self._dump_finder, self.build_dir)
    stack = dump_symbolizer.SymbolizeMinidump(minidump_path)
    if not stack:
      return (False, 'Failed to symbolize minidump.')
    self._symbolized_minidump_paths.add(minidump_path)
    return (True, stack)

  def _PullMinidumpsAndAdjustMtimes(self):
    """Pulls any minidumps from the device to the host.

    Skips pulling any dumps that have already been pulled. The modification time
    of any pulled dumps will be set to the modification time of the dump on the
    device, offset by any difference in clocks between the device and host.
    """
    # The offset is (device_time - host_time), so a positive value means that
    # the device clock is ahead.
    time_offset = self.platform_backend.GetDeviceHostClockOffset()
    device = self.platform_backend.device

    device_dump_path = posixpath.join(
        self.platform_backend.GetDumpLocation(self.package),
        'Crashpad', 'pending')
    if not device.PathExists(device_dump_path):
      logging.warning(
          'Device minidump path %s does not exist - not attempting to pull '
          'minidumps', device_dump_path)
      return
    device_dumps = device.ListDirectory(device_dump_path)
    for dump_filename in device_dumps:
      # Skip any .lock files since they're not useful and are prone to being
      # deleted by the time we try to actually pull them.
      if dump_filename.endswith('.lock'):
        continue
      host_path = os.path.join(self._tmp_minidump_dir, dump_filename)
      if os.path.exists(host_path):
        continue
      device_path = posixpath.join(device_dump_path, dump_filename)
      # Skip any files that have a .lock file associated with them, as that
      # implies that the minidump hasn't been fully written to disk yet.
      device_lock_path = device_path + '.lock'
      if device.FileExists(device_lock_path):
        logging.debug('Not pulling file %s because a .lock file exists for it',
                      device_path)
        continue
      device.PullFile(device_path, host_path)
      # Set the local version's modification time to the device's
      # The mtime returned by device_utils.StatPath only has a resolution down
      # to the minute, so we can't use that.
      # On Android L and earlier, 'stat' is not available, so fall back to
      # device_utils.StatPath and adjust the returned value so that it's as new
      # as possible while still fitting in that 1 minute resolution.
      device_mtime = None
      if device.build_version_sdk >= version_codes.MARSHMALLOW:
        device_mtime = device.RunShellCommand(
            ['stat', '-c', '%Y', device_path], single_line=True)
        device_mtime = int(device_mtime.strip())
      else:
        stat_output = device.StatPath(device_path)
        device_mtime = stat_output['st_mtime'] + 59
      host_mtime = device_mtime - time_offset
      os.utime(host_path, (host_mtime, host_mtime))

  def _StoreUiDumpAsArtifact(self, suffix):
    try:
      ui_dump = self.platform_backend.GetSystemUi().ScreenDump()
      artifact_name = posixpath.join(
          self.DEBUG_ARTIFACT_PREFIX, 'ui_dump-%s.txt' % suffix)
      artifact_logger.CreateArtifact(artifact_name, '\n'.join(ui_dump))
    except Exception:  # pylint: disable=broad-except
      logging.exception('Failed to store UI dump')

  def _StoreLogcatAsArtifact(self, suffix):
    logcat = self.platform_backend.GetLogCat()
    artifact_name = posixpath.join(
        self.DEBUG_ARTIFACT_PREFIX, 'logcat-%s.txt' % suffix)
    artifact_logger.CreateArtifact(artifact_name, logcat)

    symbolized_logcat = self.platform_backend.SymbolizeLogCat(logcat)
    if symbolized_logcat is None:
      symbolized_logcat = 'Failed to symbolize logcat. Is the script available?'
    artifact_name = posixpath.join(
        self.DEBUG_ARTIFACT_PREFIX, 'symbolized_logcat-%s.txt' % suffix)
    artifact_logger.CreateArtifact(artifact_name, symbolized_logcat)

  def _StoreTombstonesAsArtifact(self, suffix):
    tombstones = self.platform_backend.GetTombstones()
    if tombstones is None:
      tombstones = 'Failed to get tombstones. Is the script available?'
    artifact_name = posixpath.join(
        self.DEBUG_ARTIFACT_PREFIX, 'tombstones-%s.txt' % suffix)
    artifact_logger.CreateArtifact(artifact_name, tombstones)
