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

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 some 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 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
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698