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

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: 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 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
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.
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(
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
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
fgorski 2016/08/24 21:02:37 s/neeed/need/
jianli 2016/08/24 22:30:58 Done.
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
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
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);
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
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(
306 *offline_page, network_state == NetworkState::PROHIBITIVELY_SLOW_NETWORK);
307
308 *offline_file_path = offline_page->file_path;
309 return RequestResult::OFFLINE_PAGE_SERVED;
310 }
311
312 // Handles the result of finding an offline page.
313 void SelectPageForOnlineURLDone(
314 NetworkState network_state,
315 base::WeakPtr<OfflinePageRequestJob> job,
316 content::ResourceRequestInfo::WebContentsGetter web_contents_getter,
317 const OfflinePageItem* offline_page) {
318 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
319
320 base::FilePath offline_file_path;
321 RequestResult request_result = GetOfflineFilePath(
322 network_state, job, web_contents_getter, offline_page,
323 &offline_file_path);
324
325 ReportRequestResult(request_result, network_state);
326
327 // NotifyOfflineFilePathOnUI should always be called regardless the failure
328 // result and empty file path such that OfflinePageRequestJob will be notified
329 // on failure.
330 NotifyOfflineFilePathOnUI(job, offline_file_path);
331 }
332
333 void FailedToSelectOfflinePage(base::WeakPtr<OfflinePageRequestJob> job) {
334 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
335
336 // Proceed with empty file path in order to notify the OfflinePageRequestJob
337 // about the failure.
338 base::FilePath empty_file_path;
339 NotifyOfflineFilePathOnUI(job, empty_file_path);
340 }
341
342 // Tries to find the offline page to serve for |online_url|.
343 void SelectOfflinePage(
344 const GURL& online_url,
345 NetworkState network_state,
346 void* profile_id,
347 content::ResourceRequestInfo::WebContentsGetter web_contents_getter,
348 OfflinePageRequestJob::Delegate::TabIdGetter tab_id_getter,
349 base::WeakPtr<OfflinePageRequestJob> job) {
350 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
351
352 // |profile_id| needs to be checked with ProfileManager::IsValidProfile
353 // before using it.
354 if (!g_browser_process->profile_manager()->IsValidProfile(profile_id)) {
355 FailedToSelectOfflinePage(job);
356 return;
357 }
358 Profile* profile = reinterpret_cast<Profile*>(profile_id);
359
360 content::WebContents* web_contents = web_contents_getter.Run();
361 if (!web_contents){
362 ReportRequestResult(RequestResult::NO_WEB_CONTENTS, network_state);
363 FailedToSelectOfflinePage(job);
364 return;
365 }
366 int tab_id;
367 if (!tab_id_getter.Run(web_contents, &tab_id)) {
368 ReportRequestResult(RequestResult::NO_TAB_ID, network_state);
369 FailedToSelectOfflinePage(job);
370 return;
371 }
372
373 OfflinePageUtils::SelectPageForOnlineURL(
374 profile,
375 online_url,
376 tab_id,
377 base::Bind(&SelectPageForOnlineURLDone,
378 network_state,
379 job,
380 web_contents_getter));
381 }
382
383 } // namespace
384
385 // static
386 void OfflinePageRequestJob::ReportAggregatedRequestResult(
387 AggregatedRequestResult result) {
388 UMA_HISTOGRAM_ENUMERATION("OfflinePages.AggregatedRequestResult",
389 static_cast<int>(result),
390 static_cast<int>(AggregatedRequestResult::AGGREGATED_REQUEST_RESULT_MAX));
391 }
392
393 // static
394 OfflinePageRequestJob* OfflinePageRequestJob::Create(
395 void* profile_id,
396 net::URLRequest* request,
397 net::NetworkDelegate* network_delegate) {
398 const content::ResourceRequestInfo* resource_request_info =
399 content::ResourceRequestInfo::ForRequest(request);
400 if (!resource_request_info)
401 return nullptr;
402
403 // Ignore the requests not for the main resource.
404 if (resource_request_info->GetResourceType() !=
405 content::RESOURCE_TYPE_MAIN_FRAME) {
406 return nullptr;
407 }
408
409 // Ignore non-http/https requests.
410 if (!request->url().SchemeIsHTTPOrHTTPS())
411 return nullptr;
412
413 // Ignore requests other than GET.
414 if (request->method() != "GET")
415 return nullptr;
416
417 OfflinePageRequestInfo* info =
418 OfflinePageRequestInfo::GetFromRequest(request);
419 if (info) {
420 // Fall back to default which is set when an offline page cannot be served,
421 // either page not found or online version desired.
422 if (info->use_default())
423 return nullptr;
424 } else {
425 request->SetUserData(&kUserDataKey, new OfflinePageRequestInfo());
426 }
427
428 return new OfflinePageRequestJob(profile_id, request, network_delegate);
429 }
430
431 OfflinePageRequestJob::OfflinePageRequestJob(
432 void* profile_id,
433 net::URLRequest* request,
434 net::NetworkDelegate* network_delegate)
435 : net::URLRequestFileJob(
436 request,
437 network_delegate,
438 base::FilePath(),
439 content::BrowserThread::GetBlockingPool()->
440 GetTaskRunnerWithShutdownBehavior(
441 base::SequencedWorkerPool::SKIP_ON_SHUTDOWN)),
442 profile_id_(profile_id),
443 delegate_(new DefaultDelegate()),
444 weak_ptr_factory_(this) {
445 }
446
447 OfflinePageRequestJob::~OfflinePageRequestJob() {
448 }
449
450 void OfflinePageRequestJob::Start() {
451 base::ThreadTaskRunnerHandle::Get()->PostTask(
452 FROM_HERE, base::Bind(&OfflinePageRequestJob::StartAsync,
453 weak_ptr_factory_.GetWeakPtr()));
454 }
455
456 void OfflinePageRequestJob::StartAsync() {
457 NetworkState network_state = GetNetworkState(request());
458 if (network_state == NetworkState::CONNECTED_NETWORK) {
459 FallbackToDefault();
460 return;
461 }
462
463 content::BrowserThread::PostTask(
464 content::BrowserThread::UI,
465 FROM_HERE,
466 base::Bind(&SelectOfflinePage,
467 request()->url(),
468 network_state,
469 profile_id_,
470 delegate_->GetWebContentsGetter(request()),
471 delegate_->GetTabIdGetter(),
472 weak_ptr_factory_.GetWeakPtr()));
473 }
474
475 void OfflinePageRequestJob::Kill() {
476 net::URLRequestJob::Kill();
477 weak_ptr_factory_.InvalidateWeakPtrs();
478 }
479
480 void OfflinePageRequestJob::FallbackToDefault() {
481 OfflinePageRequestInfo* info =
482 OfflinePageRequestInfo::GetFromRequest(request());
483 DCHECK(info);
484 info->set_use_default(true);
485
486 URLRequestJob::NotifyRestartRequired();
487 }
488
489 void OfflinePageRequestJob::OnOfflineFilePathAvailable(
490 const base::FilePath& offline_file_path) {
491 // If offline file path is empty, it means that offline page cannot be found
492 // and we want to restart the job to fall back to the default handling.
493 if (offline_file_path.empty()) {
494 FallbackToDefault();
495 return;
496 }
497
498 // Sets the file path and lets URLRequestFileJob start to read from the file.
499 file_path_ = offline_file_path;
500 URLRequestFileJob::Start();
501 }
502
503 void OfflinePageRequestJob::SetDelegateForTesting(
504 std::unique_ptr<Delegate> delegate) {
505 delegate_ = std::move(delegate);
506 }
507
508 } // namespace offline_pages
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698