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

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

Powered by Google App Engine
This is Rietveld 408576698