Chromium Code Reviews| Index: chrome/browser/android/offline_pages/offline_page_request_handler.cc |
| diff --git a/chrome/browser/android/offline_pages/offline_page_request_handler.cc b/chrome/browser/android/offline_pages/offline_page_request_handler.cc |
| index 0e9056c5c7865f3bcc91d783838862c1968bf5ed..0227ab83839776987dc95062ceb1ee1e00d07d21 100644 |
| --- a/chrome/browser/android/offline_pages/offline_page_request_handler.cc |
| +++ b/chrome/browser/android/offline_pages/offline_page_request_handler.cc |
| @@ -4,15 +4,46 @@ |
| #include "chrome/browser/android/offline_pages/offline_page_request_handler.h" |
| +#include "base/metrics/histogram_macros.h" |
| +#include "base/strings/string_tokenizer.h" |
| +#include "base/strings/string_util.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_feature.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/web_contents.h" |
| +#include "net/base/filename_util.h" |
| +#include "net/base/network_change_notifier.h" |
| +#include "net/nqe/network_quality_estimator.h" |
| #include "net/url_request/url_request.h" |
| +#include "net/url_request/url_request_context.h" |
| #include "net/url_request/url_request_interceptor.h" |
| +#include "net/url_request/url_request_job.h" |
| namespace offline_pages { |
| +const char kLoadingOfflinePageHeader[] = "X-chromium-offline"; |
| +const char kLoadingOfflinePageReason[] = "reason="; |
| +const char kLoadingOfflinePageDueToNetError[] = "error"; |
| + |
| namespace { |
| +// 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 |
| +}; |
| + |
| int kUserDataKey; // Only address is used. |
| // An interceptor to hijack requests and potentially service them based on |
| @@ -43,6 +74,186 @@ class OfflinePageRequestInterceptor : public net::URLRequestInterceptor { |
| DISALLOW_COPY_AND_ASSIGN(OfflinePageRequestInterceptor); |
| }; |
| +NetworkState GetNetworkState(net::URLRequest* request) { |
| + DCHECK_CURRENTLY_ON(content::BrowserThread::IO); |
| + |
| + // If extra header kLoadingOfflinePageHeader is present, offline page should |
| + // be loaded if available. |
| + std::string value; |
| + if (request->extra_request_headers().GetHeader(kLoadingOfflinePageHeader, |
| + &value)) { |
| + std::string reason; |
| + 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 base::EqualsCaseInsensitiveASCII( |
| + reason, kLoadingOfflinePageDueToNetError) ? |
| + NetworkState::FLAKY_NETWORK : NetworkState::SKIPPED_NETWORK_CHECK; |
|
fgorski
2016/08/15 21:38:32
would it make sense to continue network checks if
jianli
2016/08/15 23:15:43
Done.
|
| + } |
| + |
| + if (net::NetworkChangeNotifier::IsOffline()) |
| + return NetworkState::DISCONNECTED_NETWORK; |
| + |
| + // NetworkQualityEstimator only works when it is enabled. |
| + if (!previews::IsOfflinePreviewsEnabled()) |
| + return NetworkState::CONNECTED_NETWORK; |
| + |
| + net::NetworkQualityEstimator* network_quality_estimator = |
| + request->context() ? request->context()->network_quality_estimator() |
| + : nullptr; |
| + if (!network_quality_estimator) |
| + return NetworkState::CONNECTED_NETWORK; |
| + |
| + net::EffectiveConnectionType effective_connection_type = |
| + network_quality_estimator->GetEffectiveConnectionType(); |
| + return effective_connection_type >= net::EFFECTIVE_CONNECTION_TYPE_OFFLINE && |
|
fgorski
2016/08/15 21:38:32
Is the effective connection type enum guaranteed t
jianli
2016/08/15 23:15:43
I think so.
|
| + effective_connection_type <= net::EFFECTIVE_CONNECTION_TYPE_SLOW_2G ? |
| + NetworkState::PROHIBITIVELY_SLOW_NETWORK : |
| + NetworkState::CONNECTED_NETWORK; |
| +} |
| + |
| +void ReportRequestResult(RequestResult request_result, |
|
fgorski
2016/08/15 21:38:32
Consider splitting it in 2 methods, please. (But u
jianli
2016/08/15 23:15:43
Done.
|
| + NetworkState network_state) { |
| + AggregatedRequestResult result; |
| + if (request_result == RequestResult::NO_TAB_ID) { |
| + result = AggregatedRequestResult::NO_TAB_ID; |
| + } else if (request_result == RequestResult::NO_WEB_CONTENTS) { |
| + result = AggregatedRequestResult::NO_WEB_CONTENTS; |
| + } else if (request_result == RequestResult::PAGE_NOT_FRESH) { |
| + DCHECK_EQ(NetworkState::PROHIBITIVELY_SLOW_NETWORK, network_state); |
| + result = |
| + AggregatedRequestResult::PAGE_NOT_FRESH_ON_PROHIBITIVELY_SLOW_NETWORK; |
| + } else if (request_result == RequestResult::OFFLINE_PAGE_NOT_FOUND) { |
| + switch (network_state) { |
| + case NetworkState::DISCONNECTED_NETWORK: |
| + result = |
| + AggregatedRequestResult::PAGE_NOT_FOUND_ON_DISCONNECTED_NETWORK; |
| + break; |
| + case NetworkState::PROHIBITIVELY_SLOW_NETWORK: |
| + result = |
| + AggregatedRequestResult:: |
| + PAGE_NOT_FOUND_ON_PROHIBITIVELY_SLOW_NETWORK; |
| + break; |
| + case NetworkState::FLAKY_NETWORK: |
| + result = |
| + AggregatedRequestResult::PAGE_NOT_FOUND_ON_DISCONNECTED_NETWORK; |
|
fgorski
2016/08/15 21:38:32
ON_FLAKY_NETWORK
jianli
2016/08/15 23:15:43
Done.
|
| + break; |
| + case NetworkState::SKIPPED_NETWORK_CHECK: |
| + result = net::NetworkChangeNotifier::IsOffline() ? |
| + AggregatedRequestResult::PAGE_NOT_FOUND_ON_DISCONNECTED_NETWORK : |
| + AggregatedRequestResult::PAGE_NOT_FOUND_ON_CONNECTED_NETWORK; |
| + default: |
| + NOTREACHED(); |
| + } |
| + } else { |
| + DCHECK_EQ(RequestResult::OFFLINE_PAGE_SERVED, request_result); |
| + switch (network_state) { |
| + case NetworkState::DISCONNECTED_NETWORK: |
| + result = AggregatedRequestResult::SHOW_OFFLINE_ON_DISCONNECTED_NETWORK; |
| + break; |
| + case NetworkState::PROHIBITIVELY_SLOW_NETWORK: |
| + result = |
| + AggregatedRequestResult::SHOW_OFFLINE_ON_PROHIBITIVELY_SLOW_NETWORK; |
| + break; |
| + case NetworkState::FLAKY_NETWORK: |
| + result = AggregatedRequestResult::SHOW_OFFLINE_ON_FLAKY_NETWORK; |
| + break; |
| + case NetworkState::CONNECTED_NETWORK: |
| + result = AggregatedRequestResult::SHOW_OFFLINE_ON_CONNECTED_NETWORK; |
| + break; |
| + case NetworkState::SKIPPED_NETWORK_CHECK: |
| + result = net::NetworkChangeNotifier::IsOffline() ? |
| + AggregatedRequestResult::SHOW_OFFLINE_ON_DISCONNECTED_NETWORK : |
| + AggregatedRequestResult::SHOW_OFFLINE_ON_CONNECTED_NETWORK; |
| + default: |
| + NOTREACHED(); |
| + } |
| + } |
| + |
| + OfflinePageRequestHandler::ReportAggregatedRequestResult(result); |
| +} |
| + |
| +// Called on IO thread after an offline file path is obtained. |
|
fgorski
2016/08/15 21:38:32
nit: tell the reader what it does, reather than wh
jianli
2016/08/15 23:15:43
Done.
|
| +void DidGetOfflineFilePathOnIO(base::WeakPtr<OfflinePageRequestJob> job, |
| + const base::FilePath& offline_file_path) { |
| + DCHECK_CURRENTLY_ON(content::BrowserThread::IO); |
| + |
| + if (!job) |
| + return; |
| + |
| + if (offline_file_path.empty()) |
| + job->FallbackToDefault(); |
| + else |
| + job->SetOfflineFilePath(offline_file_path); |
| +} |
| + |
| +// Called on UI thread after an offline file path is obtained. |
| +void DidGetOfflineFilePathOnUI(base::WeakPtr<OfflinePageRequestJob> job, |
| + const base::FilePath& offline_file_path) { |
| + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| + |
| + content::BrowserThread::PostTask( |
| + content::BrowserThread::IO, |
| + FROM_HERE, |
| + base::Bind(&DidGetOfflineFilePathOnIO, job, offline_file_path)); |
| +} |
| + |
| +// Called on UI thread after an offline page is found. |
|
fgorski
2016/08/15 21:38:32
ditto
jianli
2016/08/15 23:15:43
Done.
|
| +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); |
| + |
| + RequestResult request_result; |
| + base::FilePath offline_file_path; |
| + |
| + // If offline page is not found, fall through with empty file path such that |
| + // OfflinePageRequestJob will be notified on failure. |
| + if (offline_page) { |
| + // If the page is being loaded on a slow network, only use the offline page |
| + // if it was created within the past day. |
| + if (network_state == NetworkState::PROHIBITIVELY_SLOW_NETWORK && |
| + base::Time::Now() - offline_page->creation_time > |
|
fgorski
2016/08/15 21:38:32
Add TODO: this should be policy driven I think:
1.
jianli
2016/08/15 23:15:43
This is ported from existing logic in OfflinePageT
|
| + base::TimeDelta::FromDays(1)) { |
| + request_result = RequestResult::PAGE_NOT_FRESH; |
| + } else { |
| + net::FileURLToFilePath(offline_page->GetOfflineURL(), &offline_file_path); |
|
fgorski
2016/08/15 21:38:32
move to if web_contents
jianli
2016/08/15 23:15:43
Done.
|
| + |
| + content::WebContents* web_contents = web_contents_getter.Run(); |
| + if (web_contents) { |
| + request_result = RequestResult::OFFLINE_PAGE_SERVED; |
| + |
| + OfflinePageModel* offline_page_model = |
| + OfflinePageModelFactory::GetForBrowserContext( |
| + web_contents->GetBrowserContext()); |
| + DCHECK(offline_page_model); |
| + offline_page_model->MarkPageAccessed(offline_page->offline_id); |
| + |
| + OfflinePageTabHelper* tab_helper = |
| + OfflinePageTabHelper::FromWebContents(web_contents); |
| + DCHECK(tab_helper); |
| + tab_helper->SetOfflinePage(*offline_page); |
| + } else { |
| + request_result = RequestResult::NO_WEB_CONTENTS; |
| + } |
| + } |
| + } else { |
| + request_result = RequestResult::OFFLINE_PAGE_NOT_FOUND; |
| + } |
| + |
| + ReportRequestResult(request_result, network_state); |
| + DidGetOfflineFilePathOnUI(job, offline_file_path); |
| +} |
| + |
| } // namespace |
| // static |
| @@ -64,9 +275,15 @@ void OfflinePageRequestHandler::InitializeHandler( |
| if (request->method() != "GET") |
| return; |
| + // Bail out if no need to load offline page. |
| + NetworkState network_state = GetNetworkState(request); |
| + if (network_state == NetworkState::CONNECTED_NETWORK) |
| + return; |
| + |
| // Any more precise checks to see if the request should be intercepted are |
| // asynchronous, so just create our handler in all cases. |
| - request->SetUserData(&kUserDataKey, new OfflinePageRequestHandler()); |
| + request->SetUserData(&kUserDataKey, |
| + new OfflinePageRequestHandler(network_state)); |
| } |
| // static |
| @@ -86,20 +303,94 @@ OfflinePageRequestHandler::CreateInterceptor(void* profile_id) { |
| new OfflinePageRequestInterceptor(profile_id)); |
| } |
| -OfflinePageRequestHandler::OfflinePageRequestHandler() { |
| +// static |
| +void OfflinePageRequestHandler::ReportAggregatedRequestResult( |
| + AggregatedRequestResult result) { |
| + UMA_HISTOGRAM_ENUMERATION("OfflinePages.AggregatedRequestResult", |
| + static_cast<int>(result), |
| + static_cast<int>(AggregatedRequestResult::AGGREGATED_REQUEST_RESULT_MAX)); |
| +} |
| + |
| +OfflinePageRequestHandler::OfflinePageRequestHandler(NetworkState network_state) |
| + : network_state_(network_state), |
| + weak_ptr_factory_(this) { |
| + DCHECK_NE(NetworkState::CONNECTED_NETWORK, network_state); |
| } |
| OfflinePageRequestHandler::~OfflinePageRequestHandler() { |
| } |
| +void OfflinePageRequestHandler::OnPrepareToRestart() { |
| + use_default_ = true; |
| + offline_job_.reset(); |
| +} |
| + |
| net::URLRequestJob* OfflinePageRequestHandler::MaybeCreateJob( |
| net::URLRequest* request, |
| net::NetworkDelegate* network_delegate, |
| void* profile_id) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::IO); |
| - // TODO(jianli): to be implemented. |
| - return nullptr; |
| + // Fall back to default which is set when offline page is not found. |
| + if (use_default_) |
| + return nullptr; |
|
fgorski
2016/08/15 21:38:32
is default selected if this returns nullptr?
jianli
2016/08/15 23:15:43
Yes.
|
| + |
| + OfflinePageRequestJob* offline_job = |
| + new OfflinePageRequestJob(request, network_delegate, this); |
| + offline_job_ = offline_job->GetWeakPtr(); |
| + |
| + content::BrowserThread::PostTask( |
| + content::BrowserThread::UI, |
| + FROM_HERE, |
| + base::Bind(&OfflinePageRequestHandler::GetOfflineFileURL, |
| + weak_ptr_factory_.GetWeakPtr(), |
| + request->url(), |
| + profile_id, |
| + content::ResourceRequestInfo::ForRequest(request)-> |
| + GetWebContentsGetterForRequest())); |
| + |
| + return offline_job_.get(); |
| +} |
| + |
| +void OfflinePageRequestHandler::GetOfflineFileURL( |
| + const GURL& online_url, |
| + void* profile_id, |
| + content::ResourceRequestInfo::WebContentsGetter web_contents_getter){ |
| + 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)) { |
| + FailedToGetOfflineFileURL(); |
| + return; |
| + } |
| + Profile* profile = reinterpret_cast<Profile*>(profile_id); |
| + |
| + content::WebContents* web_contents = web_contents_getter.Run(); |
| + int tab_id; |
| + if (!OfflinePageUtils::GetTabId(web_contents, &tab_id)) { |
| + ReportRequestResult(RequestResult::NO_TAB_ID, network_state_); |
| + FailedToGetOfflineFileURL(); |
| + return; |
| + } |
| + |
| + OfflinePageUtils::SelectPageForOnlineURL( |
| + profile, |
| + online_url, |
| + tab_id, |
| + base::Bind(&SelectPageForOnlineURLDone, |
| + network_state_, |
| + offline_job_, |
| + web_contents_getter)); |
| +} |
| + |
| +void OfflinePageRequestHandler::FailedToGetOfflineFileURL() { |
| + 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; |
| + DidGetOfflineFilePathOnUI(offline_job_, empty_file_path); |
| } |
| } // namespace offline_pages |