chromium/fuchsia_web/runners/cast/cast_component.cc

// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "fuchsia_web/runners/cast/cast_component.h"

#include <fuchsia/ui/views/cpp/fidl.h>
#include <lib/async/default.h>
#include <lib/fidl/cpp/binding.h>
#include <lib/trace/event.h>

#include <algorithm>
#include <string>
#include <string_view>
#include <utility>

#include "base/auto_reset.h"
#include "base/files/file_util.h"
#include "base/fuchsia/fuchsia_logging.h"
#include "base/functional/bind.h"
#include "base/path_service.h"
#include "base/task/current_thread.h"
#include "components/cast/message_port/fuchsia/create_web_message.h"
#include "components/cast/message_port/fuchsia/message_port_fuchsia.h"
#include "components/cast/message_port/platform_message_port.h"
#include "fuchsia_web/runners/cast/cast_runner.h"
#include "fuchsia_web/runners/cast/cast_streaming.h"
#include "fuchsia_web/runners/common/web_component.h"

namespace {

constexpr int kBindingsFailureExitCode = 129;
constexpr int kRewriteRulesProviderDisconnectExitCode = 130;

fuchsia::web::ConsoleLogLevel SeverityToConsoleLogLevel(
    fuchsia::diagnostics::Severity severity) {
  switch (severity) {
    case fuchsia::diagnostics::Severity::TRACE:
    case fuchsia::diagnostics::Severity::DEBUG:
      return fuchsia::web::ConsoleLogLevel::DEBUG;
    case fuchsia::diagnostics::Severity::INFO:
      return fuchsia::web::ConsoleLogLevel::INFO;
    case fuchsia::diagnostics::Severity::WARN:
      return fuchsia::web::ConsoleLogLevel::WARN;
    case fuchsia::diagnostics::Severity::ERROR:
      return fuchsia::web::ConsoleLogLevel::ERROR;
    case fuchsia::diagnostics::Severity::FATAL:
      // FATAL means none per the FIDL definition.
      return fuchsia::web::ConsoleLogLevel::NONE;
  }

  // The safest thing to do for unrecognized values is to not log.
  return fuchsia::web::ConsoleLogLevel::NONE;
}

}  // namespace

CastComponent::Params::Params() = default;
CastComponent::Params::Params(Params&&) = default;
CastComponent::Params::~Params() = default;

bool CastComponent::Params::AreComplete() const {
  if (application_config.IsEmpty())
    return false;
  if (!api_bindings_client->HasBindings())
    return false;
  if (!initial_url_rewrite_rules)
    return false;
  if (!media_settings)
    return false;
  return true;
}

CastComponent::CastComponent(std::string_view debug_name,
                             WebContentRunner* runner,
                             CastComponent::Params params,
                             bool is_headless)
    : WebComponent(debug_name, runner, std::move(params.startup_context)),
      is_headless_(is_headless),
      application_config_(std::move(params.application_config)),
      url_rewrite_rules_provider_(std::move(params.url_rewrite_rules_provider)),
      initial_url_rewrite_rules_(
          std::move(params.initial_url_rewrite_rules.value())),
      api_bindings_client_(std::move(params.api_bindings_client)),
      application_context_(std::move(params.application_context),
                           async_get_default_dispatcher()),
      media_settings_(std::move(params.media_settings.value())),
      headless_disconnect_watch_(FROM_HERE),
      trace_flow_id_(params.trace_flow_id) {
  TRACE_DURATION("cast_runner", "Create CastComponent", "name", debug_name_);
  TRACE_FLOW_STEP("cast_runner", "CastComponent", trace_flow_id_);

  base::AutoReset<bool> constructor_active_reset(&constructor_active_, true);
  component_controller_.Bind(std::move(params.controller_request));
}

CastComponent::~CastComponent() = default;

void CastComponent::StartComponent() {
  TRACE_DURATION("cast_runner", "StartComponent");
  TRACE_FLOW_STEP("cast_runner", "CastComponent", trace_flow_id_);

  if (application_config_.has_enable_remote_debugging() &&
      application_config_.enable_remote_debugging()) {
    WebComponent::EnableRemoteDebugging();
  }

  WebComponent::StartComponent();

  connector_ = std::make_unique<NamedMessagePortConnectorFuchsia>(frame());

  url_rewrite_rules_provider_.set_error_handler([this](zx_status_t status) {
    ZX_LOG_IF(ERROR, status != ZX_OK, status)
        << "UrlRequestRewriteRulesProvider disconnected.";
    DestroyComponent(kRewriteRulesProviderDisconnectExitCode);
  });
  OnRewriteRulesReceived(std::move(initial_url_rewrite_rules_));

  frame()->SetMediaSettings(std::move(media_settings_));
  frame()->ConfigureInputTypes(fuchsia::web::InputTypes::ALL,
                               fuchsia::web::AllowInputState::DENY);
  if (application_config_.has_initial_min_console_log_severity()) {
    frame()->SetJavaScriptLogLevel(SeverityToConsoleLogLevel(
        application_config_.initial_min_console_log_severity()));
  }

  if (IsAppConfigForCastStreaming(application_config_)) {
    // TODO(crbug.com/40131115): Remove this once the Cast Streaming Receiver
    // component has been implemented.

    // Register the MessagePort for the Cast Streaming Receiver.
    std::unique_ptr<cast_api_bindings::MessagePort> message_port_for_web_engine;
    std::unique_ptr<cast_api_bindings::MessagePort> message_port_for_agent;
    cast_api_bindings::CreatePlatformMessagePortPair(
        &message_port_for_agent, &message_port_for_web_engine);
    frame()->PostMessage(
        GetMessagePortOriginForAppId(application_config_.id()),
        CreateWebMessage("", std::move(message_port_for_web_engine)),
        [this](fuchsia::web::Frame_PostMessage_Result result) {
          if (result.is_err()) {
            DestroyComponent(kBindingsFailureExitCode);
          }
        });
    api_bindings_client_->OnPortConnected(kCastStreamingMessagePortName,
                                          std::move(message_port_for_agent));
  }

  api_bindings_client_->AttachToFrame(
      frame(), connector_.get(),
      base::BindOnce(&CastComponent::DestroyComponent, base::Unretained(this),
                     kBindingsFailureExitCode));

  // Media loading has to be unblocked by the agent via the
  // ApplicationController.
  frame()->SetBlockMediaLoading(true);

  if (application_config_.has_force_content_dimensions()) {
    frame()->ForceContentDimensions(std::make_unique<fuchsia::ui::gfx::vec2>(
        application_config_.force_content_dimensions()));
  }

  application_controller_ = std::make_unique<ApplicationControllerImpl>(
      frame(), application_context_, trace_flow_id_);

  // Apply application-specific web permissions to the fuchsia.web.Frame.
  if (application_config_.has_permissions()) {
    // TODO(crbug.com/40724536): Replace this with the PermissionManager API
    // when available.
    const std::string origin =
        GURL(application_config_.web_url()).DeprecatedGetOriginAsURL().spec();
    for (auto& permission : application_config_.permissions()) {
      fuchsia::web::PermissionDescriptor permission_clone;
      zx_status_t status = permission.Clone(&permission_clone);
      ZX_DCHECK(status == ZX_OK, status);
      const bool all_origins =
          permission_clone.has_type() &&
          (permission_clone.type() ==
           fuchsia::web::PermissionType::PROTECTED_MEDIA_IDENTIFIER);
      frame()->SetPermissionState(std::move(permission_clone),
                                  all_origins ? "*" : origin,
                                  fuchsia::web::PermissionState::GRANTED);
    }
  }

  fuchsia::web::ContentAreaSettings settings;
  // Disable scrollbars on all Cast applications.
  settings.set_hide_scrollbars(true);

  // Get the theme from `fuchsia.settings.Display`, except in headless mode
  // where the service may not be available.
  if (!is_headless_) {
    settings.set_theme(fuchsia::settings::ThemeType::DEFAULT);
  }

  frame()->SetContentAreaSettings(std::move(settings));
}

void CastComponent::DestroyComponent(int64_t exit_code) {
  DCHECK(!constructor_active_);

  TRACE_DURATION("cast_runner", "CastComponent::DestroyComponent", "name",
                 debug_name_);
  TRACE_FLOW_END("cast_runner", "CastComponent", trace_flow_id_);

  // If the `application_controller_` is available then use it to inform the
  // Agent of the `exit_code`. For graceful teardown (whether self-initiated by
  // the web content, or due to a component `Stop()` request) the Agent expects
  // to be notified with `exit_code` set to `ZX_OK`.  All other `exit_code`
  // values, or failure to report one, indicate teardown due to error.
  if (application_controller_) {
    auto result = application_context_->OnApplicationExit(exit_code);
    LOG_IF(ERROR, result.is_error())
        << base::FidlMethodResultErrorMessage(result, "OnApplicationExit");
  }

  // frame() is about to be destroyed, so there is no need to perform cleanup
  // such as removing before-load JavaScripts.
  api_bindings_client_->DetachFromFrame(frame());
  connector_->DetachFromFrame();

  // If the `application_config_` specifies that the web content should be
  // granted an extended shutdown delay, then use `CloseFrameWithTimeout()` to
  // close it before tearing down the component.
  if (application_config_.has_shutdown_delay()) {
    CloseFrameWithTimeout(
        base::TimeDelta::FromZxDuration(application_config_.shutdown_delay()));
  }

  WebComponent::DestroyComponent(exit_code);
}

void CastComponent::OnRewriteRulesReceived(
    std::vector<fuchsia::web::UrlRequestRewriteRule> rewrite_rules) {
  frame()->SetUrlRequestRewriteRules(std::move(rewrite_rules), [this]() {
    url_rewrite_rules_provider_->GetUrlRequestRewriteRules(
        fit::bind_member(this, &CastComponent::OnRewriteRulesReceived));
  });
}

void CastComponent::OnNavigationStateChanged(
    fuchsia::web::NavigationState change,
    OnNavigationStateChangedCallback callback) {
  if (change.has_is_main_document_loaded() &&
      change.is_main_document_loaded()) {
    std::string connect_message;
    std::unique_ptr<cast_api_bindings::MessagePort> connect_port;
    connector_->GetConnectMessage(&connect_message, &connect_port);

    // Send the NamedMessagePortConnector handshake to the page.
    frame()->PostMessage(
        "*", CreateWebMessage(connect_message, std::move(connect_port)),
        [](fuchsia::web::Frame_PostMessage_Result result) {
          DCHECK(result.is_response());
        });
  }

  WebComponent::OnNavigationStateChanged(std::move(change),
                                         std::move(callback));
}

void CastComponent::CreateViewWithViewRef(
    zx::eventpair view_token,
    fuchsia::ui::views::ViewRefControl control_ref,
    fuchsia::ui::views::ViewRef view_ref) {
  TRACE_DURATION("cast_runner", "CastComponent::CreateViewWithViewRef");
  TRACE_FLOW_STEP("cast_runner", "CastComponent", trace_flow_id_);

  if (is_headless_) {
    // For headless CastComponents, |view_token| does not actually connect to a
    // Scenic View. It is merely used as a conduit for propagating termination
    // signals.
    headless_view_token_ = std::move(view_token);
    base::CurrentIOThread::Get()->WatchZxHandle(
        headless_view_token_.get(), false /* persistent */,
        ZX_SOCKET_PEER_CLOSED, &headless_disconnect_watch_, this);

    frame()->EnableHeadlessRendering();
    return;
  }

  WebComponent::CreateViewWithViewRef(
      std::move(view_token), std::move(control_ref), std::move(view_ref));
}

void CastComponent::CreateView2(fuchsia::ui::app::CreateView2Args view_args) {
  TRACE_DURATION("cast_runner", "CastComponent::CreateView2");
  TRACE_FLOW_STEP("cast_runner", "CastComponent", trace_flow_id_);

  if (is_headless_) {
    frame()->EnableHeadlessRendering();
    return;
  }

  WebComponent::CreateView2(std::move(view_args));
}

void CastComponent::Kill() {
  // The Component Framework has requested forcible teardown, so immediately
  // destroy this component.
  DestroyComponent(ZX_OK);
}

void CastComponent::Stop() {
  TRACE_DURATION("cast_runner", "CastComponent::Stop");
  TRACE_FLOW_STEP("cast_runner", "CastComponent", trace_flow_id_);

  // The Component Framework has requested graceful teardown, so request that
  // the `Frame` close the page. The framework typically allows components
  // several seconds to complete teardown, before forcibly `Kill()`ing them.
  // Using a timeout of 1 minute here effectively ensures that the content
  // has until the framework timeout expires, in which to teardown.
  constexpr base::TimeDelta kStopTimeout = base::Minutes(1u);
  frame()->Close(std::move(fuchsia::web::FrameCloseRequest().set_timeout(
      kStopTimeout.ToZxDuration())));
}

void CastComponent::OnZxHandleSignalled(zx_handle_t handle,
                                        zx_signals_t signals) {
  DCHECK_EQ(signals, ZX_SOCKET_PEER_CLOSED);
  DCHECK(is_headless_);

  frame()->DisableHeadlessRendering();
}