Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(3942)

Unified Diff: chrome/browser/android/offline_pages/offline_page_request_job.cc

Issue 2245733004: Serve offline page for online URL on disconnected or bad networks (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Fix junit test Created 4 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: chrome/browser/android/offline_pages/offline_page_request_job.cc
diff --git a/chrome/browser/android/offline_pages/offline_page_request_job.cc b/chrome/browser/android/offline_pages/offline_page_request_job.cc
new file mode 100644
index 0000000000000000000000000000000000000000..0b443808d3ce524e5d874ed341e11f221149eba7
--- /dev/null
+++ b/chrome/browser/android/offline_pages/offline_page_request_job.cc
@@ -0,0 +1,508 @@
+// Copyright 2016 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 "chrome/browser/android/offline_pages/offline_page_request_job.h"
+
+#include <string>
+
+#include "base/bind.h"
+#include "base/files/file_path.h"
+#include "base/logging.h"
+#include "base/metrics/histogram_macros.h"
+#include "base/strings/string_tokenizer.h"
+#include "base/strings/string_util.h"
+#include "base/threading/thread_task_runner_handle.h"
+#include "chrome/browser/android/offline_pages/offline_page_model_factory.h"
+#include "chrome/browser/android/offline_pages/offline_page_tab_helper.h"
+#include "chrome/browser/android/offline_pages/offline_page_utils.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/profiles/profile_manager.h"
+#include "components/offline_pages/offline_page_model.h"
+#include "components/previews/previews_experiments.h"
+#include "content/public/browser/browser_thread.h"
+#include "content/public/browser/resource_request_info.h"
+#include "content/public/browser/web_contents.h"
+#include "content/public/common/resource_type.h"
+#include "net/base/network_change_notifier.h"
+#include "net/http/http_request_headers.h"
+#include "net/nqe/network_quality_estimator.h"
+#include "net/url_request/url_request.h"
+#include "net/url_request/url_request_context.h"
+
+namespace offline_pages {
+
+const char kLoadingOfflinePageHeader[] = "X-chromium-offline";
+const char kLoadingOfflinePageReason[] = "reason=";
+const char kLoadingOfflinePageDueToNetError[] = "error";
+
+namespace {
+
+enum class NetworkState {
+ // No network connection.
+ DISCONNECTED_NETWORK,
+ // Prohibitively slow means that the NetworkQualityEstimator reported a
+ // connection slow enough to warrant showing an offline page if available.
+ PROHIBITIVELY_SLOW_NETWORK,
+ // Network error received due to bad network, i.e. connected to a hotspot or
+ // proxy that does not have a working network.
+ FLAKY_NETWORK,
+ // Network is in working condition.
+ CONNECTED_NETWORK,
+ // Force to load the offline page if it is available, though network is in
+ // working condition.
+ FORCE_OFFLINE_ON_CONNECTED_NETWORK
+};
+
+// This enum is used to tell all possible outcomes of handling network requests
+// that might serve offline contents.
+enum class RequestResult {
+ OFFLINE_PAGE_SERVED,
+ NO_TAB_ID,
+ NO_WEB_CONTENTS,
+ PAGE_NOT_FRESH,
+ OFFLINE_PAGE_NOT_FOUND
+};
+
+const char kUserDataKey[] = "offline_page_key";
+
+// Contains the info to handle offline page request.
+class OfflinePageRequestInfo : public base::SupportsUserData::Data {
+ public:
+ OfflinePageRequestInfo() : use_default_(false) {}
+ ~OfflinePageRequestInfo() override {}
+
+ static OfflinePageRequestInfo* GetFromRequest(net::URLRequest* request) {
+ return static_cast<OfflinePageRequestInfo*>(
+ request->GetUserData(&kUserDataKey));
+ }
+
+ bool use_default() const { return use_default_; }
+ void set_use_default(bool use_default) { use_default_ = use_default; }
+
+ private:
+ // True if the next time this request is started, the request should be
+ // serviced from the default handler.
+ bool use_default_;
+
+ DISALLOW_COPY_AND_ASSIGN(OfflinePageRequestInfo);
+};
+
+class DefaultDelegate : public OfflinePageRequestJob::Delegate {
+ public:
+ DefaultDelegate() {}
+
+ content::ResourceRequestInfo::WebContentsGetter
+ GetWebContentsGetter(net::URLRequest* request) const override {
+ const content::ResourceRequestInfo* info =
+ content::ResourceRequestInfo::ForRequest(request);
+ return info ? info->GetWebContentsGetterForRequest()
+ : content::ResourceRequestInfo::WebContentsGetter();
+ }
+
+ OfflinePageRequestJob::Delegate::TabIdGetter GetTabIdGetter() const override {
+ return base::Bind(&DefaultDelegate::GetTabId);
+ }
+
+ private:
+ static bool GetTabId(content::WebContents* web_contents, int* tab_id) {
+ return OfflinePageUtils::GetTabId(web_contents, tab_id);
+ }
+
+ DISALLOW_COPY_AND_ASSIGN(DefaultDelegate);
+};
+
+// Returns true if custom offline header is present.
+// |reason| may be set with the reason to trigger the offline page loading.
+bool ParseOfflineHeader(net::URLRequest* request, std::string* reason) {
+ std::string value;
+ if (!request->extra_request_headers().GetHeader(kLoadingOfflinePageHeader,
+ &value)) {
+ return false;
+ }
+
+ // Currently we only support reason field.
+ base::StringTokenizer tokenizer(value, ", ");
+ while (tokenizer.GetNext()) {
+ if (base::StartsWith(tokenizer.token(),
+ kLoadingOfflinePageReason,
+ base::CompareCase::INSENSITIVE_ASCII)) {
+ *reason = tokenizer.token().substr(
+ arraysize(kLoadingOfflinePageReason) - 1);
+ break;
+ }
+ }
+ return true;
+}
+
+bool IsNetworkProhibitivelySlow(net::URLRequest* request) {
+ // NetworkQualityEstimator only works when it is enabled.
+ if (!previews::IsOfflinePreviewsEnabled())
+ return false;
+
+ if (!request->context())
+ return false;
+
+ net::NetworkQualityEstimator* network_quality_estimator =
+ request->context()->network_quality_estimator();
+ if (!network_quality_estimator)
+ return false;
+
+ net::EffectiveConnectionType effective_connection_type =
+ network_quality_estimator->GetEffectiveConnectionType();
+ return effective_connection_type >= net::EFFECTIVE_CONNECTION_TYPE_OFFLINE &&
+ effective_connection_type <= net::EFFECTIVE_CONNECTION_TYPE_SLOW_2G;
+}
+
+NetworkState GetNetworkState(net::URLRequest* request) {
+ DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
+
+ std::string reason;
+ bool has_offline_header = ParseOfflineHeader(request, &reason);
+ if (has_offline_header &&
+ base::EqualsCaseInsensitiveASCII(reason,
+ kLoadingOfflinePageDueToNetError)) {
+ return NetworkState::FLAKY_NETWORK;
+ }
+
+ if (net::NetworkChangeNotifier::IsOffline())
+ return NetworkState::DISCONNECTED_NETWORK;
+
+ if (IsNetworkProhibitivelySlow(request))
+ return NetworkState::PROHIBITIVELY_SLOW_NETWORK;
+
+ return has_offline_header ? NetworkState::FORCE_OFFLINE_ON_CONNECTED_NETWORK
fgorski 2016/08/24 21:02:37 any header other than reason=error will trip this.
jianli 2016/08/24 22:30:58 Yes, this is what we want. Added comment.
+ : NetworkState::CONNECTED_NETWORK;
+}
+
+OfflinePageRequestJob::AggregatedRequestResult
+RequestResultToAggregatedRequestResult(
+ RequestResult request_result, NetworkState network_state) {
+ if (request_result == RequestResult::NO_TAB_ID)
+ return OfflinePageRequestJob::AggregatedRequestResult::NO_TAB_ID;
+
+ if (request_result == RequestResult::NO_WEB_CONTENTS)
+ return OfflinePageRequestJob::AggregatedRequestResult::NO_WEB_CONTENTS;
+
+ if (request_result == RequestResult::PAGE_NOT_FRESH) {
+ DCHECK_EQ(NetworkState::PROHIBITIVELY_SLOW_NETWORK, network_state);
+ return OfflinePageRequestJob::AggregatedRequestResult::
+ PAGE_NOT_FRESH_ON_PROHIBITIVELY_SLOW_NETWORK;
+ }
+
+ if (request_result == RequestResult::OFFLINE_PAGE_NOT_FOUND) {
+ switch (network_state) {
+ case NetworkState::DISCONNECTED_NETWORK:
+ return OfflinePageRequestJob::AggregatedRequestResult::
+ PAGE_NOT_FOUND_ON_DISCONNECTED_NETWORK;
+ case NetworkState::PROHIBITIVELY_SLOW_NETWORK:
+ return OfflinePageRequestJob::AggregatedRequestResult::
+ PAGE_NOT_FOUND_ON_PROHIBITIVELY_SLOW_NETWORK;
+ case NetworkState::FLAKY_NETWORK:
+ return OfflinePageRequestJob::AggregatedRequestResult::
+ PAGE_NOT_FOUND_ON_FLAKY_NETWORK;
+ case NetworkState::FORCE_OFFLINE_ON_CONNECTED_NETWORK:
+ return OfflinePageRequestJob::AggregatedRequestResult::
+ PAGE_NOT_FOUND_ON_CONNECTED_NETWORK;
+ default:
+ NOTREACHED();
+ }
+ }
+
+ DCHECK_EQ(RequestResult::OFFLINE_PAGE_SERVED, request_result);
+ DCHECK_NE(NetworkState::CONNECTED_NETWORK, network_state);
+ switch (network_state) {
+ case NetworkState::DISCONNECTED_NETWORK:
+ return OfflinePageRequestJob::AggregatedRequestResult::
+ SHOW_OFFLINE_ON_DISCONNECTED_NETWORK;
+ case NetworkState::PROHIBITIVELY_SLOW_NETWORK:
+ return OfflinePageRequestJob::AggregatedRequestResult::
+ SHOW_OFFLINE_ON_PROHIBITIVELY_SLOW_NETWORK;
+ case NetworkState::FLAKY_NETWORK:
+ return OfflinePageRequestJob::AggregatedRequestResult::
+ SHOW_OFFLINE_ON_FLAKY_NETWORK;
+ case NetworkState::FORCE_OFFLINE_ON_CONNECTED_NETWORK:
+ return OfflinePageRequestJob::AggregatedRequestResult::
+ SHOW_OFFLINE_ON_CONNECTED_NETWORK;
+ default:
+ NOTREACHED();
+ }
+
+ return OfflinePageRequestJob::AggregatedRequestResult::
+ AGGREGATED_REQUEST_RESULT_MAX;
+}
+
+void ReportRequestResult(
+ RequestResult request_result, NetworkState network_state) {
+ OfflinePageRequestJob::ReportAggregatedRequestResult(
+ RequestResultToAggregatedRequestResult(request_result, network_state));
+}
+
+void NotifyOfflineFilePathOnIO(base::WeakPtr<OfflinePageRequestJob> job,
+ const base::FilePath& offline_file_path) {
+ DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
+
+ if (!job)
+ return;
+ job->OnOfflineFilePathAvailable(offline_file_path);
+}
+
+// Notifies OfflinePageRequestJob about the offline file path. Note that the
+// file path may be empty if not found or on error.
+void NotifyOfflineFilePathOnUI(base::WeakPtr<OfflinePageRequestJob> job,
+ const base::FilePath& offline_file_path) {
+ DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+
+ // Delegates to IO thread since OfflinePageRequestJob should only be accessed
+ // from IO thread.
+ content::BrowserThread::PostTask(
+ content::BrowserThread::IO,
+ FROM_HERE,
+ base::Bind(&NotifyOfflineFilePathOnIO, job, offline_file_path));
+}
+
+// Finds the offline file path based on the select page result and network
+// state.
+RequestResult GetOfflineFilePath(
fgorski 2016/08/24 21:02:37 I don't think this name reflects what the function
jianli 2016/08/24 22:30:58 Renamed to AccessOfflineFile
+ NetworkState network_state,
+ base::WeakPtr<OfflinePageRequestJob> job,
+ content::ResourceRequestInfo::WebContentsGetter web_contents_getter,
+ const OfflinePageItem* offline_page,
+ base::FilePath* offline_file_path) {
+ DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+
+ if (!offline_page)
+ return RequestResult::OFFLINE_PAGE_NOT_FOUND;
+
+ // |web_contents_getter| is passed from IO thread. We neeed to check if
fgorski 2016/08/24 21:02:37 s/neeed/need/
jianli 2016/08/24 22:30:58 Done.
+ // web contents is still valid.
+ content::WebContents* web_contents = web_contents_getter.Run();
+ if (!web_contents)
+ return RequestResult::NO_WEB_CONTENTS;
+
+ // If the page is being loaded on a slow network, only use the offline page
fgorski 2016/08/24 21:02:37 move this section above the web_contents getting,
jianli 2016/08/24 22:30:58 Moving this above will only affect the UMA reporti
+ // if it was created within the past day.
+ // TODO(romax): Make the constant be policy driven.
+ if (network_state == NetworkState::PROHIBITIVELY_SLOW_NETWORK &&
+ base::Time::Now() - offline_page->creation_time >
+ base::TimeDelta::FromDays(1)) {
+ return RequestResult::PAGE_NOT_FRESH;
+ }
+
+ // Since offline page will be loaded, it should be marked as accessed.
+ OfflinePageModel* offline_page_model =
+ OfflinePageModelFactory::GetForBrowserContext(
+ web_contents->GetBrowserContext());
+ DCHECK(offline_page_model);
+ offline_page_model->MarkPageAccessed(offline_page->offline_id);
fgorski 2016/08/24 21:02:37 did you check this on incognito?
jianli 2016/08/24 22:30:58 OfflinePageRequestInterceptor will not be created
+
+ // Save an cached copy of OfflinePageItem such that Tab code can get
+ // the loaded offline page immediately.
+ OfflinePageTabHelper* tab_helper =
+ OfflinePageTabHelper::FromWebContents(web_contents);
+ DCHECK(tab_helper);
+ tab_helper->SetOfflinePage(
+ *offline_page, network_state == NetworkState::PROHIBITIVELY_SLOW_NETWORK);
+
+ *offline_file_path = offline_page->file_path;
+ return RequestResult::OFFLINE_PAGE_SERVED;
+}
+
+// Handles the result of finding an offline page.
+void SelectPageForOnlineURLDone(
+ NetworkState network_state,
+ base::WeakPtr<OfflinePageRequestJob> job,
+ content::ResourceRequestInfo::WebContentsGetter web_contents_getter,
+ const OfflinePageItem* offline_page) {
+ DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+
+ base::FilePath offline_file_path;
+ RequestResult request_result = GetOfflineFilePath(
+ network_state, job, web_contents_getter, offline_page,
+ &offline_file_path);
+
+ ReportRequestResult(request_result, network_state);
+
+ // NotifyOfflineFilePathOnUI should always be called regardless the failure
+ // result and empty file path such that OfflinePageRequestJob will be notified
+ // on failure.
+ NotifyOfflineFilePathOnUI(job, offline_file_path);
+}
+
+void FailedToSelectOfflinePage(base::WeakPtr<OfflinePageRequestJob> job) {
+ DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+
+ // Proceed with empty file path in order to notify the OfflinePageRequestJob
+ // about the failure.
+ base::FilePath empty_file_path;
+ NotifyOfflineFilePathOnUI(job, empty_file_path);
+}
+
+// Tries to find the offline page to serve for |online_url|.
+void SelectOfflinePage(
+ const GURL& online_url,
+ NetworkState network_state,
+ void* profile_id,
+ content::ResourceRequestInfo::WebContentsGetter web_contents_getter,
+ OfflinePageRequestJob::Delegate::TabIdGetter tab_id_getter,
+ base::WeakPtr<OfflinePageRequestJob> job) {
+ DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+
+ // |profile_id| needs to be checked with ProfileManager::IsValidProfile
+ // before using it.
+ if (!g_browser_process->profile_manager()->IsValidProfile(profile_id)) {
+ FailedToSelectOfflinePage(job);
+ return;
+ }
+ Profile* profile = reinterpret_cast<Profile*>(profile_id);
+
+ content::WebContents* web_contents = web_contents_getter.Run();
+ if (!web_contents){
+ ReportRequestResult(RequestResult::NO_WEB_CONTENTS, network_state);
+ FailedToSelectOfflinePage(job);
+ return;
+ }
+ int tab_id;
+ if (!tab_id_getter.Run(web_contents, &tab_id)) {
+ ReportRequestResult(RequestResult::NO_TAB_ID, network_state);
+ FailedToSelectOfflinePage(job);
+ return;
+ }
+
+ OfflinePageUtils::SelectPageForOnlineURL(
+ profile,
+ online_url,
+ tab_id,
+ base::Bind(&SelectPageForOnlineURLDone,
+ network_state,
+ job,
+ web_contents_getter));
+}
+
+} // namespace
+
+// static
+void OfflinePageRequestJob::ReportAggregatedRequestResult(
+ AggregatedRequestResult result) {
+ UMA_HISTOGRAM_ENUMERATION("OfflinePages.AggregatedRequestResult",
+ static_cast<int>(result),
+ static_cast<int>(AggregatedRequestResult::AGGREGATED_REQUEST_RESULT_MAX));
+}
+
+// static
+OfflinePageRequestJob* OfflinePageRequestJob::Create(
+ void* profile_id,
+ net::URLRequest* request,
+ net::NetworkDelegate* network_delegate) {
+ const content::ResourceRequestInfo* resource_request_info =
+ content::ResourceRequestInfo::ForRequest(request);
+ if (!resource_request_info)
+ return nullptr;
+
+ // Ignore the requests not for the main resource.
+ if (resource_request_info->GetResourceType() !=
+ content::RESOURCE_TYPE_MAIN_FRAME) {
+ return nullptr;
+ }
+
+ // Ignore non-http/https requests.
+ if (!request->url().SchemeIsHTTPOrHTTPS())
+ return nullptr;
+
+ // Ignore requests other than GET.
+ if (request->method() != "GET")
+ return nullptr;
+
+ OfflinePageRequestInfo* info =
+ OfflinePageRequestInfo::GetFromRequest(request);
+ if (info) {
+ // Fall back to default which is set when an offline page cannot be served,
+ // either page not found or online version desired.
+ if (info->use_default())
+ return nullptr;
+ } else {
+ request->SetUserData(&kUserDataKey, new OfflinePageRequestInfo());
+ }
+
+ return new OfflinePageRequestJob(profile_id, request, network_delegate);
+}
+
+OfflinePageRequestJob::OfflinePageRequestJob(
+ void* profile_id,
+ net::URLRequest* request,
+ net::NetworkDelegate* network_delegate)
+ : net::URLRequestFileJob(
+ request,
+ network_delegate,
+ base::FilePath(),
+ content::BrowserThread::GetBlockingPool()->
+ GetTaskRunnerWithShutdownBehavior(
+ base::SequencedWorkerPool::SKIP_ON_SHUTDOWN)),
+ profile_id_(profile_id),
+ delegate_(new DefaultDelegate()),
+ weak_ptr_factory_(this) {
+}
+
+OfflinePageRequestJob::~OfflinePageRequestJob() {
+}
+
+void OfflinePageRequestJob::Start() {
+ base::ThreadTaskRunnerHandle::Get()->PostTask(
+ FROM_HERE, base::Bind(&OfflinePageRequestJob::StartAsync,
+ weak_ptr_factory_.GetWeakPtr()));
+}
+
+void OfflinePageRequestJob::StartAsync() {
+ NetworkState network_state = GetNetworkState(request());
+ if (network_state == NetworkState::CONNECTED_NETWORK) {
+ FallbackToDefault();
+ return;
+ }
+
+ content::BrowserThread::PostTask(
+ content::BrowserThread::UI,
+ FROM_HERE,
+ base::Bind(&SelectOfflinePage,
+ request()->url(),
+ network_state,
+ profile_id_,
+ delegate_->GetWebContentsGetter(request()),
+ delegate_->GetTabIdGetter(),
+ weak_ptr_factory_.GetWeakPtr()));
+}
+
+void OfflinePageRequestJob::Kill() {
+ net::URLRequestJob::Kill();
+ weak_ptr_factory_.InvalidateWeakPtrs();
+}
+
+void OfflinePageRequestJob::FallbackToDefault() {
+ OfflinePageRequestInfo* info =
+ OfflinePageRequestInfo::GetFromRequest(request());
+ DCHECK(info);
+ info->set_use_default(true);
+
+ URLRequestJob::NotifyRestartRequired();
+}
+
+void OfflinePageRequestJob::OnOfflineFilePathAvailable(
+ const base::FilePath& offline_file_path) {
+ // If offline file path is empty, it means that offline page cannot be found
+ // and we want to restart the job to fall back to the default handling.
+ if (offline_file_path.empty()) {
+ FallbackToDefault();
+ return;
+ }
+
+ // Sets the file path and lets URLRequestFileJob start to read from the file.
+ file_path_ = offline_file_path;
+ URLRequestFileJob::Start();
+}
+
+void OfflinePageRequestJob::SetDelegateForTesting(
+ std::unique_ptr<Delegate> delegate) {
+ delegate_ = std::move(delegate);
+}
+
+} // namespace offline_pages

Powered by Google App Engine
This is Rietveld 408576698