blob: 05a9a8956ee963ac58e1ac3446ad7bb8d5d35f15 [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 "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 "net/base/net_errors.h"
#include "url/gurl.h"
namespace chromecast {
CastWebContentsImpl::CastWebContentsImpl(
CastWebContentsImpl::Delegate* delegate,
content::WebContents* web_contents,
bool enabled_for_dev)
: delegate_(delegate),
web_contents_(web_contents),
page_state_(PageState::IDLE),
enabled_for_dev_(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(delegate_);
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_);
}
delegate_->OnPageStateChanged(this);
}
CastWebContentsImpl::~CastWebContentsImpl() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DisableDebugging();
}
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::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::OnClosePageTimeout() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!closing_ || stopped_) {
return;
}
closing_ = false;
Stop(net::OK);
}
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::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