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/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 } | |
OLD | NEW |