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

Side by Side Diff: chrome/browser/installable/installable_checker.cc

Issue 2160513002: Extract AppBannerDataFetcher into an InstallableManager. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Addressing reviewer comments Created 4 years, 5 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/installable/installable_checker.h"
6
7 #include "base/bind.h"
8 #include "base/strings/string_util.h"
9 #include "chrome/browser/manifest/manifest_icon_downloader.h"
10 #include "chrome/browser/manifest/manifest_icon_selector.h"
11 #include "chrome/browser/profiles/profile.h"
12 #include "content/public/browser/browser_context.h"
13 #include "content/public/browser/browser_thread.h"
14 #include "content/public/browser/navigation_handle.h"
15 #include "content/public/browser/service_worker_context.h"
16 #include "content/public/browser/storage_partition.h"
17 #include "third_party/WebKit/public/platform/WebDisplayMode.h"
18
19 namespace {
20
21 const int kIconSizeUnset = -1;
22
23 const char kPngExtension[] = ".png";
24
25 // This constant is the icon size on Android (48dp) multiplied by the scale
26 // factor of a Nexus 5 device (3). For mobile and desktop platforms, a 144px
27 // icon is an approximate, appropriate lower bound.
28 // TODO(dominickn): consolidate this constant with minimum_icon_size_in_dp
29 // across platforms.
30 const int kIconMinimumSizeInPx = 144;
31
32 // Returns true if |manifest| specifies a PNG icon that is at least 144x144px
33 // (or has size "any").
34 bool DoesManifestContainRequiredIcon(const content::Manifest& manifest) {
35 for (const auto& icon : manifest.icons) {
36 // The type field is optional. If it isn't present, fall back on checking
37 // the src extension, and allow the icon if the extension ends with png.
38 if (!base::EqualsASCII(icon.type.string(), "image/png") &&
39 !(icon.type.is_null() &&
40 base::EndsWith(icon.src.ExtractFileName(), kPngExtension,
41 base::CompareCase::INSENSITIVE_ASCII)))
42 continue;
43
44 for (const auto& size : icon.sizes) {
45 if (size.IsEmpty()) // "any"
46 return true;
47 if (size.width() >= kIconMinimumSizeInPx &&
48 size.height() >= kIconMinimumSizeInPx) {
49 return true;
50 }
51 }
52 }
53
54 return false;
55 }
56
57 } // anonymous namespace
58
59 DEFINE_WEB_CONTENTS_USER_DATA_KEY(installable::InstallableChecker);
60
61 namespace installable {
62
63 InstallableParams::InstallableParams()
64 : ideal_icon_size_in_dp(kIconSizeUnset),
65 minimum_icon_size_in_dp(kIconSizeUnset),
66 check_valid_webapp_manifest(false),
67 check_service_worker(false),
68 check_valid_icon(false) { }
69
70 InstallableChecker::InstallableChecker(content::WebContents* web_contents)
71 : content::WebContentsObserver(web_contents),
72 status_(DORMANT),
73 processing_error_(NO_ERROR),
74 manifest_error_(NO_ERROR),
75 valid_webapp_manifest_error_(NO_ERROR),
76 service_worker_error_(NO_ERROR),
77 icon_error_(NO_ERROR),
78 ideal_icon_size_in_dp_(kIconSizeUnset),
79 minimum_icon_size_in_dp_(kIconSizeUnset),
80 has_valid_webapp_manifest_(false),
81 has_service_worker_(false),
82 weak_factory_(this) {}
83
84 InstallableChecker::~InstallableChecker() { }
85
86 bool InstallableChecker::IsManifestValidForWebApp(
87 const content::Manifest& manifest) {
88 if (manifest.IsEmpty()) {
89 valid_webapp_manifest_error_ = MANIFEST_EMPTY;
90 return false;
91 }
92
93 if (!manifest.start_url.is_valid()) {
94 valid_webapp_manifest_error_ = START_URL_NOT_VALID;
95 return false;
96 }
97
98 if ((manifest.name.is_null() || manifest.name.string().empty()) &&
99 (manifest.short_name.is_null() || manifest.short_name.string().empty())) {
100 valid_webapp_manifest_error_ = MANIFEST_MISSING_NAME_OR_SHORT_NAME;
101 return false;
102 }
103
104 // TODO(dominickn,mlamouri): when Chrome supports "minimal-ui", it should be
105 // accepted. If we accept it today, it would fallback to "browser" and make
106 // this check moot. See https://crbug.com/604390.
107 if (manifest.display != blink::WebDisplayModeStandalone &&
108 manifest.display != blink::WebDisplayModeFullscreen) {
109 valid_webapp_manifest_error_ = MANIFEST_DISPLAY_NOT_SUPPORTED;
110 return false;
111 }
112
113 if (!DoesManifestContainRequiredIcon(manifest)) {
114 valid_webapp_manifest_error_ = MANIFEST_MISSING_SUITABLE_ICON;
115 return false;
116 }
117
118 return true;
119 }
120
121 void InstallableChecker::Start(const InstallableParams& params,
122 const InstallableCallback& callback) {
123 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
124
125 // Always reset processing_error_, as it records events like navigation which
126 // are outside of fetching/validating resources.
127 processing_error_ = NO_ERROR;
128
129 // If we've already working on a task, or running callbacks, add the new task
130 // to the pending list and return. It will be dealt with once the current work
131 // completes. We keep the pending list separate in case this call was
132 // initiated during an InstallableCallback invocation, which modifies the
133 // tasks_ list.
134 if (IsActive() || HasFlag(RUNNING_CALLBACKS)) {
135 pending_tasks_.push_back({params, callback});
136 return;
137 }
138
139 tasks_.push_back({params, callback});
140 SetFlag(STARTED);
141 StartTask();
142 }
143
144 void InstallableChecker::Cancel() {
145 // Clear the STARTED flag to signal that we should stop work immediately.
146 // Callers of this method should immediately call back to FetchResource() or
147 // Start(), which will terminate the current task and run callbacks.
148 ClearFlag(STARTED);
149 }
150
151 bool InstallableChecker::DoesIconSizeMatch(
152 const InstallableParams& params) const {
153 return (ideal_icon_size_in_dp_ == params.ideal_icon_size_in_dp) &&
154 (minimum_icon_size_in_dp_ == params.minimum_icon_size_in_dp);
155 }
156
157 ErrorCode InstallableChecker::GetErrorCode(const InstallableParams& params) {
158 if (processing_error_ != NO_ERROR)
159 return processing_error_;
160 if (manifest_error_ != NO_ERROR)
161 return manifest_error_;
162 if (params.check_valid_webapp_manifest &&
163 valid_webapp_manifest_error_ != NO_ERROR) {
164 return valid_webapp_manifest_error_;
165 }
166 if (params.check_service_worker && service_worker_error_ != NO_ERROR)
167 return service_worker_error_;
168 if (params.check_valid_icon && icon_error_ != NO_ERROR)
169 return icon_error_;
170
171 return NO_ERROR;
172 }
173
174 content::WebContents* InstallableChecker::GetWebContents() {
175 content::WebContents* contents = web_contents();
176 if (!contents || contents->IsBeingDestroyed())
177 return nullptr;
178 return contents;
179 }
180
181 bool InstallableChecker::IsComplete(const InstallableParams& params) const {
182 // Returns true if for all resources:
183 // a. the params did not request it, OR
184 // b. the resource has been retrieved.
185 return (HasFlag(MANIFEST_FETCHED)) &&
186 (!params.check_valid_webapp_manifest || HasFlag(MANIFEST_VALIDATED)) &&
187 (!params.check_service_worker || HasFlag(SERVICE_WORKER_CHECKED)) &&
188 (!params.check_valid_icon ||
189 (HasFlag(ICON_FETCHED) && DoesIconSizeMatch(params)));
190 }
191
192 bool InstallableChecker::IsRunning(content::WebContents* web_contents) {
193 if (!web_contents) {
194 processing_error_ = RENDERER_EXITING;
195 return false;
196 }
197
198 if (!IsActive()) {
199 processing_error_ = USER_NAVIGATED;
200 return false;
201 }
202
203 return true;
204 }
205
206 void InstallableChecker::Reset() {
207 status_ = DORMANT;
208 processing_error_ = NO_ERROR;
209 manifest_error_ = NO_ERROR;
210 valid_webapp_manifest_error_ = NO_ERROR;
211 service_worker_error_ = NO_ERROR;
212 icon_error_ = NO_ERROR;
213
214 ideal_icon_size_in_dp_ = kIconSizeUnset;
215 minimum_icon_size_in_dp_ = kIconSizeUnset;
216
217 tasks_.clear();
218 pending_tasks_.clear();
219
220 manifest_url_ = GURL();
221 manifest_ = content::Manifest();
222 icon_url_ = GURL();
223 icon_.reset(nullptr);
224 has_valid_webapp_manifest_ = false;
225 has_service_worker_ = false;
226 }
227
228 void InstallableChecker::RunCallbacks() {
229 // Post a callback and delete it from the list of tasks if:
230 // - the STARTED status bit is missing. This means that we've either finished
231 // all possible checks, or we have canceled the pipeline
232 // - the params that the callback was started with have been satisfied.
233 // We run through the entire tasks_ vector here since we may have completed
234 // other tasks while working on the current one. For example, we may have
235 // initiated a full check on page load for app banners. Whilst that runs,
236 // multiple other requests may come in and be queued. When the app banner task
237 // completes, the queued requests will be moved to the tasks_ vector, and as
238 // we have already fetched all resources, we can dispatch their callbacks
239 // immediately.
240 SetFlag(RUNNING_CALLBACKS);
241 for (auto it = tasks_.begin(); it != tasks_.end();) {
242 const InstallableParams& params = it->first;
243 if (!IsActive() || IsComplete(params)) {
244 InstallableResult result = {
245 GetErrorCode(params), manifest_url_, manifest_,
246 params.check_valid_icon ? icon_url_ : GURL::EmptyGURL(),
247 params.check_valid_icon ? icon_.get() : nullptr,
248 params.check_valid_webapp_manifest ? has_valid_webapp_manifest_
249 : false,
250 params.check_service_worker ? has_service_worker_ : false};
251 // We must run this directly to guarantee the callback gets consistent
252 // results. If we PostTask, a second Task may begin that invalidates the
253 // icon object before the callback gets a chance to use it.
254 it->second.Run(result);
255 it = tasks_.erase(it);
256 } else {
257 ++it;
258 }
259 }
260 ClearFlag(RUNNING_CALLBACKS);
261 }
262
263 void InstallableChecker::StartTask() {
264 RunCallbacks();
265
266 if (!pending_tasks_.empty()) {
267 // Shift any pending tasks to the end of tasks_.
268 tasks_.insert(tasks_.end(), pending_tasks_.begin(), pending_tasks_.end());
269 pending_tasks_.clear();
270 }
271
272 // If there's nothing to do, exit. Resources remain cached so any future calls
273 // won't re-fetch anything that has already been retrieved.
274 if (tasks_.empty()) {
275 ClearFlag(STARTED);
276 return;
277 }
278
279 // If we are requesting an icon, and the requested size differs from any
280 // previously fetched (or if we haven't yet fetched an icon), reset the icon
281 // state. This has no impact on the other resources, so don't reset them.
282 const InstallableParams& params = tasks_[0].first;
283 if (params.check_valid_icon && !DoesIconSizeMatch(params)) {
284 ideal_icon_size_in_dp_ = params.ideal_icon_size_in_dp;
285 minimum_icon_size_in_dp_ = params.minimum_icon_size_in_dp;
286 icon_url_ = GURL();
287 icon_.reset(nullptr);
288 icon_error_ = NO_ERROR;
289 ClearFlag(ICON_FETCHED);
290 }
291
292 FetchResource();
293 }
294
295 void InstallableChecker::FetchResource() {
296 DCHECK(!tasks_.empty());
297 const InstallableParams& params = tasks_[0].first;
298
299 // Cancel if there is an error code for any resource requested by params.
300 if (GetErrorCode(params) != NO_ERROR)
301 Cancel();
302
303 // If not active, then Cancel() has been called. Go straight back to
304 // StartTask, which will clear the current task. Otherwise, if we fall through
305 // to the else case, we've fetched everything necessary for this task, so
306 // call StartTask to run its callback and start the next task.
307 if (!IsActive())
308 StartTask();
309 else if (!HasFlag(MANIFEST_FETCHED))
310 FetchManifest();
311 else if (params.check_valid_webapp_manifest && !HasFlag(MANIFEST_VALIDATED))
312 CheckValidWebappManifest();
313 else if (params.check_service_worker && !HasFlag(SERVICE_WORKER_CHECKED))
314 CheckServiceWorker();
315 else if (params.check_valid_icon && !HasFlag(ICON_FETCHED))
316 ExtractAndFetchBestIcon();
317 else
318 StartTask();
319 }
320
321 void InstallableChecker::FetchManifest() {
322 DCHECK(!HasFlag(MANIFEST_FETCHED));
323 DCHECK(manifest_.IsEmpty());
324 DCHECK(manifest_url_.is_empty());
325
326 content::WebContents* web_contents = GetWebContents();
327 DCHECK(web_contents);
328
329 web_contents->GetManifest(base::Bind(&InstallableChecker::OnDidGetManifest,
330 weak_factory_.GetWeakPtr()));
331 }
332
333 void InstallableChecker::OnDidGetManifest(const GURL& manifest_url,
334 const content::Manifest& manifest) {
335 if (!IsRunning(GetWebContents()))
336 Cancel();
337 else if (manifest_url.is_empty())
338 manifest_error_ = NO_MANIFEST;
339 else if (manifest.IsEmpty())
340 manifest_error_ = MANIFEST_EMPTY;
341
342 manifest_url_ = manifest_url;
343 manifest_ = manifest;
344
345 SetFlag(MANIFEST_FETCHED);
346 FetchResource();
347 }
348
349 void InstallableChecker::CheckValidWebappManifest() {
350 DCHECK(!HasFlag(MANIFEST_VALIDATED));
351 DCHECK(!manifest_.IsEmpty());
352
353 has_valid_webapp_manifest_ = IsManifestValidForWebApp(manifest_);
354 if (!has_valid_webapp_manifest_)
355 Cancel();
356
357 SetFlag(MANIFEST_VALIDATED);
358 FetchResource();
359 }
360
361 void InstallableChecker::CheckServiceWorker() {
362 DCHECK(!HasFlag(SERVICE_WORKER_CHECKED));
363 DCHECK(!manifest_.IsEmpty());
364
365 content::WebContents* web_contents = GetWebContents();
366
367 // Check to see if there is a single service worker controlling this page
368 // and the manifest's start url.
369 Profile* profile =
370 Profile::FromBrowserContext(web_contents->GetBrowserContext());
371 content::StoragePartition* storage_partition =
372 content::BrowserContext::GetStoragePartition(
373 profile, web_contents->GetSiteInstance());
374 DCHECK(storage_partition);
375
376 storage_partition->GetServiceWorkerContext()->CheckHasServiceWorker(
377 web_contents->GetLastCommittedURL(), manifest_.start_url,
378 base::Bind(&InstallableChecker::OnDidCheckHasServiceWorker,
379 weak_factory_.GetWeakPtr()));
380 }
381
382 void InstallableChecker::OnDidCheckHasServiceWorker(bool has_service_worker) {
383 if (!IsRunning(GetWebContents()))
384 Cancel();
385
386 has_service_worker_ = has_service_worker;
387 if (!has_service_worker)
388 service_worker_error_ = NO_MATCHING_SERVICE_WORKER;
389
390 SetFlag(SERVICE_WORKER_CHECKED);
391 FetchResource();
392 }
393
394 void InstallableChecker::ExtractAndFetchBestIcon() {
395 // icon_url_ and icon_ should have both been reset if this method is called.
396 DCHECK(!HasFlag(ICON_FETCHED));
397 DCHECK(!manifest_.IsEmpty());
398 DCHECK(icon_url_.is_empty());
399 DCHECK(icon_.get() == nullptr);
400 DCHECK_GT(ideal_icon_size_in_dp_, 0);
401 DCHECK_GT(minimum_icon_size_in_dp_, 0);
402
403 GURL icon_url = ManifestIconSelector::FindBestMatchingIcon(
404 manifest_.icons, ideal_icon_size_in_dp_, minimum_icon_size_in_dp_);
405
406 // First, check the icon URL. If it exists, see if the ManifestIconDownloader
407 // is able to download it. If it can, it will call OnAppIconFetched. Any
408 // conditional failures will set the ICON_FETCHED bit and fall through back to
409 // FetchResource.
410 if (icon_url.is_empty()) {
411 icon_error_ = NO_ACCEPTABLE_ICON;
412 } else {
413 bool can_download_icon = ManifestIconDownloader::Download(
414 GetWebContents(), icon_url, ideal_icon_size_in_dp_,
415 minimum_icon_size_in_dp_,
416 base::Bind(&InstallableChecker::OnAppIconFetched,
417 weak_factory_.GetWeakPtr(), icon_url));
418 if (can_download_icon)
419 return;
420 icon_error_ = CANNOT_DOWNLOAD_ICON;
421 }
422
423 SetFlag(ICON_FETCHED);
424 FetchResource();
425 }
426
427 void InstallableChecker::OnAppIconFetched(const GURL icon_url,
428 const SkBitmap& bitmap) {
429 if (!IsRunning(GetWebContents())) {
430 Cancel();
431 } else if (bitmap.drawsNothing()) {
432 icon_error_ = NO_ICON_AVAILABLE;
433 } else {
434 icon_url_ = icon_url;
435 icon_.reset(new SkBitmap(bitmap));
436 }
437
438 SetFlag(ICON_FETCHED);
439 FetchResource();
440 }
441
442 void InstallableChecker::DidFinishNavigation(
443 content::NavigationHandle* handle) {
444 if (handle->IsInMainFrame() && handle->HasCommitted() &&
445 !handle->IsErrorPage() && !handle->IsSamePage()) {
446 Reset();
447 }
448 }
449
450 void InstallableChecker::WebContentsDestroyed() {
451 Reset();
452 Observe(nullptr);
453 }
454
455 // static
456 int InstallableChecker::GetMinimumIconSizeInPx() {
457 return kIconMinimumSizeInPx;
458 }
459
460 } // namespace installable
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698