OLD | NEW |
---|---|
(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 | |
OLD | NEW |