blob: c89c8a71c3b94e9334edefb053cfd731661fd4bf [file] [log] [blame]
// Copyright 2018 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.
#include "chromecast/browser/cast_web_contents_impl.h"
#include <utility>
#include "base/bind.h"
#include "base/threading/sequenced_task_runner_handle.h"
#include "chromecast/browser/cast_browser_process.h"
#include "chromecast/browser/devtools/remote_debugging_server.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_view_host.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/common/bindings_policy.h"
#include "net/base/net_errors.h"
#include "services/service_manager/public/cpp/interface_provider.h"
#include "url/gurl.h"
namespace chromecast {
CastWebContentsImpl::CastWebContentsImpl(content::WebContents* web_contents,
const InitParams& init_params)
: web_contents_(web_contents),
delegate_(init_params.delegate),
page_state_(PageState::IDLE),
enabled_for_dev_(init_params.enabled_for_dev),
remote_debugging_server_(
shell::CastBrowserProcess::GetInstance()->remote_debugging_server()),
closing_(false),
stopped_(false),
stop_notified_(false),
notifying_(false),
last_error_(net::OK),
task_runner_(base::SequencedTaskRunnerHandle::Get()),
weak_factory_(this) {
DCHECK(web_contents_);
DCHECK(web_contents_->GetController().IsInitialNavigation());
DCHECK(!web_contents_->IsLoading());
content::WebContentsObserver::Observe(web_contents_);
if (enabled_for_dev_) {
LOG(INFO) << "Enabling dev console for CastWebContentsImpl";
remote_debugging_server_->EnableWebContentsForDebugging(web_contents_);
}
}
CastWebContentsImpl::~CastWebContentsImpl() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DisableDebugging();
for (auto& observer : observer_list_) {
observer.ResetCastWebContents();
}
}
content::WebContents* CastWebContentsImpl::web_contents() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return web_contents_;
}
CastWebContents::PageState CastWebContentsImpl::page_state() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return page_state_;
}
void CastWebContentsImpl::AddRendererFeatures(
std::vector<RendererFeature> features) {
for (auto& feature : features) {
renderer_features_.push_back({feature.name, feature.value.Clone()});
}
}
void CastWebContentsImpl::LoadUrl(const GURL& url) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!web_contents_) {
LOG(ERROR) << "Cannot load URL for deleted WebContents";
return;
}
if (closing_) {
LOG(ERROR) << "Cannot load URL for WebContents while closing";
return;
}
closing_ = false;
stopped_ = false;
stop_notified_ = false;
last_error_ = net::OK;
start_loading_ticks_ = base::TimeTicks::Now();
LOG(INFO) << "Load url: " << url.possibly_invalid_spec();
TracePageLoadBegin(url);
web_contents_->GetController().LoadURL(url, content::Referrer(),
ui::PAGE_TRANSITION_TYPED, "");
UpdatePageState();
DCHECK_EQ(PageState::LOADING, page_state_);
}
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::TimeDelta::FromMilliseconds(1000));
}
void CastWebContentsImpl::Stop(int error_code) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (stopped_) {
UpdatePageState();
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_);
}
void CastWebContentsImpl::SetDelegate(CastWebContents::Delegate* delegate) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
delegate_ = delegate;
}
void CastWebContentsImpl::AllowWebAndMojoWebUiBindings() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
content::RenderViewHost* rvh = web_contents_->GetRenderViewHost();
DCHECK(rvh);
rvh->GetMainFrame()->AllowBindings(content::BINDINGS_POLICY_WEB_UI |
content::BINDINGS_POLICY_MOJO_WEB_UI);
}
// Set background to transparent before making the view visible. This is in
// case Chrome dev tools was opened and caused background color to be reset.
// Note: we also have to set color to black first, because
// RenderWidgetHostViewBase::SetBackgroundColor ignores setting color to
// current color, and it isn't aware that dev tools has changed the color.
void CastWebContentsImpl::ClearRenderWidgetHostView() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
content::RenderWidgetHostView* view =
web_contents_->GetRenderWidgetHostView();
if (view) {
view->SetBackgroundColor(SK_ColorBLACK);
view->SetBackgroundColor(SK_ColorTRANSPARENT);
}
}
void CastWebContentsImpl::AddObserver(CastWebContents::Observer* observer) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(observer);
observer_list_.AddObserver(observer);
}
void CastWebContentsImpl::RemoveObserver(CastWebContents::Observer* observer) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(observer);
observer_list_.RemoveObserver(observer);
}
service_manager::BinderRegistry* CastWebContentsImpl::binder_registry() {
return &binder_registry_;
}
void CastWebContentsImpl::RegisterInterfaceProvider(
const InterfaceSet& interface_set,
service_manager::InterfaceProvider* interface_provider) {
DCHECK(interface_provider);
interface_providers_map_.emplace(interface_set, interface_provider);
}
void CastWebContentsImpl::OnClosePageTimeout() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!closing_ || stopped_) {
return;
}
closing_ = false;
Stop(net::OK);
}
void CastWebContentsImpl::RenderFrameCreated(
content::RenderFrameHost* render_frame_host) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(render_frame_host);
// New render frame has been created, we need to add it to the app
// whitelisting session so URL requests are handled correctly. This must be
// done before URL requests are executed within render frame.
auto* process = render_frame_host->GetProcess();
const int render_process_id = process->GetID();
const int render_frame_id = render_frame_host->GetRoutingID();
for (Observer& observer : observer_list_) {
observer.RenderFrameCreated(render_process_id, render_frame_id);
}
chromecast::shell::mojom::FeatureManagerPtr feature_manager_ptr;
render_frame_host->GetRemoteInterfaces()->GetInterface(&feature_manager_ptr);
feature_manager_ptr->ConfigureFeatures(GetRendererFeatures());
}
std::vector<chromecast::shell::mojom::FeaturePtr>
CastWebContentsImpl::GetRendererFeatures() {
std::vector<chromecast::shell::mojom::FeaturePtr> features;
for (const auto& feature : renderer_features_) {
features.push_back(chromecast::shell::mojom::Feature::New(
feature.name, feature.value.Clone()));
}
return features;
}
void CastWebContentsImpl::OnInterfaceRequestFromFrame(
content::RenderFrameHost* /* render_frame_host */,
const std::string& interface_name,
mojo::ScopedMessagePipeHandle* interface_pipe) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (binder_registry_.TryBindInterface(interface_name, interface_pipe)) {
return;
}
for (auto& entry : interface_providers_map_) {
auto const& interface_set = entry.first;
// Interface is provided by this InterfaceProvider.
if (interface_set.find(interface_name) != interface_set.end()) {
auto* interface_provider = entry.second;
interface_provider->GetInterfaceByName(interface_name,
std::move(*interface_pipe));
break;
}
}
}
void CastWebContentsImpl::RenderProcessGone(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::DidFinishNavigation(
content::NavigationHandle* navigation_handle) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// 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())
return;
net::Error error_code = navigation_handle->GetNetErrorCode();
// If we abort errors in an iframe, it can create a really confusing
// and fragile user experience. Rather than create a list of errors
// that are most likely to occur, we ignore all of them for now.
if (!navigation_handle->IsInMainFrame()) {
LOG(ERROR) << "Got error on sub-iframe: url=" << navigation_handle->GetURL()
<< ", error=" << error_code
<< ", description=" << net::ErrorToShortString(error_code);
return;
}
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);
}
void CastWebContentsImpl::DidStartLoading() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
UpdatePageState();
DCHECK_EQ(page_state_, PageState::LOADING);
}
void CastWebContentsImpl::DidStopLoading() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
int http_status_code = 0;
GURL final_url;
content::NavigationEntry* nav_entry =
web_contents()->GetController().GetVisibleEntry();
if (nav_entry) {
http_status_code = nav_entry->GetHttpStatusCode();
final_url = nav_entry->GetVirtualURL();
}
TracePageLoadEnd(final_url);
if (http_status_code != 0 && http_status_code / 100 != 2) {
// We successfully loaded an error HTML page.
LOG(INFO) << "Failed loading page for: " << final_url
<< "; http status code: " << http_status_code;
Stop(net::ERR_FAILED);
DCHECK_EQ(page_state_, PageState::ERROR);
return;
}
// Main frame finished loading.
base::TimeDelta load_time = base::TimeTicks::Now() - start_loading_ticks_;
LOG(INFO) << "Finished loading page after " << load_time.InMilliseconds()
<< " ms, url=" << final_url;
PageState previous = page_state_;
UpdatePageState();
DCHECK((previous == PageState::ERROR && page_state_ == PageState::ERROR) ||
page_state_ == PageState::LOADED)
<< "Page is in unexpected state: " << page_state_;
}
void CastWebContentsImpl::UpdatePageState() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
PageState last_state = page_state_;
if (!web_contents_) {
DCHECK(stopped_);
page_state_ = PageState::DESTROYED;
} else if (!stopped_) {
if (web_contents_->IsLoading()) {
page_state_ = PageState::LOADING;
} else {
page_state_ = PageState::LOADED;
}
} else if (stopped_) {
if (last_error_ != net::OK) {
page_state_ = PageState::ERROR;
} else {
page_state_ = PageState::CLOSED;
}
}
if (!delegate_)
return;
// Don't notify if the page state didn't change.
if (last_state == page_state_)
return;
// Don't recursively notify the delegate.
if (notifying_)
return;
notifying_ = true;
if (stopped_ && !stop_notified_) {
stop_notified_ = true;
delegate_->OnPageStopped(this, last_error_);
} else {
delegate_->OnPageStateChanged(this);
}
notifying_ = false;
}
void CastWebContentsImpl::DidFailLoad(
content::RenderFrameHost* render_frame_host,
const GURL& validated_url,
int error_code,
const base::string16& error_description) {
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();
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::InnerWebContentsCreated(
content::WebContents* inner_web_contents) {
auto result = inner_contents_.insert(std::make_unique<CastWebContentsImpl>(
inner_web_contents, InitParams{nullptr, enabled_for_dev_}));
if (delegate_)
delegate_->InnerContentsCreated(result.first->get(), this);
}
void CastWebContentsImpl::WebContentsDestroyed() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
closing_ = false;
DisableDebugging();
content::WebContentsObserver::Observe(nullptr);
web_contents_ = nullptr;
Stop(net::OK);
DCHECK_EQ(PageState::DESTROYED, page_state_);
}
void CastWebContentsImpl::TracePageLoadBegin(const GURL& url) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
TRACE_EVENT_ASYNC_BEGIN1("browser,navigation", "CastWebContentsImpl Launch",
this, "URL", url.possibly_invalid_spec());
}
void CastWebContentsImpl::TracePageLoadEnd(const GURL& url) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
TRACE_EVENT_ASYNC_END1("browser,navigation", "CastWebContentsImpl Launch",
this, "URL", url.possibly_invalid_spec());
}
void CastWebContentsImpl::DisableDebugging() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!enabled_for_dev_ || !web_contents_)
return;
LOG(INFO) << "Disabling dev console for " << web_contents_->GetVisibleURL();
remote_debugging_server_->DisableWebContentsForDebugging(web_contents_);
}
std::ostream& operator<<(std::ostream& os,
CastWebContentsImpl::PageState state) {
#define CASE(state) \
case CastWebContentsImpl::PageState::state: \
os << #state; \
return os;
switch (state) {
CASE(IDLE);
CASE(LOADING);
CASE(LOADED);
CASE(CLOSED);
CASE(DESTROYED);
CASE(ERROR);
}
#undef CASE
}
} // namespace chromecast