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

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

Powered by Google App Engine
This is Rietveld 408576698