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 int kUserDataKey; // Only address is used. | |
69 | |
70 // Contains the info to handle offline page request. | |
71 struct OfflinePageRequestInfo : public base::SupportsUserData::Data { | |
72 OfflinePageRequestInfo() : use_default(false) {} | |
73 ~OfflinePageRequestInfo() override {} | |
74 | |
75 static OfflinePageRequestInfo* GetFromRequest(net::URLRequest* request) { | |
76 return static_cast<OfflinePageRequestInfo*>( | |
77 request->GetUserData(&kUserDataKey)); | |
78 } | |
79 | |
80 // True if the next time this request is started, the request should be | |
81 // serviced from the default handler. | |
82 bool use_default; | |
83 }; | |
84 | |
85 // Returns true if custom offline header is present. | |
86 // |reason| may be set with the reason to trigger the offline page loading. | |
87 bool ParseOfflineHeader(net::URLRequest* request, std::string* reason) { | |
88 std::string value; | |
89 if (!request->extra_request_headers().GetHeader(kLoadingOfflinePageHeader, | |
90 &value)) { | |
91 return false; | |
92 } | |
93 | |
94 // Currently we only support reason field. | |
95 base::StringTokenizer tokenizer(value, ", "); | |
96 while (tokenizer.GetNext()) { | |
97 if (base::StartsWith(tokenizer.token(), | |
98 kLoadingOfflinePageReason, | |
99 base::CompareCase::INSENSITIVE_ASCII)) { | |
100 *reason = tokenizer.token().substr( | |
101 arraysize(kLoadingOfflinePageReason) - 1); | |
102 break; | |
103 } | |
104 } | |
105 return true; | |
106 } | |
107 | |
108 bool IsNetworkProhibitivelySlow(net::URLRequest* request) { | |
109 // NetworkQualityEstimator only works when it is enabled. | |
110 if (!previews::IsOfflinePreviewsEnabled()) | |
111 return false; | |
112 | |
113 if (!request->context()) | |
114 return false; | |
115 | |
116 net::NetworkQualityEstimator* network_quality_estimator = | |
117 request->context()->network_quality_estimator(); | |
118 if (network_quality_estimator) | |
119 return false; | |
120 | |
121 net::EffectiveConnectionType effective_connection_type = | |
122 network_quality_estimator->GetEffectiveConnectionType(); | |
123 return effective_connection_type >= net::EFFECTIVE_CONNECTION_TYPE_OFFLINE && | |
124 effective_connection_type <= net::EFFECTIVE_CONNECTION_TYPE_SLOW_2G; | |
125 } | |
126 | |
127 NetworkState GetNetworkState(net::URLRequest* request) { | |
128 DCHECK_CURRENTLY_ON(content::BrowserThread::IO); | |
129 | |
130 std::string reason; | |
131 bool has_offline_header = ParseOfflineHeader(request, &reason); | |
132 if (has_offline_header && | |
133 base::EqualsCaseInsensitiveASCII(reason, | |
134 kLoadingOfflinePageDueToNetError)) { | |
135 return NetworkState::FLAKY_NETWORK; | |
136 } | |
137 | |
138 if (net::NetworkChangeNotifier::IsOffline()) | |
139 return NetworkState::DISCONNECTED_NETWORK; | |
140 | |
141 if (IsNetworkProhibitivelySlow(request)) | |
142 return NetworkState::PROHIBITIVELY_SLOW_NETWORK; | |
143 | |
144 return has_offline_header ? NetworkState::FORCE_OFFLINE_ON_CONNECTED_NETWORK | |
145 : NetworkState::CONNECTED_NETWORK; | |
146 } | |
147 | |
148 AggregatedRequestResult RequestResultToAggregatedRequestResult( | |
149 RequestResult request_result, NetworkState network_state) { | |
150 if (request_result == RequestResult::NO_TAB_ID) | |
151 return AggregatedRequestResult::NO_TAB_ID; | |
152 | |
153 if (request_result == RequestResult::NO_WEB_CONTENTS) | |
154 return AggregatedRequestResult::NO_WEB_CONTENTS; | |
155 | |
156 if (request_result == RequestResult::PAGE_NOT_FRESH) { | |
157 DCHECK_EQ(NetworkState::PROHIBITIVELY_SLOW_NETWORK, network_state); | |
158 return | |
159 AggregatedRequestResult::PAGE_NOT_FRESH_ON_PROHIBITIVELY_SLOW_NETWORK; | |
160 } | |
161 | |
162 if (request_result == RequestResult::OFFLINE_PAGE_NOT_FOUND) { | |
163 switch (network_state) { | |
164 case NetworkState::DISCONNECTED_NETWORK: | |
165 return AggregatedRequestResult::PAGE_NOT_FOUND_ON_DISCONNECTED_NETWORK; | |
166 case NetworkState::PROHIBITIVELY_SLOW_NETWORK: | |
167 return AggregatedRequestResult:: | |
168 PAGE_NOT_FOUND_ON_PROHIBITIVELY_SLOW_NETWORK; | |
169 case NetworkState::FLAKY_NETWORK: | |
170 return AggregatedRequestResult::PAGE_NOT_FOUND_ON_FLAKY_NETWORK; | |
171 case NetworkState::FORCE_OFFLINE_ON_CONNECTED_NETWORK: | |
172 return AggregatedRequestResult::PAGE_NOT_FOUND_ON_CONNECTED_NETWORK; | |
173 default: | |
174 NOTREACHED(); | |
175 } | |
176 } | |
177 | |
178 DCHECK_EQ(RequestResult::OFFLINE_PAGE_SERVED, request_result); | |
179 switch (network_state) { | |
180 case NetworkState::DISCONNECTED_NETWORK: | |
181 return AggregatedRequestResult::SHOW_OFFLINE_ON_DISCONNECTED_NETWORK; | |
182 case NetworkState::PROHIBITIVELY_SLOW_NETWORK: | |
183 return AggregatedRequestResult:: | |
184 SHOW_OFFLINE_ON_PROHIBITIVELY_SLOW_NETWORK; | |
185 case NetworkState::FLAKY_NETWORK: | |
186 return AggregatedRequestResult::SHOW_OFFLINE_ON_FLAKY_NETWORK; | |
187 case NetworkState::CONNECTED_NETWORK: | |
188 return AggregatedRequestResult::SHOW_OFFLINE_ON_CONNECTED_NETWORK; | |
189 case NetworkState::FORCE_OFFLINE_ON_CONNECTED_NETWORK: | |
190 return AggregatedRequestResult::SHOW_OFFLINE_ON_CONNECTED_NETWORK; | |
Dmitry Titov
2016/08/20 01:08:14
Could you add a category here, for FORCE_OFFLINE_O
jianli
2016/08/20 01:48:52
Indeed we should not hit "case NetworkState::CONNE
| |
191 default: | |
192 NOTREACHED(); | |
193 } | |
194 | |
195 return AggregatedRequestResult::AGGREGATED_REQUEST_RESULT_MAX; | |
196 } | |
197 | |
198 void ReportRequestResult( | |
199 RequestResult request_result, NetworkState network_state) { | |
200 OfflinePageRequestJob::ReportAggregatedRequestResult( | |
201 RequestResultToAggregatedRequestResult(request_result, network_state)); | |
202 } | |
203 | |
204 void NotifyOfflineFilePathOnIO(base::WeakPtr<OfflinePageRequestJob> job, | |
205 const base::FilePath& offline_file_path) { | |
206 DCHECK_CURRENTLY_ON(content::BrowserThread::IO); | |
207 | |
208 if (!job) | |
209 return; | |
210 job->OnOfflineFilePathAvailable(offline_file_path); | |
211 } | |
212 | |
213 // Notifies OfflinePageRequestJob about the offline file path. Note that the | |
214 // file path may be empty if not found or on error. | |
215 void NotifyOfflineFilePathOnUI(base::WeakPtr<OfflinePageRequestJob> job, | |
216 const base::FilePath& offline_file_path) { | |
217 DCHECK_CURRENTLY_ON(content::BrowserThread::UI); | |
218 | |
219 // Delegates to IO thread since OfflinePageRequestJob should only be accessed | |
220 // from IO thread. | |
221 content::BrowserThread::PostTask( | |
222 content::BrowserThread::IO, | |
223 FROM_HERE, | |
224 base::Bind(&NotifyOfflineFilePathOnIO, job, offline_file_path)); | |
225 } | |
226 | |
227 // Finds the offline file path based on the select page result and network | |
228 // state. | |
229 RequestResult GetOfflineFilePath( | |
230 NetworkState network_state, | |
231 base::WeakPtr<OfflinePageRequestJob> job, | |
232 content::ResourceRequestInfo::WebContentsGetter web_contents_getter, | |
233 const OfflinePageItem* offline_page, | |
234 base::FilePath* offline_file_path) { | |
235 DCHECK_CURRENTLY_ON(content::BrowserThread::UI); | |
236 | |
237 if (!offline_page) | |
238 return RequestResult::OFFLINE_PAGE_NOT_FOUND; | |
239 | |
240 // |web_contents_getter| is passed from IO thread. We neeed to check if | |
241 // web contents is still valid. | |
242 content::WebContents* web_contents = web_contents_getter.Run(); | |
243 if (!web_contents) | |
244 return RequestResult::NO_WEB_CONTENTS; | |
245 | |
246 // If the page is being loaded on a slow network, only use the offline page | |
247 // if it was created within the past day. | |
248 // TODO(romax): Make the constant be policy driven. | |
249 if (network_state == NetworkState::PROHIBITIVELY_SLOW_NETWORK && | |
250 base::Time::Now() - offline_page->creation_time > | |
251 base::TimeDelta::FromDays(1)) { | |
252 return RequestResult::PAGE_NOT_FRESH; | |
253 } | |
254 | |
255 // Since offline page will be loaded, it should be marked as accessed. | |
256 OfflinePageModel* offline_page_model = | |
257 OfflinePageModelFactory::GetForBrowserContext( | |
258 web_contents->GetBrowserContext()); | |
259 DCHECK(offline_page_model); | |
260 offline_page_model->MarkPageAccessed(offline_page->offline_id); | |
261 | |
262 // Save an cached copy of OfflinePageItem such that Tab code can get | |
263 // the loaded offline page immediately. | |
264 OfflinePageTabHelper* tab_helper = | |
265 OfflinePageTabHelper::FromWebContents(web_contents); | |
266 DCHECK(tab_helper); | |
267 tab_helper->SetOfflinePage(*offline_page); | |
268 | |
269 *offline_file_path = offline_page->file_path; | |
270 return RequestResult::OFFLINE_PAGE_SERVED; | |
271 } | |
272 | |
273 // Handles the result of finding an offline page. | |
274 void SelectPageForOnlineURLDone( | |
275 NetworkState network_state, | |
276 base::WeakPtr<OfflinePageRequestJob> job, | |
277 content::ResourceRequestInfo::WebContentsGetter web_contents_getter, | |
278 const OfflinePageItem* offline_page) { | |
279 DCHECK_CURRENTLY_ON(content::BrowserThread::UI); | |
280 | |
281 base::FilePath offline_file_path; | |
282 RequestResult request_result = GetOfflineFilePath( | |
283 network_state, job, web_contents_getter, offline_page, | |
284 &offline_file_path); | |
285 | |
286 ReportRequestResult(request_result, network_state); | |
287 | |
288 // NotifyOfflineFilePathOnUI should always be called regardless the failure | |
289 // result and empty file path such that OfflinePageRequestJob will be notified | |
290 // on failure. | |
291 NotifyOfflineFilePathOnUI(job, offline_file_path); | |
292 } | |
293 | |
294 void FailedToSelectOfflinePage(base::WeakPtr<OfflinePageRequestJob> job) { | |
295 DCHECK_CURRENTLY_ON(content::BrowserThread::UI); | |
296 | |
297 // Proceed with empty file path in order to notify the OfflinePageRequestJob | |
298 // about the failure. | |
299 base::FilePath empty_file_path; | |
300 NotifyOfflineFilePathOnUI(job, empty_file_path); | |
301 } | |
302 | |
303 // Tries to find the offline page to serve for |online_url|. | |
304 void SelectOfflinePage( | |
305 const GURL& online_url, | |
306 NetworkState network_state, | |
307 void* profile_id, | |
308 content::ResourceRequestInfo::WebContentsGetter web_contents_getter, | |
309 base::WeakPtr<OfflinePageRequestJob> job) { | |
310 DCHECK_CURRENTLY_ON(content::BrowserThread::UI); | |
311 | |
312 // |profile_id| needs to be checked with ProfileManager::IsValidProfile | |
313 // before using it. | |
314 if (!g_browser_process->profile_manager()->IsValidProfile(profile_id)) { | |
315 FailedToSelectOfflinePage(job); | |
316 return; | |
317 } | |
318 Profile* profile = reinterpret_cast<Profile*>(profile_id); | |
319 | |
320 content::WebContents* web_contents = web_contents_getter.Run(); | |
321 if (!web_contents){ | |
322 ReportRequestResult(RequestResult::NO_WEB_CONTENTS, network_state); | |
323 FailedToSelectOfflinePage(job); | |
324 return; | |
325 } | |
326 int tab_id; | |
327 if (!OfflinePageUtils::GetTabId(web_contents, &tab_id)) { | |
328 ReportRequestResult(RequestResult::NO_TAB_ID, network_state); | |
329 FailedToSelectOfflinePage(job); | |
330 return; | |
331 } | |
332 | |
333 OfflinePageUtils::SelectPageForOnlineURL( | |
334 profile, | |
335 online_url, | |
336 tab_id, | |
337 base::Bind(&SelectPageForOnlineURLDone, | |
338 network_state, | |
339 job, | |
340 web_contents_getter)); | |
341 } | |
342 | |
343 } // namespace | |
344 | |
345 // static | |
346 void OfflinePageRequestJob::ReportAggregatedRequestResult( | |
347 AggregatedRequestResult result) { | |
348 UMA_HISTOGRAM_ENUMERATION("OfflinePages.AggregatedRequestResult", | |
349 static_cast<int>(result), | |
350 static_cast<int>(AggregatedRequestResult::AGGREGATED_REQUEST_RESULT_MAX)); | |
351 } | |
352 | |
353 // static | |
354 OfflinePageRequestJob* OfflinePageRequestJob::Create( | |
355 void* profile_id, | |
356 net::URLRequest* request, | |
357 net::NetworkDelegate* network_delegate) { | |
358 const content::ResourceRequestInfo* resource_request_info = | |
359 content::ResourceRequestInfo::ForRequest(request); | |
360 if (!resource_request_info) | |
361 return nullptr; | |
362 | |
363 // Ignore the requests not for the main resource. | |
364 if (resource_request_info->GetResourceType() != | |
365 content::RESOURCE_TYPE_MAIN_FRAME) { | |
366 return nullptr; | |
367 } | |
368 | |
369 // Ignore non-http/https requests. | |
370 if (!request->url().SchemeIsHTTPOrHTTPS()) | |
371 return nullptr; | |
372 | |
373 // Ignore requests other than GET. | |
374 if (request->method() != "GET") | |
375 return nullptr; | |
376 | |
377 OfflinePageRequestInfo* info = | |
378 OfflinePageRequestInfo::GetFromRequest(request); | |
379 if (info) { | |
380 // Fall back to default which is set when an offline page cannot be served, | |
381 // either page not found or online version desired. | |
382 if (info->use_default) | |
383 return nullptr; | |
384 } else { | |
385 request->SetUserData(&kUserDataKey, new OfflinePageRequestInfo()); | |
386 } | |
387 | |
388 return new OfflinePageRequestJob(profile_id, request, network_delegate); | |
389 } | |
390 | |
391 OfflinePageRequestJob::OfflinePageRequestJob( | |
392 void* profile_id, | |
393 net::URLRequest* request, | |
394 net::NetworkDelegate* network_delegate) | |
395 : net::URLRequestFileJob( | |
396 request, | |
397 network_delegate, | |
398 base::FilePath(), | |
399 content::BrowserThread::GetBlockingPool()-> | |
400 GetTaskRunnerWithShutdownBehavior( | |
401 base::SequencedWorkerPool::SKIP_ON_SHUTDOWN)), | |
402 profile_id_(profile_id), | |
403 weak_ptr_factory_(this) { | |
404 } | |
405 | |
406 OfflinePageRequestJob::~OfflinePageRequestJob() { | |
407 } | |
408 | |
409 void OfflinePageRequestJob::Start() { | |
410 base::ThreadTaskRunnerHandle::Get()->PostTask( | |
411 FROM_HERE, base::Bind(&OfflinePageRequestJob::StartAsync, | |
412 weak_ptr_factory_.GetWeakPtr())); | |
413 } | |
414 | |
415 void OfflinePageRequestJob::StartAsync() { | |
416 NetworkState network_state = GetNetworkState(request()); | |
417 if (network_state == NetworkState::CONNECTED_NETWORK) { | |
418 FallbackToDefault(); | |
419 return; | |
420 } | |
421 | |
422 content::BrowserThread::PostTask( | |
423 content::BrowserThread::UI, | |
424 FROM_HERE, | |
425 base::Bind(&SelectOfflinePage, | |
426 request()->url(), | |
427 network_state, | |
428 profile_id_, | |
429 content::ResourceRequestInfo::ForRequest(request())-> | |
430 GetWebContentsGetterForRequest(), | |
431 weak_ptr_factory_.GetWeakPtr())); | |
432 } | |
433 | |
434 void OfflinePageRequestJob::Kill() { | |
435 net::URLRequestJob::Kill(); | |
436 weak_ptr_factory_.InvalidateWeakPtrs(); | |
437 } | |
438 | |
439 void OfflinePageRequestJob::FallbackToDefault() { | |
440 OfflinePageRequestInfo* info = | |
441 OfflinePageRequestInfo::GetFromRequest(request()); | |
442 DCHECK(info); | |
443 info->use_default = true; | |
444 | |
445 URLRequestJob::NotifyRestartRequired(); | |
446 } | |
447 | |
448 void OfflinePageRequestJob::OnOfflineFilePathAvailable( | |
449 const base::FilePath& offline_file_path) { | |
450 // If offline file path is empty, it means that offline page cannot be found | |
451 // and we want to restart the job to fall back to the default handling. | |
452 if (offline_file_path.empty()) { | |
453 FallbackToDefault(); | |
454 return; | |
455 } | |
456 | |
457 // Sets the file path and lets URLRequestFileJob start to read from the file. | |
458 file_path_ = offline_file_path; | |
459 URLRequestFileJob::Start(); | |
460 } | |
461 | |
462 } // namespace offline_pages | |
OLD | NEW |