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

Side by Side 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: Address more feedback 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 unified diff | Download patch
OLDNEW
(Empty)
1 // Copyright 2016 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "chrome/browser/android/offline_pages/offline_page_request_job.h"
6
7 #include <string>
8
9 #include "base/bind.h"
10 #include "base/files/file_path.h"
11 #include "base/logging.h"
12 #include "base/metrics/histogram_macros.h"
13 #include "base/strings/string_tokenizer.h"
14 #include "base/strings/string_util.h"
15 #include "base/threading/thread_task_runner_handle.h"
16 #include "chrome/browser/android/offline_pages/offline_page_model_factory.h"
17 #include "chrome/browser/android/offline_pages/offline_page_tab_helper.h"
18 #include "chrome/browser/android/offline_pages/offline_page_utils.h"
19 #include "chrome/browser/browser_process.h"
20 #include "chrome/browser/profiles/profile.h"
21 #include "chrome/browser/profiles/profile_manager.h"
22 #include "components/offline_pages/offline_page_model.h"
23 #include "components/previews/previews_experiments.h"
24 #include "content/public/browser/browser_thread.h"
25 #include "content/public/browser/resource_request_info.h"
26 #include "content/public/browser/web_contents.h"
27 #include "content/public/common/resource_type.h"
28 #include "net/base/network_change_notifier.h"
29 #include "net/http/http_request_headers.h"
30 #include "net/nqe/network_quality_estimator.h"
31 #include "net/url_request/url_request.h"
32 #include "net/url_request/url_request_context.h"
33
34 namespace offline_pages {
35
36 const char kLoadingOfflinePageHeader[] = "X-chromium-offline";
37 const char kLoadingOfflinePageReason[] = "reason=";
38 const char kLoadingOfflinePageDueToNetError[] = "error";
39
40 namespace {
41
42 enum class NetworkState {
43 // No network connection.
44 DISCONNECTED_NETWORK,
45 // Prohibitively slow means that the NetworkQualityEstimator reported a
46 // connection slow enough to warrant showing an offline page if available.
47 PROHIBITIVELY_SLOW_NETWORK,
48 // Network error received due to bad network, i.e. connected to a hotspot or
49 // proxy that does not have a working network.
50 FLAKY_NETWORK,
51 // Network is in working condition.
52 CONNECTED_NETWORK,
53 // Force to load the offline page if it is available, though network is in
54 // working condition.
55 FORCE_OFFLINE_ON_CONNECTED_NETWORK
56 };
57
58 // This enum is used to tell all possible outcomes of handling network requests
59 // that might serve offline contents.
60 enum class RequestResult {
61 OFFLINE_PAGE_SERVED,
62 NO_TAB_ID,
63 NO_WEB_CONTENTS,
64 PAGE_NOT_FRESH,
65 OFFLINE_PAGE_NOT_FOUND
66 };
67
68 const char kUserDataKey[] = "offline_page_key";
69
70 // Contains the info to handle offline page request.
71 class OfflinePageRequestInfo : public base::SupportsUserData::Data {
72 public:
73 OfflinePageRequestInfo() : use_default_(false) {}
74 ~OfflinePageRequestInfo() override {}
75
76 static OfflinePageRequestInfo* GetFromRequest(net::URLRequest* request) {
77 return static_cast<OfflinePageRequestInfo*>(
78 request->GetUserData(&kUserDataKey));
79 }
80
81 bool use_default() const { return use_default_; }
82 void set_use_default(bool use_default) { use_default_ = use_default; }
83
84 private:
85 // True if the next time this request is started, the request should be
86 // serviced from the default handler.
87 bool use_default_;
88
89 DISALLOW_COPY_AND_ASSIGN(OfflinePageRequestInfo);
90 };
91
92 class DefaultDelegate : public OfflinePageRequestJob::Delegate {
93 public:
94 DefaultDelegate() {}
95
96 content::ResourceRequestInfo::WebContentsGetter
97 GetWebContentsGetter(net::URLRequest* request) const override {
98 const content::ResourceRequestInfo* info =
99 content::ResourceRequestInfo::ForRequest(request);
100 return info ? info->GetWebContentsGetterForRequest()
101 : content::ResourceRequestInfo::WebContentsGetter();
102 }
103
104 OfflinePageRequestJob::Delegate::TabIdGetter GetTabIdGetter() const override {
105 return base::Bind(&DefaultDelegate::GetTabId);
106 }
107
108 private:
109 static bool GetTabId(content::WebContents* web_contents, int* tab_id) {
110 return OfflinePageUtils::GetTabId(web_contents, tab_id);
111 }
112
113 DISALLOW_COPY_AND_ASSIGN(DefaultDelegate);
114 };
115
116 // Returns true if custom offline header is present.
117 // |reason| may be set with the reason to trigger the offline page loading.
118 bool ParseOfflineHeader(net::URLRequest* request, std::string* reason) {
119 std::string value;
120 if (!request->extra_request_headers().GetHeader(kLoadingOfflinePageHeader,
121 &value)) {
122 return false;
123 }
124
125 // Currently we only support reason field.
126 base::StringTokenizer tokenizer(value, ", ");
127 while (tokenizer.GetNext()) {
128 if (base::StartsWith(tokenizer.token(),
129 kLoadingOfflinePageReason,
130 base::CompareCase::INSENSITIVE_ASCII)) {
131 *reason = tokenizer.token().substr(
132 arraysize(kLoadingOfflinePageReason) - 1);
133 break;
134 }
135 }
136 return true;
137 }
138
139 bool IsNetworkProhibitivelySlow(net::URLRequest* request) {
140 // NetworkQualityEstimator only works when it is enabled.
141 if (!previews::IsOfflinePreviewsEnabled())
142 return false;
143
144 if (!request->context())
145 return false;
146
147 net::NetworkQualityEstimator* network_quality_estimator =
148 request->context()->network_quality_estimator();
149 if (!network_quality_estimator)
150 return false;
151
152 net::EffectiveConnectionType effective_connection_type =
153 network_quality_estimator->GetEffectiveConnectionType();
154 return effective_connection_type >= net::EFFECTIVE_CONNECTION_TYPE_OFFLINE &&
155 effective_connection_type <= net::EFFECTIVE_CONNECTION_TYPE_SLOW_2G;
156 }
157
158 NetworkState GetNetworkState(net::URLRequest* request) {
159 DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
160
161 std::string reason;
162 bool has_offline_header = ParseOfflineHeader(request, &reason);
163 if (has_offline_header &&
164 base::EqualsCaseInsensitiveASCII(reason,
165 kLoadingOfflinePageDueToNetError)) {
166 return NetworkState::FLAKY_NETWORK;
167 }
168
169 if (net::NetworkChangeNotifier::IsOffline())
170 return NetworkState::DISCONNECTED_NETWORK;
171
172 if (IsNetworkProhibitivelySlow(request))
173 return NetworkState::PROHIBITIVELY_SLOW_NETWORK;
174
175 return has_offline_header ? NetworkState::FORCE_OFFLINE_ON_CONNECTED_NETWORK
176 : NetworkState::CONNECTED_NETWORK;
177 }
178
179 OfflinePageRequestJob::AggregatedRequestResult
180 RequestResultToAggregatedRequestResult(
181 RequestResult request_result, NetworkState network_state) {
182 if (request_result == RequestResult::NO_TAB_ID)
183 return OfflinePageRequestJob::AggregatedRequestResult::NO_TAB_ID;
184
185 if (request_result == RequestResult::NO_WEB_CONTENTS)
186 return OfflinePageRequestJob::AggregatedRequestResult::NO_WEB_CONTENTS;
187
188 if (request_result == RequestResult::PAGE_NOT_FRESH) {
189 DCHECK_EQ(NetworkState::PROHIBITIVELY_SLOW_NETWORK, network_state);
190 return OfflinePageRequestJob::AggregatedRequestResult::
191 PAGE_NOT_FRESH_ON_PROHIBITIVELY_SLOW_NETWORK;
192 }
193
194 if (request_result == RequestResult::OFFLINE_PAGE_NOT_FOUND) {
195 switch (network_state) {
196 case NetworkState::DISCONNECTED_NETWORK:
197 return OfflinePageRequestJob::AggregatedRequestResult::
198 PAGE_NOT_FOUND_ON_DISCONNECTED_NETWORK;
199 case NetworkState::PROHIBITIVELY_SLOW_NETWORK:
200 return OfflinePageRequestJob::AggregatedRequestResult::
201 PAGE_NOT_FOUND_ON_PROHIBITIVELY_SLOW_NETWORK;
202 case NetworkState::FLAKY_NETWORK:
203 return OfflinePageRequestJob::AggregatedRequestResult::
204 PAGE_NOT_FOUND_ON_FLAKY_NETWORK;
205 case NetworkState::FORCE_OFFLINE_ON_CONNECTED_NETWORK:
206 return OfflinePageRequestJob::AggregatedRequestResult::
207 PAGE_NOT_FOUND_ON_CONNECTED_NETWORK;
208 default:
209 NOTREACHED();
210 }
211 }
212
213 DCHECK_EQ(RequestResult::OFFLINE_PAGE_SERVED, request_result);
214 DCHECK_NE(NetworkState::CONNECTED_NETWORK, network_state);
215 switch (network_state) {
216 case NetworkState::DISCONNECTED_NETWORK:
217 return OfflinePageRequestJob::AggregatedRequestResult::
218 SHOW_OFFLINE_ON_DISCONNECTED_NETWORK;
219 case NetworkState::PROHIBITIVELY_SLOW_NETWORK:
220 return OfflinePageRequestJob::AggregatedRequestResult::
221 SHOW_OFFLINE_ON_PROHIBITIVELY_SLOW_NETWORK;
222 case NetworkState::FLAKY_NETWORK:
223 return OfflinePageRequestJob::AggregatedRequestResult::
224 SHOW_OFFLINE_ON_FLAKY_NETWORK;
225 case NetworkState::FORCE_OFFLINE_ON_CONNECTED_NETWORK:
226 return OfflinePageRequestJob::AggregatedRequestResult::
227 SHOW_OFFLINE_ON_CONNECTED_NETWORK;
228 default:
229 NOTREACHED();
230 }
231
232 return OfflinePageRequestJob::AggregatedRequestResult::
233 AGGREGATED_REQUEST_RESULT_MAX;
234 }
235
236 void ReportRequestResult(
237 RequestResult request_result, NetworkState network_state) {
238 OfflinePageRequestJob::ReportAggregatedRequestResult(
239 RequestResultToAggregatedRequestResult(request_result, network_state));
240 }
241
242 void NotifyOfflineFilePathOnIO(base::WeakPtr<OfflinePageRequestJob> job,
243 const base::FilePath& offline_file_path) {
244 DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
245
246 if (!job)
247 return;
248 job->OnOfflineFilePathAvailable(offline_file_path);
249 }
250
251 // Notifies OfflinePageRequestJob about the offline file path. Note that the
252 // file path may be empty if not found or on error.
253 void NotifyOfflineFilePathOnUI(base::WeakPtr<OfflinePageRequestJob> job,
254 const base::FilePath& offline_file_path) {
255 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
256
257 // Delegates to IO thread since OfflinePageRequestJob should only be accessed
258 // from IO thread.
259 content::BrowserThread::PostTask(
260 content::BrowserThread::IO,
261 FROM_HERE,
262 base::Bind(&NotifyOfflineFilePathOnIO, job, offline_file_path));
263 }
264
265 // Finds the offline file path based on the select page result and network
266 // state.
267 RequestResult GetOfflineFilePath(
268 NetworkState network_state,
269 base::WeakPtr<OfflinePageRequestJob> job,
270 content::ResourceRequestInfo::WebContentsGetter web_contents_getter,
271 const OfflinePageItem* offline_page,
272 base::FilePath* offline_file_path) {
273 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
274
275 if (!offline_page)
276 return RequestResult::OFFLINE_PAGE_NOT_FOUND;
277
278 // |web_contents_getter| is passed from IO thread. We neeed to check if
279 // web contents is still valid.
280 content::WebContents* web_contents = web_contents_getter.Run();
281 if (!web_contents)
282 return RequestResult::NO_WEB_CONTENTS;
283
284 // If the page is being loaded on a slow network, only use the offline page
285 // if it was created within the past day.
286 // TODO(romax): Make the constant be policy driven.
287 if (network_state == NetworkState::PROHIBITIVELY_SLOW_NETWORK &&
288 base::Time::Now() - offline_page->creation_time >
289 base::TimeDelta::FromDays(1)) {
290 return RequestResult::PAGE_NOT_FRESH;
291 }
292
293 // Since offline page will be loaded, it should be marked as accessed.
294 OfflinePageModel* offline_page_model =
295 OfflinePageModelFactory::GetForBrowserContext(
296 web_contents->GetBrowserContext());
297 DCHECK(offline_page_model);
298 offline_page_model->MarkPageAccessed(offline_page->offline_id);
299
300 // Save an cached copy of OfflinePageItem such that Tab code can get
301 // the loaded offline page immediately.
302 OfflinePageTabHelper* tab_helper =
303 OfflinePageTabHelper::FromWebContents(web_contents);
304 DCHECK(tab_helper);
305 tab_helper->SetOfflinePage(*offline_page);
306
307 *offline_file_path = offline_page->file_path;
308 return RequestResult::OFFLINE_PAGE_SERVED;
309 }
310
311 // Handles the result of finding an offline page.
312 void SelectPageForOnlineURLDone(
313 NetworkState network_state,
314 base::WeakPtr<OfflinePageRequestJob> job,
315 content::ResourceRequestInfo::WebContentsGetter web_contents_getter,
316 const OfflinePageItem* offline_page) {
317 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
318
319 base::FilePath offline_file_path;
320 RequestResult request_result = GetOfflineFilePath(
321 network_state, job, web_contents_getter, offline_page,
322 &offline_file_path);
323
324 ReportRequestResult(request_result, network_state);
325
326 // NotifyOfflineFilePathOnUI should always be called regardless the failure
327 // result and empty file path such that OfflinePageRequestJob will be notified
328 // on failure.
329 NotifyOfflineFilePathOnUI(job, offline_file_path);
330 }
331
332 void FailedToSelectOfflinePage(base::WeakPtr<OfflinePageRequestJob> job) {
333 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
334
335 // Proceed with empty file path in order to notify the OfflinePageRequestJob
336 // about the failure.
337 base::FilePath empty_file_path;
338 NotifyOfflineFilePathOnUI(job, empty_file_path);
339 }
340
341 // Tries to find the offline page to serve for |online_url|.
342 void SelectOfflinePage(
343 const GURL& online_url,
344 NetworkState network_state,
345 void* profile_id,
346 content::ResourceRequestInfo::WebContentsGetter web_contents_getter,
347 OfflinePageRequestJob::Delegate::TabIdGetter tab_id_getter,
348 base::WeakPtr<OfflinePageRequestJob> job) {
349 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
350
351 // |profile_id| needs to be checked with ProfileManager::IsValidProfile
352 // before using it.
353 if (!g_browser_process->profile_manager()->IsValidProfile(profile_id)) {
354 FailedToSelectOfflinePage(job);
355 return;
356 }
357 Profile* profile = reinterpret_cast<Profile*>(profile_id);
358
359 content::WebContents* web_contents = web_contents_getter.Run();
360 if (!web_contents){
361 ReportRequestResult(RequestResult::NO_WEB_CONTENTS, network_state);
362 FailedToSelectOfflinePage(job);
363 return;
364 }
365 int tab_id;
366 if (!tab_id_getter.Run(web_contents, &tab_id)) {
367 ReportRequestResult(RequestResult::NO_TAB_ID, network_state);
368 FailedToSelectOfflinePage(job);
369 return;
370 }
371
372 OfflinePageUtils::SelectPageForOnlineURL(
373 profile,
374 online_url,
375 tab_id,
376 base::Bind(&SelectPageForOnlineURLDone,
377 network_state,
378 job,
379 web_contents_getter));
380 }
381
382 } // namespace
383
384 // static
385 void OfflinePageRequestJob::ReportAggregatedRequestResult(
386 AggregatedRequestResult result) {
387 UMA_HISTOGRAM_ENUMERATION("OfflinePages.AggregatedRequestResult",
388 static_cast<int>(result),
389 static_cast<int>(AggregatedRequestResult::AGGREGATED_REQUEST_RESULT_MAX));
390 }
391
392 // static
393 OfflinePageRequestJob* OfflinePageRequestJob::Create(
394 void* profile_id,
395 net::URLRequest* request,
396 net::NetworkDelegate* network_delegate) {
397 const content::ResourceRequestInfo* resource_request_info =
398 content::ResourceRequestInfo::ForRequest(request);
399 if (!resource_request_info)
400 return nullptr;
401
402 // Ignore the requests not for the main resource.
403 if (resource_request_info->GetResourceType() !=
404 content::RESOURCE_TYPE_MAIN_FRAME) {
405 return nullptr;
406 }
407
408 // Ignore non-http/https requests.
409 if (!request->url().SchemeIsHTTPOrHTTPS())
410 return nullptr;
411
412 // Ignore requests other than GET.
413 if (request->method() != "GET")
414 return nullptr;
415
416 OfflinePageRequestInfo* info =
417 OfflinePageRequestInfo::GetFromRequest(request);
418 if (info) {
419 // Fall back to default which is set when an offline page cannot be served,
420 // either page not found or online version desired.
421 if (info->use_default())
422 return nullptr;
423 } else {
424 request->SetUserData(&kUserDataKey, new OfflinePageRequestInfo());
425 }
426
427 return new OfflinePageRequestJob(profile_id, request, network_delegate);
428 }
429
430 OfflinePageRequestJob::OfflinePageRequestJob(
431 void* profile_id,
432 net::URLRequest* request,
433 net::NetworkDelegate* network_delegate)
434 : net::URLRequestFileJob(
435 request,
436 network_delegate,
437 base::FilePath(),
438 content::BrowserThread::GetBlockingPool()->
439 GetTaskRunnerWithShutdownBehavior(
440 base::SequencedWorkerPool::SKIP_ON_SHUTDOWN)),
441 profile_id_(profile_id),
442 delegate_(new DefaultDelegate()),
443 weak_ptr_factory_(this) {
444 }
445
446 OfflinePageRequestJob::~OfflinePageRequestJob() {
447 }
448
449 void OfflinePageRequestJob::Start() {
450 base::ThreadTaskRunnerHandle::Get()->PostTask(
451 FROM_HERE, base::Bind(&OfflinePageRequestJob::StartAsync,
452 weak_ptr_factory_.GetWeakPtr()));
453 }
454
455 void OfflinePageRequestJob::StartAsync() {
456 NetworkState network_state = GetNetworkState(request());
457 if (network_state == NetworkState::CONNECTED_NETWORK) {
458 FallbackToDefault();
459 return;
460 }
461
462 content::BrowserThread::PostTask(
463 content::BrowserThread::UI,
464 FROM_HERE,
465 base::Bind(&SelectOfflinePage,
466 request()->url(),
467 network_state,
468 profile_id_,
469 delegate_->GetWebContentsGetter(request()),
470 delegate_->GetTabIdGetter(),
471 weak_ptr_factory_.GetWeakPtr()));
472 }
473
474 void OfflinePageRequestJob::Kill() {
475 net::URLRequestJob::Kill();
476 weak_ptr_factory_.InvalidateWeakPtrs();
477 }
478
479 void OfflinePageRequestJob::FallbackToDefault() {
480 OfflinePageRequestInfo* info =
481 OfflinePageRequestInfo::GetFromRequest(request());
482 DCHECK(info);
483 info->set_use_default(true);
484
485 URLRequestJob::NotifyRestartRequired();
486 }
487
488 void OfflinePageRequestJob::OnOfflineFilePathAvailable(
489 const base::FilePath& offline_file_path) {
490 // If offline file path is empty, it means that offline page cannot be found
491 // and we want to restart the job to fall back to the default handling.
492 if (offline_file_path.empty()) {
493 FallbackToDefault();
494 return;
495 }
496
497 // Sets the file path and lets URLRequestFileJob start to read from the file.
498 file_path_ = offline_file_path;
499 URLRequestFileJob::Start();
500 }
501
502 void OfflinePageRequestJob::SetDelegateForTesting(
503 std::unique_ptr<Delegate> delegate) {
504 delegate_ = std::move(delegate);
505 }
506
507 } // namespace offline_pages
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698