// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chromecast/browser/cast_web_contents_impl.h"
#include <optional>
#include <string_view>
#include <utility>
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/no_destructor.h"
#include "base/ranges/algorithm.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/sequenced_task_runner.h"
#include "base/trace_event/trace_event.h"
#include "base/values.h"
#include "chromecast/base/cast_features.h"
#include "chromecast/base/chromecast_switches.h"
#include "chromecast/base/metrics/cast_metrics_helper.h"
#include "chromecast/browser/cast_browser_process.h"
#include "chromecast/browser/cast_navigation_ui_data.h"
#include "chromecast/browser/cast_permission_user_data.h"
#include "chromecast/browser/cast_session_id_map.h"
#include "chromecast/browser/devtools/remote_debugging_server.h"
#include "chromecast/common/mojom/activity_url_filter.mojom.h"
#include "chromecast/common/mojom/queryable_data_store.mojom.h"
#include "chromecast/common/queryable_data.h"
#include "chromecast/net/connectivity_checker.h"
#include "components/cast/message_port/blink_message_port_adapter.h"
#include "components/cast/message_port/cast/message_port_cast.h"
#include "components/media_control/browser/media_blocker.h"
#include "components/media_control/mojom/media_playback_options.mojom.h"
#include "content/public/browser/message_port_provider.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/common/bindings_policy.h"
#include "media/mojo/buildflags.h"
#include "mojo/public/cpp/bindings/associated_remote.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "net/base/net_errors.h"
#include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h"
#include "third_party/blink/public/common/navigation/navigation_params.h"
#include "third_party/blink/public/common/renderer_preferences/renderer_preferences.h"
#include "third_party/blink/public/mojom/autoplay/autoplay.mojom.h"
#include "third_party/blink/public/mojom/favicon/favicon_url.mojom.h"
#include "third_party/blink/public/mojom/loader/resource_load_info.mojom.h"
#include "third_party/skia/include/core/SkColor.h"
#include "url/gurl.h"
namespace chromecast {
namespace {
// IDs start at 1, since 0 is reserved for the root content window.
size_t next_tab_id = 1;
// Next id for id()
size_t next_id = 0;
// Remove the given CastWebContents pointer from the global instance vector.
void RemoveCastWebContents(CastWebContents* instance) {
auto& all_cast_web_contents = CastWebContents::GetAll();
auto it = base::ranges::find(all_cast_web_contents, instance);
if (it != all_cast_web_contents.end()) {
all_cast_web_contents.erase(it);
}
}
content::mojom::RendererType ToContentRendererType(mojom::RendererType type) {
switch (type) {
case mojom::RendererType::DEFAULT_RENDERER:
return content::mojom::RendererType::DEFAULT_RENDERER;
case mojom::RendererType::MOJO_RENDERER:
return content::mojom::RendererType::MOJO_RENDERER;
case mojom::RendererType::REMOTING_RENDERER:
return content::mojom::RendererType::REMOTING_RENDERER;
}
}
} // namespace
// static
std::vector<CastWebContents*>& CastWebContents::GetAll() {
static base::NoDestructor<std::vector<CastWebContents*>> instance;
return *instance;
}
// static
CastWebContents* CastWebContents::FromWebContents(
content::WebContents* web_contents) {
auto& all_cast_web_contents = CastWebContents::GetAll();
auto it = base::ranges::find(all_cast_web_contents, web_contents,
&CastWebContents::web_contents);
if (it == all_cast_web_contents.end()) {
return nullptr;
}
return *it;
}
void CastWebContentsImpl::RenderProcessReady(content::RenderProcessHost* host) {
DCHECK(host->IsReady());
const base::Process& process = host->GetProcess();
for (auto& observer : observers_) {
observer->OnRenderProcessReady(process.Pid());
}
}
void CastWebContentsImpl::RenderProcessExited(
content::RenderProcessHost* host,
const content::ChildProcessTerminationInfo& info) {
RemoveRenderProcessHostObserver();
}
void CastWebContentsImpl::RenderProcessHostDestroyed(
content::RenderProcessHost* host) {
RemoveRenderProcessHostObserver();
}
void CastWebContentsImpl::RemoveRenderProcessHostObserver() {
if (main_process_host_) {
main_process_host_->RemoveObserver(this);
}
main_process_host_ = nullptr;
}
CastWebContentsImpl::CastWebContentsImpl(content::WebContents* web_contents,
mojom::CastWebViewParamsPtr params)
: CastWebContentsImpl(web_contents,
std::move(params),
nullptr /* parent */) {}
CastWebContentsImpl::CastWebContentsImpl(content::WebContents* web_contents,
mojom::CastWebViewParamsPtr params,
CastWebContents* parent)
: web_contents_(web_contents),
params_(std::move(params)),
page_state_(PageState::IDLE),
last_state_(PageState::IDLE),
remote_debugging_server_(
shell::CastBrowserProcess::GetInstance()->remote_debugging_server()),
media_blocker_(params_->use_media_blocker
? std::make_unique<CastMediaBlocker>(web_contents_)
: nullptr),
main_process_host_(nullptr),
parent_cast_web_contents_(parent),
tab_id_(params_->is_root_window ? 0 : next_tab_id++),
id_(next_id++),
main_frame_loaded_(false),
closing_(false),
stopped_(false),
stop_notified_(false),
notifying_(false),
last_error_(net::OK),
task_runner_(base::SequencedTaskRunner::GetCurrentDefault()),
weak_factory_(this) {
DCHECK(web_contents_);
DCHECK(web_contents_->GetController().IsInitialNavigation());
DCHECK(!web_contents_->IsLoading());
DCHECK(web_contents_->GetPrimaryMainFrame());
main_process_host_ = web_contents_->GetPrimaryMainFrame()->GetProcess();
DCHECK(main_process_host_);
main_process_host_->AddObserver(this);
CastWebContents::GetAll().push_back(this);
content::WebContentsObserver::Observe(web_contents_);
// The URL rewrite rules manager must be initialized only for the root
// CastWebContents that is created with this public ctor. All the inner
// CastWebContents created in |InnerWebContentsCreated()| callback will use
// the private ctor with |parent| specified which allows sharing the same
// manager, so that the whole Cast session applies the same rules.
if (params_->enable_url_rewrite_rules) {
if (!parent_cast_web_contents_) {
url_rewrite_rules_manager_.emplace();
}
url_rewrite_rules_manager()->AddWebContents(web_contents_);
}
if (params_->enabled_for_dev) {
LOG(INFO) << "Enabling dev console for CastWebContentsImpl";
remote_debugging_server_->EnableWebContentsForDebugging(web_contents_);
}
// TODO(yucliu): Change the flag name to kDisableCmaRenderer in a latter diff.
if (GetSwitchValueBoolean(switches::kDisableMojoRenderer, false) &&
params_->renderer_type == mojom::RendererType::MOJO_RENDERER) {
params_->renderer_type = mojom::RendererType::DEFAULT_RENDERER;
} else if (GetSwitchValueBoolean(switches::kForceMojoRenderer, false)) {
#if BUILDFLAG(ENABLE_MOJO_RENDERER) && BUILDFLAG(ENABLE_CAST_RENDERER)
LOG(INFO) << "Enabling mojo renderer";
#else
LOG(ERROR)
<< "The switch " << switches::kForceMojoRenderer
<< " was used, but either the mojo renderer or cast renderer is "
"disabled via GN args. Check the values of enable_cast_renderer and "
"mojo_media_services in your GN args";
#endif // BUILDFLAG(ENABLE_MOJO_RENDERER) && BUILDFLAG(ENABLE_CAST_RENDERER)
params_->renderer_type = mojom::RendererType::MOJO_RENDERER;
}
web_contents_->SetPageBaseBackgroundColor(chromecast::GetSwitchValueColor(
switches::kCastAppBackgroundColor, SK_ColorBLACK));
if (params_->enable_webui_bindings_permission) {
web_contents_->GetPrimaryMainFrame()->AllowBindings(
content::kWebUIBindingsPolicySet);
}
}
CastWebContentsImpl::~CastWebContentsImpl() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(!notifying_) << "Do not destroy CastWebContents during observer "
"notification!";
RemoveRenderProcessHostObserver();
DisableDebugging();
observers_.Clear();
for (auto& observer : sync_observers_) {
observer.ResetCastWebContents();
}
RemoveCastWebContents(this);
}
int CastWebContentsImpl::tab_id() const {
return tab_id_;
}
int CastWebContentsImpl::id() const {
return id_;
}
content::WebContents* CastWebContentsImpl::web_contents() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return web_contents_;
}
PageState CastWebContentsImpl::page_state() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return page_state_;
}
url_rewrite::UrlRequestRewriteRulesManager*
CastWebContentsImpl::url_rewrite_rules_manager() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (parent_cast_web_contents_) {
return parent_cast_web_contents_->url_rewrite_rules_manager();
}
return &*url_rewrite_rules_manager_;
}
const media_control::MediaBlocker* CastWebContentsImpl::media_blocker() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return media_blocker_.get();
}
void CastWebContentsImpl::AddRendererFeatures(base::Value::Dict features) {
renderer_features_ = std::move(features);
}
void CastWebContentsImpl::SetInterfacesForRenderer(
mojo::PendingRemote<mojom::RemoteInterfaces> remote_interfaces) {
remote_interfaces_.SetProvider(std::move(remote_interfaces));
}
void CastWebContentsImpl::SetUrlRewriteRules(
url_rewrite::mojom::UrlRequestRewriteRulesPtr rules) {
DCHECK(params_->enable_url_rewrite_rules);
if (!url_rewrite_rules_manager()->OnRulesUpdated(std::move(rules))) {
LOG(ERROR) << "URL rewrite rules update failed.";
}
}
void CastWebContentsImpl::LoadUrl(const GURL& url) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (api_bindings_ && !bindings_received_) {
LOG(INFO) << "Will load URL: " << url.possibly_invalid_spec()
<< " once bindings has been received.";
pending_load_url_ = url;
return;
}
if (!web_contents_) {
LOG(ERROR) << "Cannot load URL for deleted WebContents";
return;
}
if (closing_) {
LOG(ERROR) << "Cannot load URL for WebContents while closing";
return;
}
OnPageLoading();
LOG(INFO) << "Load url: " << url.possibly_invalid_spec();
web_contents_->GetController().LoadURL(url, content::Referrer(),
ui::PAGE_TRANSITION_TYPED, "");
UpdatePageState();
DCHECK_EQ(PageState::LOADING, page_state_);
NotifyPageState();
}
void CastWebContentsImpl::ClosePage() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!web_contents_ || closing_) {
return;
}
closing_ = true;
web_contents_->DispatchBeforeUnload(false /* auto_cancel */);
web_contents_->ClosePage();
// If the WebContents doesn't close within the specified timeout, then signal
// the page closure anyway so that the Delegate can delete the WebContents and
// stop the page itself.
task_runner_->PostDelayedTask(
FROM_HERE,
base::BindOnce(&CastWebContentsImpl::OnClosePageTimeout,
weak_factory_.GetWeakPtr()),
base::Milliseconds(1000));
}
void CastWebContentsImpl::Stop(int error_code) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (stopped_) {
UpdatePageState();
NotifyPageState();
return;
}
last_error_ = error_code;
closing_ = false;
stopped_ = true;
UpdatePageState();
DCHECK_NE(PageState::IDLE, page_state_);
DCHECK_NE(PageState::LOADING, page_state_);
DCHECK_NE(PageState::LOADED, page_state_);
NotifyPageState();
}
void CastWebContentsImpl::SetWebVisibilityAndPaint(bool visible) {
if (!web_contents_) {
return;
}
if (visible) {
web_contents_->WasShown();
} else {
web_contents_->WasHidden();
}
if (web_contents_->GetVisibility() != content::Visibility::VISIBLE) {
// Since we are managing the visibility, we need to ensure pages are
// unfrozen in the event this occurred while in the background.
web_contents_->SetPageFrozen(false);
}
}
void CastWebContentsImpl::BlockMediaLoading(bool blocked) {
if (media_blocker_) {
media_blocker_->BlockMediaLoading(blocked);
}
}
void CastWebContentsImpl::BlockMediaStarting(bool blocked) {
if (media_blocker_) {
media_blocker_->BlockMediaStarting(blocked);
}
}
void CastWebContentsImpl::EnableBackgroundVideoPlayback(bool enabled) {
if (media_blocker_) {
media_blocker_->EnableBackgroundVideoPlayback(enabled);
}
}
void CastWebContentsImpl::SetAppProperties(
const std::string& app_id,
const std::string& session_id,
bool is_audio_app,
const GURL& app_web_url,
bool enforce_feature_permissions,
const std::vector<int32_t>& feature_permissions,
const std::vector<std::string>& additional_feature_permission_origins) {
if (!web_contents_) {
return;
}
shell::CastNavigationUIData::SetAppPropertiesForWebContents(
web_contents_, session_id, is_audio_app);
new shell::CastPermissionUserData(
web_contents_, app_id, app_web_url, enforce_feature_permissions,
feature_permissions, additional_feature_permission_origins);
}
void CastWebContentsImpl::SetGroupInfo(const std::string& session_id,
bool is_multizone_launch) {
shell::CastSessionIdMap::GetInstance()->SetGroupInfo(session_id,
is_multizone_launch);
}
void CastWebContentsImpl::AddBeforeLoadJavaScript(uint64_t id,
std::string_view script) {
script_injector_.AddScriptForAllOrigins(id, std::string(script));
}
void CastWebContentsImpl::PostMessageToMainFrame(
const std::string& target_origin,
const std::string& data,
std::vector<blink::WebMessagePort> ports) {
DCHECK(!data.empty());
std::u16string data_utf16;
data_utf16 = base::UTF8ToUTF16(data);
// If origin is set as wildcard, no origin scoping would be applied.
std::optional<std::u16string> target_origin_utf16;
constexpr char kWildcardOrigin[] = "*";
if (target_origin != kWildcardOrigin) {
target_origin_utf16 = base::UTF8ToUTF16(target_origin);
}
content::MessagePortProvider::PostMessageToFrame(
web_contents()->GetPrimaryPage(), std::u16string(), target_origin_utf16,
data_utf16, std::move(ports));
}
void CastWebContentsImpl::ExecuteJavaScript(
const std::u16string& javascript,
base::OnceCallback<void(base::Value)> callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!web_contents_ || closing_ || !main_frame_loaded_ ||
!web_contents_->GetPrimaryMainFrame()) {
return;
}
web_contents_->GetPrimaryMainFrame()->ExecuteJavaScript(javascript,
std::move(callback));
}
void CastWebContentsImpl::ConnectToBindingsService(
mojo::PendingRemote<mojom::ApiBindings> api_bindings_remote) {
DCHECK(api_bindings_remote);
bindings_received_ = false;
named_message_port_connector_ =
std::make_unique<NamedMessagePortConnectorCast>(this);
named_message_port_connector_->RegisterPortHandler(base::BindRepeating(
&CastWebContentsImpl::OnPortConnected, base::Unretained(this)));
api_bindings_.Bind(std::move(api_bindings_remote));
// Fetch bindings and inject scripts into |script_injector_|.
api_bindings_->GetAll(base::BindOnce(&CastWebContentsImpl::OnBindingsReceived,
base::Unretained(this)));
}
void CastWebContentsImpl::SetEnabledForRemoteDebugging(bool enabled) {
DCHECK(remote_debugging_server_);
if (enabled && !params_->enabled_for_dev) {
LOG(INFO) << "Enabling dev console for CastWebContentsImpl";
remote_debugging_server_->EnableWebContentsForDebugging(web_contents_);
} else if (!enabled && params_->enabled_for_dev) {
LOG(INFO) << "Disabling dev console for CastWebContentsImpl";
remote_debugging_server_->DisableWebContentsForDebugging(web_contents_);
}
params_->enabled_for_dev = enabled;
// Propagate setting change to inner contents.
for (auto& inner : inner_contents_) {
inner->SetEnabledForRemoteDebugging(enabled);
}
}
void CastWebContentsImpl::GetMainFramePid(GetMainFramePidCallback cb) {
if (!web_contents_ || !web_contents_->GetPrimaryMainFrame()) {
std::move(cb).Run(base::kNullProcessHandle);
return;
}
auto* rph = web_contents_->GetPrimaryMainFrame()->GetProcess();
if (!rph || rph->GetProcess().Handle() == base::kNullProcessHandle) {
std::move(cb).Run(base::kNullProcessHandle);
return;
}
std::move(cb).Run(rph->GetProcess().Handle());
}
bool CastWebContentsImpl::TryBindReceiver(
mojo::GenericPendingReceiver& receiver) {
// First try binding local interfaces.
if (local_interfaces_.TryBindReceiver(receiver)) {
return true;
}
const std::string interface_name = *receiver.interface_name();
mojo::ScopedMessagePipeHandle interface_pipe = receiver.PassPipe();
receiver =
mojo::GenericPendingReceiver(interface_name, std::move(interface_pipe));
remote_interfaces_.Bind(std::move(receiver));
// The PendingReceiver has been passed along at this point, so return true.
// Note that this doesn't guarantee that the interface will eventually be
// connected.
return true;
}
InterfaceBundle* CastWebContentsImpl::local_interfaces() {
return &local_interfaces_;
}
bool CastWebContentsImpl::is_websql_enabled() {
return params_->enable_websql;
}
bool CastWebContentsImpl::is_mixer_audio_enabled() {
return params_->enable_mixer_audio;
}
void CastWebContentsImpl::OnClosePageTimeout() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!closing_ || stopped_) {
return;
}
closing_ = false;
Stop(net::OK);
}
void CastWebContentsImpl::RenderFrameCreated(
content::RenderFrameHost* frame_host) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(frame_host);
// TODO(b/187758538): Merge the two ConfigureFeatures() calls.
mojo::Remote<chromecast::shell::mojom::FeatureManager> feature_manager_remote;
frame_host->GetRemoteInterfaces()->GetInterface(
feature_manager_remote.BindNewPipeAndPassReceiver());
feature_manager_remote->ConfigureFeatures(GetRendererFeatures());
mojo::AssociatedRemote<chromecast::shell::mojom::FeatureManager>
feature_manager_associated_remote;
frame_host->GetRemoteAssociatedInterfaces()->GetInterface(
&feature_manager_associated_remote);
feature_manager_associated_remote->ConfigureFeatures(GetRendererFeatures());
mojo::AssociatedRemote<components::media_control::mojom::MediaPlaybackOptions>
media_playback_options;
frame_host->GetRemoteAssociatedInterfaces()->GetInterface(
&media_playback_options);
media_playback_options->SetRendererType(
ToContentRendererType(params_->renderer_type));
// Send queryable values
mojo::Remote<chromecast::shell::mojom::QueryableDataStore>
queryable_data_store_remote;
frame_host->GetRemoteInterfaces()->GetInterface(
queryable_data_store_remote.BindNewPipeAndPassReceiver());
for (const auto& value : QueryableData::GetValues()) {
// base::Value is not copyable.
queryable_data_store_remote->Set(value.first, value.second.Clone());
}
// Set up URL filter
if (params_->url_filters) {
mojo::AssociatedRemote<chromecast::mojom::ActivityUrlFilterConfiguration>
activity_filter_setter;
frame_host->GetRemoteAssociatedInterfaces()->GetInterface(
&activity_filter_setter);
activity_filter_setter->SetFilter(
chromecast::mojom::ActivityUrlFilterCriteria::New(
params_->url_filters.value()));
}
// Set the background color for main frames.
if (!frame_host->GetParent()) {
if (params_->background_color == mojom::BackgroundColor::WHITE) {
frame_host->GetView()->SetBackgroundColor(SK_ColorWHITE);
} else if (params_->background_color == mojom::BackgroundColor::BLACK) {
frame_host->GetView()->SetBackgroundColor(SK_ColorBLACK);
} else if (params_->background_color ==
mojom::BackgroundColor::TRANSPARENT) {
frame_host->GetView()->SetBackgroundColor(SK_ColorTRANSPARENT);
} else {
frame_host->GetView()->SetBackgroundColor(chromecast::GetSwitchValueColor(
switches::kCastAppBackgroundColor, SK_ColorBLACK));
}
}
}
std::vector<chromecast::shell::mojom::FeaturePtr>
CastWebContentsImpl::GetRendererFeatures() {
std::vector<chromecast::shell::mojom::FeaturePtr> features;
for (const auto pair : renderer_features_) {
const std::string& name = pair.first;
const base::Value& config_value = pair.second;
const base::Value::Dict* maybe_config_dict = config_value.GetIfDict();
// There are only 2 callers of `AddRendererFeatures` (both in
// `runtime_application_service_impl.cc`) and they always provide
// well-formed dictionaries as values.
DCHECK(maybe_config_dict);
base::Value::Dict config_dict = maybe_config_dict->Clone();
features.push_back(
chromecast::shell::mojom::Feature::New(name, std::move(config_dict)));
}
return features;
}
void CastWebContentsImpl::OnBindingsReceived(
std::vector<chromecast::mojom::ApiBindingPtr> bindings) {
bindings_received_ = true;
if (bindings.empty()) {
LOG(ERROR) << "ApiBindings remote sent empty bindings. Stopping the page.";
Stop(net::ERR_UNEXPECTED);
} else {
constexpr uint64_t kBindingsIdStart = 0xFF0000;
// Enumerate and inject all scripts in |bindings|.
uint64_t bindings_id = kBindingsIdStart;
for (auto& entry : bindings) {
AddBeforeLoadJavaScript(bindings_id++, entry->script);
}
}
DVLOG(1) << "Bindings has been received. Start loading URL if requested.";
if (!pending_load_url_.is_empty()) {
auto gurl = std::move(pending_load_url_);
pending_load_url_ = GURL();
LoadUrl(gurl);
}
}
bool CastWebContentsImpl::OnPortConnected(
std::string_view port_name,
std::unique_ptr<cast_api_bindings::MessagePort> port) {
DCHECK(api_bindings_);
api_bindings_->Connect(
std::string(port_name),
cast_api_bindings::BlinkMessagePortAdapter::FromServerPlatformMessagePort(
std::move(port))
.PassPort());
return true;
}
void CastWebContentsImpl::PrimaryMainFrameRenderProcessGone(
base::TerminationStatus status) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
LOG(INFO) << "Render process for main frame exited unexpectedly.";
Stop(net::ERR_UNEXPECTED);
}
void CastWebContentsImpl::DidStartNavigation(
content::NavigationHandle* navigation_handle) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(navigation_handle);
if (!web_contents_ || closing_ || stopped_) {
return;
}
if (!navigation_handle->IsInMainFrame() ||
navigation_handle->IsSameDocument()) {
return;
}
// Main frame has an ongoing navigation. This might overwrite a
// previously active navigation. We only care about tracking
// the most recent main frame navigation.
active_navigation_ = navigation_handle;
// Main frame has begun navigating/loading.
LOG(INFO) << "Navigation started: " << navigation_handle->GetURL();
OnPageLoading();
start_loading_ticks_ = base::TimeTicks::Now();
GURL loading_url;
content::NavigationEntry* nav_entry =
web_contents()->GetController().GetVisibleEntry();
if (nav_entry) {
loading_url = nav_entry->GetVirtualURL();
}
TracePageLoadBegin(loading_url);
UpdatePageState();
DCHECK_EQ(page_state_, PageState::LOADING);
NotifyPageState();
}
void CastWebContentsImpl::DidRedirectNavigation(
content::NavigationHandle* navigation_handle) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(navigation_handle);
if (!web_contents_ || closing_ || stopped_) {
return;
}
if (!navigation_handle->IsInMainFrame()) {
return;
}
// Main frame navigation was redirected by the server.
LOG(INFO) << "Navigation was redirected by server: "
<< navigation_handle->GetURL();
}
void CastWebContentsImpl::ReadyToCommitNavigation(
content::NavigationHandle* navigation_handle) {
DCHECK(navigation_handle);
if (!web_contents_ || closing_ || stopped_) {
return;
}
// We want to honor the autoplay feature policy (via allow="autoplay") without
// explicit user activation, since media on Cast is extremely likely to have
// already been explicitly requested by a user via voice or over the network.
// By spoofing the "high media engagement" signal, we can bypass the user
// gesture requirement for autoplay.
int32_t autoplay_flags = blink::mojom::kAutoplayFlagHighMediaEngagement;
// Main frames should have autoplay enabled by default, since autoplay
// delegation via parent frame doesn't work here.
if (navigation_handle->IsInMainFrame()) {
autoplay_flags |= blink::mojom::kAutoplayFlagForceAllow;
}
mojo::AssociatedRemote<blink::mojom::AutoplayConfigurationClient> client;
navigation_handle->GetRenderFrameHost()
->GetRemoteAssociatedInterfaces()
->GetInterface(&client);
auto autoplay_origin = url::Origin::Create(navigation_handle->GetURL());
client->AddAutoplayFlags(autoplay_origin, autoplay_flags);
// Skip injecting bindings scripts if |navigation_handle| is not
// 'current' main frame navigation, e.g. another DidStartNavigation is
// emitted. Also skip injecting for same document navigation and error page.
if (navigation_handle == active_navigation_ &&
!navigation_handle->IsErrorPage()) {
// Injects registered bindings script into the main frame.
script_injector_.InjectScriptsForURL(
navigation_handle->GetURL(), navigation_handle->GetRenderFrameHost());
}
// Always allow mixed content (https and http in the same page) for cast.
// TODO(https://crbug.com/333795270): Decide whether to use
// kKeyAllowInsecureContent to configure allowing mixed content.
auto content_settings = blink::CreateDefaultRendererContentSettings();
content_settings->allow_mixed_content = true;
navigation_handle->SetContentSettings(std::move(content_settings));
// Notifies observers that the navigation of the main frame is ready.
for (Observer& observer : sync_observers_) {
observer.MainFrameReadyToCommitNavigation(navigation_handle);
}
}
void CastWebContentsImpl::DidFinishNavigation(
content::NavigationHandle* navigation_handle) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Ignore sub-frame and non-current main frame navigation.
if (navigation_handle != active_navigation_) {
return;
}
active_navigation_ = nullptr;
// If the navigation was not committed, it means either the page was a
// download or error 204/205, or the navigation never left the previous
// URL. Ignore these navigations.
if (!navigation_handle->HasCommitted()) {
LOG(WARNING) << "Navigation did not commit: url="
<< navigation_handle->GetURL();
return;
}
if (navigation_handle->IsErrorPage()) {
const net::Error error_code = navigation_handle->GetNetErrorCode();
LOG(ERROR) << "Got error on navigation: url=" << navigation_handle->GetURL()
<< ", error_code=" << error_code
<< ", description=" << net::ErrorToShortString(error_code);
Stop(error_code);
DCHECK_EQ(page_state_, PageState::ERROR);
}
// Notifies observers that the navigation of the main frame has finished
// with no errors.
for (auto& observer : observers_) {
observer->MainFrameFinishedNavigation();
}
}
void CastWebContentsImpl::DidFinishLoad(
content::RenderFrameHost* render_frame_host,
const GURL& validated_url) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (page_state_ != PageState::LOADING || !web_contents_ ||
render_frame_host != web_contents_->GetPrimaryMainFrame()) {
return;
}
// Don't process load completion on the current document if the WebContents
// is already in the process of navigating to a different page.
if (active_navigation_) {
return;
}
// The main frame finished loading. Before proceeding, we need to verify that
// the loaded page is the one that was requested.
TracePageLoadEnd(validated_url);
int http_status_code = 0;
content::NavigationEntry* nav_entry =
web_contents()->GetController().GetVisibleEntry();
if (nav_entry) {
http_status_code = nav_entry->GetHttpStatusCode();
}
if (http_status_code != 0 && http_status_code / 100 != 2) {
// An error HTML page was loaded instead of the content we requested.
LOG(ERROR) << "Failed loading page for: " << validated_url
<< "; http status code: " << http_status_code;
Stop(net::ERR_HTTP_RESPONSE_CODE_FAILURE);
DCHECK_EQ(page_state_, PageState::ERROR);
return;
}
// Main frame finished loading properly.
base::TimeDelta load_time = base::TimeTicks::Now() - start_loading_ticks_;
LOG(INFO) << "Finished loading page after " << load_time.InMilliseconds()
<< " ms, url=" << validated_url;
OnPageLoaded();
}
void CastWebContentsImpl::DidFailLoad(
content::RenderFrameHost* render_frame_host,
const GURL& validated_url,
int error_code) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Only report an error if we are the main frame. See b/8433611.
if (render_frame_host->GetParent()) {
LOG(ERROR) << "Got error on sub-iframe: url=" << validated_url.spec()
<< ", error=" << error_code;
return;
}
if (error_code == net::ERR_ABORTED) {
// ERR_ABORTED means download was aborted by the app, typically this happens
// when flinging URL for direct playback, the initial URLRequest gets
// cancelled/aborted and then the same URL is requested via the buffered
// data source for media::Pipeline playback.
LOG(INFO) << "Load canceled: url=" << validated_url.spec();
// We consider the page to be fully loaded in this case, since the app has
// intentionally entered this state. If the app wanted to stop, it would
// have called window.close() instead.
OnPageLoaded();
return;
}
LOG(ERROR) << "Got error on load: url=" << validated_url.spec()
<< ", error_code=" << error_code;
TracePageLoadEnd(validated_url);
Stop(error_code);
DCHECK_EQ(PageState::ERROR, page_state_);
}
void CastWebContentsImpl::OnPageLoading() {
closing_ = false;
stopped_ = false;
stop_notified_ = false;
main_frame_loaded_ = false;
last_error_ = net::OK;
}
void CastWebContentsImpl::OnPageLoaded() {
main_frame_loaded_ = true;
UpdatePageState();
DCHECK(page_state_ == PageState::LOADED);
NotifyPageState();
}
void CastWebContentsImpl::UpdatePageState() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
last_state_ = page_state_;
if (!web_contents_) {
DCHECK(stopped_);
page_state_ = PageState::DESTROYED;
} else if (!stopped_) {
if (main_frame_loaded_) {
page_state_ = PageState::LOADED;
} else {
page_state_ = PageState::LOADING;
}
} else if (stopped_) {
if (last_error_ != net::OK) {
page_state_ = PageState::ERROR;
} else {
page_state_ = PageState::CLOSED;
}
}
}
void CastWebContentsImpl::NotifyPageState() {
// Don't notify if the page state didn't change.
if (last_state_ == page_state_) {
return;
}
// Don't recursively notify the observers.
if (notifying_) {
return;
}
notifying_ = true;
if (stopped_ && !stop_notified_) {
stop_notified_ = true;
for (auto& observer : observers_) {
observer->PageStopped(page_state_, last_error_);
}
// Notifies the local observers.
for (Observer& observer : sync_observers_) {
observer.PageStopped(page_state_, last_error_);
}
} else {
for (auto& observer : observers_) {
observer->PageStateChanged(page_state_);
}
// Notifies the local observers.
for (Observer& observer : sync_observers_) {
observer.PageStateChanged(page_state_);
}
}
notifying_ = false;
}
void CastWebContentsImpl::ResourceLoadComplete(
content::RenderFrameHost* render_frame_host,
const content::GlobalRequestID& request_id,
const blink::mojom::ResourceLoadInfo& resource_load_info) {
if (!web_contents_ ||
render_frame_host != web_contents_->GetPrimaryMainFrame()) {
return;
}
int net_error = resource_load_info.net_error;
if (net_error == net::OK) {
return;
}
metrics::CastMetricsHelper* metrics_helper =
metrics::CastMetricsHelper::GetInstance();
metrics_helper->RecordApplicationEventWithValue(
"Cast.Platform.ResourceRequestError", net_error);
LOG(ERROR) << "Resource \"" << resource_load_info.original_url << "\""
<< " failed to load with net_error=" << net_error
<< ", description=" << net::ErrorToShortString(net_error);
shell::CastBrowserProcess::GetInstance()->connectivity_checker()->Check();
for (auto& observer : observers_) {
observer->ResourceLoadFailed();
}
}
void CastWebContentsImpl::InnerWebContentsCreated(
content::WebContents* inner_web_contents) {
if (!params_->handle_inner_contents) {
return;
}
mojom::CastWebViewParamsPtr params = mojom::CastWebViewParams::New();
params->enabled_for_dev = params_->enabled_for_dev;
params->background_color = params_->background_color;
auto result = inner_contents_.insert(std::unique_ptr<CastWebContentsImpl>(
new CastWebContentsImpl(inner_web_contents, std::move(params), this)));
// Notifies remote observers.
for (auto& observer : observers_) {
mojo::PendingRemote<mojom::CastWebContents> pending_remote;
result.first->get()->BindSharedReceiver(
pending_remote.InitWithNewPipeAndPassReceiver());
observer->InnerContentsCreated(std::move(pending_remote));
}
// Notifies the local observers.
for (Observer& observer : sync_observers_) {
observer.InnerContentsCreated(result.first->get(), this);
}
}
void CastWebContentsImpl::TitleWasSet(content::NavigationEntry* entry) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!entry) {
return;
}
for (auto& observer : observers_) {
observer->UpdateTitle(base::UTF16ToUTF8(entry->GetTitle()));
}
}
void CastWebContentsImpl::DidFirstVisuallyNonEmptyPaint() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
metrics::CastMetricsHelper::GetInstance()->LogTimeToFirstPaint();
for (auto& observer : observers_) {
observer->DidFirstVisuallyNonEmptyPaint();
}
}
void CastWebContentsImpl::WebContentsDestroyed() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
closing_ = false;
DisableDebugging();
media_blocker_.reset();
content::WebContentsObserver::Observe(nullptr);
web_contents_ = nullptr;
Stop(net::OK);
RemoveCastWebContents(this);
DCHECK_EQ(PageState::DESTROYED, page_state_);
}
void CastWebContentsImpl::DidUpdateFaviconURL(
content::RenderFrameHost* render_frame_host,
const std::vector<blink::mojom::FaviconURLPtr>& candidates) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (candidates.empty()) {
return;
}
GURL icon_url;
bool found_touch_icon = false;
// icon search order:
// 1) apple-touch-icon-precomposed
// 2) apple-touch-icon
// 3) icon
for (auto& favicon : candidates) {
if (favicon->icon_type ==
blink::mojom::FaviconIconType::kTouchPrecomposedIcon) {
icon_url = favicon->icon_url;
break;
} else if ((favicon->icon_type ==
blink::mojom::FaviconIconType::kTouchIcon) &&
!found_touch_icon) {
found_touch_icon = true;
icon_url = favicon->icon_url;
} else if (!found_touch_icon) {
icon_url = favicon->icon_url;
}
}
for (auto& observer : observers_) {
observer->UpdateFaviconURL(icon_url);
}
}
void CastWebContentsImpl::MediaStartedPlaying(
const MediaPlayerInfo& video_type,
const content::MediaPlayerId& id) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
metrics::CastMetricsHelper::GetInstance()->LogMediaPlay();
for (auto& observer : observers_) {
observer->MediaPlaybackChanged(true /* media_playing */);
}
// Notifies the local observers.
for (Observer& observer : sync_observers_) {
observer.MediaPlaybackChanged(true /* media_playing */);
}
}
void CastWebContentsImpl::MediaStoppedPlaying(
const MediaPlayerInfo& video_type,
const content::MediaPlayerId& id,
content::WebContentsObserver::MediaStoppedReason reason) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
metrics::CastMetricsHelper::GetInstance()->LogMediaPause();
for (auto& observer : observers_) {
observer->MediaPlaybackChanged(false /* media_playing */);
}
// Notifies the local observers.
for (Observer& observer : sync_observers_) {
observer.MediaPlaybackChanged(false /* media_playing */);
}
}
void CastWebContentsImpl::TracePageLoadBegin(const GURL& url) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
TRACE_EVENT_NESTABLE_ASYNC_BEGIN1(
"browser,navigation", "CastWebContentsImpl Launch", TRACE_ID_LOCAL(this),
"URL", url.possibly_invalid_spec());
}
void CastWebContentsImpl::TracePageLoadEnd(const GURL& url) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
TRACE_EVENT_NESTABLE_ASYNC_END1(
"browser,navigation", "CastWebContentsImpl Launch", TRACE_ID_LOCAL(this),
"URL", url.possibly_invalid_spec());
}
void CastWebContentsImpl::DisableDebugging() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!params_->enabled_for_dev || !web_contents_) {
return;
}
LOG(INFO) << "Disabling dev console for CastWebContentsImpl";
remote_debugging_server_->DisableWebContentsForDebugging(web_contents_);
}
} // namespace chromecast