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_manager.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 char kPngExtension[] = ".png"; | |
22 | |
23 // This constant is the icon size on Android (48dp) multiplied by the scale | |
24 // factor of a Nexus 5 device (3x). For mobile and desktop platforms, a 144px | |
25 // icon is an approximate, appropriate lower bound. | |
26 // TODO(dominickn): consolidate with minimum_icon_size_in_dp across platforms. | |
27 const int kIconMinimumSizeInPx = 144; | |
28 | |
29 // Returns true if |manifest| specifies a PNG icon >= 144x144px (or size "any"). | |
30 bool DoesManifestContainRequiredIcon(const content::Manifest& manifest) { | |
31 for (const auto& icon : manifest.icons) { | |
32 // The type field is optional. If it isn't present, fall back on checking | |
33 // the src extension, and allow the icon if the extension ends with png. | |
34 if (!base::EqualsASCII(icon.type.string(), "image/png") && | |
35 !(icon.type.is_null() && | |
36 base::EndsWith(icon.src.ExtractFileName(), kPngExtension, | |
37 base::CompareCase::INSENSITIVE_ASCII))) | |
38 continue; | |
39 | |
40 for (const auto& size : icon.sizes) { | |
41 if (size.IsEmpty()) // "any" | |
42 return true; | |
43 if (size.width() >= kIconMinimumSizeInPx && | |
44 size.height() >= kIconMinimumSizeInPx) { | |
45 return true; | |
46 } | |
47 } | |
48 } | |
49 | |
50 return false; | |
51 } | |
52 | |
53 } // anonymous namespace | |
54 | |
55 DEFINE_WEB_CONTENTS_USER_DATA_KEY(InstallableManager); | |
56 | |
57 struct InstallableManager::ManifestProperty { | |
58 InstallableErrorCode error = NO_ERROR_DETECTED; | |
59 GURL url; | |
60 content::Manifest manifest; | |
61 bool fetched = false; | |
62 }; | |
63 | |
64 struct InstallableManager::InstallableProperty { | |
65 InstallableErrorCode error = NO_ERROR_DETECTED; | |
66 bool installable = false; | |
67 bool fetched = false; | |
68 }; | |
69 | |
70 struct InstallableManager::IconProperty { | |
71 IconProperty() : | |
72 error(NO_ERROR_DETECTED), url(), icon(), fetched(false) { } | |
73 IconProperty(IconProperty&& other) = default; | |
74 IconProperty& operator=(IconProperty&& other) = default; | |
75 | |
76 InstallableErrorCode error = NO_ERROR_DETECTED; | |
77 GURL url; | |
78 std::unique_ptr<SkBitmap> icon; | |
79 bool fetched; | |
80 | |
81 private: | |
82 // This class contains a std::unique_ptr and therefore must be move-only. | |
83 DISALLOW_COPY_AND_ASSIGN(IconProperty); | |
84 }; | |
85 | |
86 | |
87 InstallableManager::InstallableManager(content::WebContents* web_contents) | |
88 : content::WebContentsObserver(web_contents), | |
89 manifest_(new ManifestProperty()), | |
90 installable_(new InstallableProperty()), | |
91 is_active_(false), | |
92 weak_factory_(this) { } | |
93 | |
94 InstallableManager::~InstallableManager() = default; | |
95 | |
96 bool InstallableManager::IsManifestValidForWebApp( | |
97 const content::Manifest& manifest) { | |
98 if (manifest.IsEmpty()) { | |
99 installable_->error = MANIFEST_EMPTY; | |
100 return false; | |
101 } | |
102 | |
103 if (!manifest.start_url.is_valid()) { | |
104 installable_->error = START_URL_NOT_VALID; | |
105 return false; | |
106 } | |
107 | |
108 if ((manifest.name.is_null() || manifest.name.string().empty()) && | |
109 (manifest.short_name.is_null() || manifest.short_name.string().empty())) { | |
110 installable_->error = MANIFEST_MISSING_NAME_OR_SHORT_NAME; | |
111 return false; | |
112 } | |
113 | |
114 // TODO(dominickn,mlamouri): when Chrome supports "minimal-ui", it should be | |
115 // accepted. If we accept it today, it would fallback to "browser" and make | |
116 // this check moot. See https://crbug.com/604390. | |
117 if (manifest.display != blink::WebDisplayModeStandalone && | |
118 manifest.display != blink::WebDisplayModeFullscreen) { | |
119 installable_->error = MANIFEST_DISPLAY_NOT_SUPPORTED; | |
120 return false; | |
121 } | |
122 | |
123 if (!DoesManifestContainRequiredIcon(manifest)) { | |
124 installable_->error = MANIFEST_MISSING_SUITABLE_ICON; | |
125 return false; | |
126 } | |
127 | |
128 return true; | |
129 } | |
130 | |
131 void InstallableManager::GetData(const InstallableParams& params, | |
132 const InstallableCallback& callback) { | |
133 DCHECK_CURRENTLY_ON(content::BrowserThread::UI); | |
134 | |
135 // Return immediately if we've already working on a task. The new task will be | |
136 // looked at once the current task is finished. | |
137 tasks_.push_back({params, callback}); | |
138 if (is_active_) | |
139 return; | |
140 | |
141 is_active_ = true; | |
142 StartNextTask(); | |
143 } | |
144 | |
145 bool InstallableManager::IsIconFetched(const InstallableParams& params) const { | |
146 const auto it = icons_.find( | |
147 {params.ideal_icon_size_in_dp, params.minimum_icon_size_in_dp}); | |
148 return it != icons_.end() && it->second.fetched; | |
149 } | |
150 | |
151 void InstallableManager::SetIconFetched(const InstallableParams& params) { | |
152 GetIcon(params).fetched = true; | |
153 } | |
154 | |
155 InstallableManager::IconProperty& InstallableManager::GetIcon( | |
156 const InstallableParams& params) { | |
157 return icons_[{params.ideal_icon_size_in_dp, params.minimum_icon_size_in_dp}]; | |
158 } | |
159 | |
160 InstallableErrorCode InstallableManager::GetErrorCode( | |
161 const InstallableParams& params) { | |
162 if (manifest_->error != NO_ERROR_DETECTED) | |
163 return manifest_->error; | |
164 | |
165 if (params.check_installable && installable_->error != NO_ERROR_DETECTED) | |
166 return installable_->error; | |
167 | |
168 if (params.fetch_valid_icon) { | |
169 IconProperty& icon = GetIcon(params); | |
170 if (icon.error != NO_ERROR_DETECTED) | |
171 return icon.error; | |
172 } | |
173 | |
174 return NO_ERROR_DETECTED; | |
175 } | |
176 | |
177 InstallableErrorCode InstallableManager::manifest_error() const { | |
178 return manifest_->error; | |
179 } | |
180 | |
181 InstallableErrorCode InstallableManager::installable_error() const { | |
182 return installable_->error; | |
183 } | |
184 | |
185 void InstallableManager::set_installable_error( | |
186 InstallableErrorCode error_code) { | |
187 installable_->error = error_code; | |
188 } | |
189 | |
190 InstallableErrorCode InstallableManager::icon_error( | |
191 const InstallableManager::IconParams& icon_params) { | |
192 return icons_[icon_params].error; | |
193 } | |
194 | |
195 GURL& InstallableManager::icon_url( | |
196 const InstallableManager::IconParams& icon_params) { | |
197 return icons_[icon_params].url; | |
198 } | |
199 | |
200 const SkBitmap* InstallableManager::icon( | |
201 const InstallableManager::IconParams& icon_params) { | |
202 return icons_[icon_params].icon.get(); | |
203 } | |
204 | |
205 content::WebContents* InstallableManager::GetWebContents() { | |
206 content::WebContents* contents = web_contents(); | |
207 if (!contents || contents->IsBeingDestroyed()) | |
208 return nullptr; | |
209 return contents; | |
210 } | |
211 | |
212 bool InstallableManager::IsComplete(const InstallableParams& params) const { | |
213 // Returns true if for all resources: | |
214 // a. the params did not request it, OR | |
215 // b. the resource has been fetched/checked. | |
216 return manifest_->fetched && | |
217 (!params.check_installable || installable_->fetched) && | |
218 (!params.fetch_valid_icon || IsIconFetched(params)); | |
219 } | |
220 | |
221 void InstallableManager::Reset() { | |
222 // Prevent any outstanding callbacks to or from this object from being called. | |
223 weak_factory_.InvalidateWeakPtrs(); | |
224 tasks_.clear(); | |
225 icons_.clear(); | |
226 | |
227 manifest_.reset(new ManifestProperty()); | |
228 installable_.reset(new InstallableProperty()); | |
229 | |
230 is_active_ = false; | |
231 } | |
232 | |
233 void InstallableManager::SetManifestDependentTasksComplete() { | |
234 DCHECK(!tasks_.empty()); | |
235 const InstallableParams& params = tasks_[0].first; | |
236 | |
237 installable_->fetched = true; | |
238 SetIconFetched(params); | |
239 } | |
240 | |
241 void InstallableManager::StartNextTask() { | |
242 // If there's nothing to do, exit. Resources remain cached so any future calls | |
243 // won't re-fetch anything that has already been retrieved. | |
244 if (tasks_.empty()) { | |
245 is_active_ = false; | |
246 return; | |
247 } | |
248 | |
249 DCHECK(is_active_); | |
250 WorkOnTask(); | |
251 } | |
252 | |
253 void InstallableManager::RunCallback(const Task& task, | |
254 InstallableErrorCode code) { | |
255 const InstallableParams& params = task.first; | |
256 IconProperty& icon = GetIcon(params); | |
257 InstallableData data = { | |
258 code, | |
259 manifest_url(), | |
260 manifest(), | |
261 params.fetch_valid_icon ? icon.url : GURL::EmptyGURL(), | |
262 params.fetch_valid_icon ? icon.icon.get() : nullptr, | |
263 params.check_installable ? is_installable() : false}; | |
264 | |
265 task.second.Run(data); | |
266 } | |
267 | |
268 void InstallableManager::WorkOnTask() { | |
269 DCHECK(!tasks_.empty()); | |
270 const Task& task = tasks_[0]; | |
271 const InstallableParams& params = task.first; | |
272 | |
273 InstallableErrorCode code = GetErrorCode(params); | |
274 if (code != NO_ERROR_DETECTED || IsComplete(params)) { | |
275 RunCallback(task, code); | |
276 tasks_.erase(tasks_.begin()); | |
277 StartNextTask(); | |
278 return; | |
279 } | |
280 | |
281 if (!manifest_->fetched) | |
282 FetchManifest(); | |
283 else if (params.check_installable && !installable_->fetched) | |
284 CheckInstallable(); | |
285 else if (params.fetch_valid_icon && !IsIconFetched(params)) | |
286 CheckAndFetchBestIcon(); | |
287 else | |
288 NOTREACHED(); | |
289 } | |
290 | |
291 void InstallableManager::FetchManifest() { | |
292 DCHECK(!manifest_->fetched); | |
293 | |
294 content::WebContents* web_contents = GetWebContents(); | |
295 DCHECK(web_contents); | |
296 | |
297 web_contents->GetManifest(base::Bind(&InstallableManager::OnDidGetManifest, | |
298 weak_factory_.GetWeakPtr())); | |
299 } | |
300 | |
301 void InstallableManager::OnDidGetManifest(const GURL& manifest_url, | |
302 const content::Manifest& manifest) { | |
303 if (!GetWebContents()) { | |
304 return; | |
305 } else if (manifest_url.is_empty()) { | |
Lei Zhang
2016/08/03 06:49:51
no need for else after a return.
dominickn
2016/08/03 07:01:43
Done.
dominickn
2016/08/03 07:01:43
Done.
| |
306 manifest_->error = NO_MANIFEST; | |
307 SetManifestDependentTasksComplete(); | |
308 } else if (manifest.IsEmpty()) { | |
309 manifest_->error = MANIFEST_EMPTY; | |
310 SetManifestDependentTasksComplete(); | |
311 } | |
312 | |
313 manifest_->url = manifest_url; | |
314 manifest_->manifest = manifest; | |
315 manifest_->fetched = true; | |
316 WorkOnTask(); | |
317 } | |
318 | |
319 void InstallableManager::CheckInstallable() { | |
320 DCHECK(!installable_->fetched); | |
321 DCHECK(!manifest().IsEmpty()); | |
322 | |
323 if (IsManifestValidForWebApp(manifest())) { | |
324 CheckServiceWorker(); | |
325 } else { | |
326 installable_->installable = false; | |
327 installable_->fetched = true; | |
328 WorkOnTask(); | |
329 } | |
330 } | |
331 | |
332 void InstallableManager::CheckServiceWorker() { | |
333 DCHECK(!installable_->fetched); | |
334 DCHECK(!manifest().IsEmpty()); | |
335 DCHECK(manifest().start_url.is_valid()); | |
336 | |
337 content::WebContents* web_contents = GetWebContents(); | |
338 | |
339 // Check to see if there is a single service worker controlling this page | |
340 // and the manifest's start url. | |
341 content::StoragePartition* storage_partition = | |
342 content::BrowserContext::GetStoragePartition( | |
343 Profile::FromBrowserContext(web_contents->GetBrowserContext()), | |
344 web_contents->GetSiteInstance()); | |
345 DCHECK(storage_partition); | |
346 | |
347 storage_partition->GetServiceWorkerContext()->CheckHasServiceWorker( | |
348 web_contents->GetLastCommittedURL(), manifest().start_url, | |
349 base::Bind(&InstallableManager::OnDidCheckHasServiceWorker, | |
350 weak_factory_.GetWeakPtr())); | |
351 } | |
352 | |
353 void InstallableManager::OnDidCheckHasServiceWorker(bool has_service_worker) { | |
354 if (!GetWebContents()) | |
355 return; | |
356 | |
357 if (has_service_worker) { | |
358 installable_->installable = true; | |
359 } else { | |
360 installable_->installable = false; | |
361 installable_->error = NO_MATCHING_SERVICE_WORKER; | |
362 } | |
363 | |
364 installable_->fetched = true; | |
365 WorkOnTask(); | |
366 } | |
367 | |
368 void InstallableManager::CheckAndFetchBestIcon() { | |
369 DCHECK(!manifest().IsEmpty()); | |
370 DCHECK(!tasks_.empty()); | |
371 | |
372 const InstallableParams& params = tasks_[0].first; | |
373 IconProperty& icon = GetIcon(params); | |
374 icon.fetched = true; | |
375 | |
376 GURL icon_url = ManifestIconSelector::FindBestMatchingIcon( | |
377 manifest().icons, params.ideal_icon_size_in_dp, | |
378 params.minimum_icon_size_in_dp); | |
379 | |
380 if (icon_url.is_empty()) { | |
381 icon.error = NO_ACCEPTABLE_ICON; | |
382 } else { | |
383 bool can_download_icon = ManifestIconDownloader::Download( | |
384 GetWebContents(), icon_url, params.ideal_icon_size_in_dp, | |
385 params.minimum_icon_size_in_dp, | |
386 base::Bind(&InstallableManager::OnAppIconFetched, | |
387 weak_factory_.GetWeakPtr(), icon_url)); | |
388 if (can_download_icon) | |
389 return; | |
390 icon.error = CANNOT_DOWNLOAD_ICON; | |
391 } | |
392 | |
393 WorkOnTask(); | |
394 } | |
395 | |
396 void InstallableManager::OnAppIconFetched(const GURL icon_url, | |
397 const SkBitmap& bitmap) { | |
398 DCHECK(!tasks_.empty()); | |
399 const InstallableParams& params = tasks_[0].first; | |
400 IconProperty& icon = GetIcon(params); | |
401 | |
402 if (!GetWebContents()) { | |
403 return; | |
404 } else if (bitmap.drawsNothing()) { | |
Lei Zhang
2016/08/03 06:49:51
Ditto
dominickn
2016/08/03 07:01:43
Done.
| |
405 icon.error = NO_ICON_AVAILABLE; | |
406 } else { | |
407 icon.url = icon_url; | |
408 icon.icon.reset(new SkBitmap(bitmap)); | |
409 } | |
410 | |
411 WorkOnTask(); | |
412 } | |
413 | |
414 void InstallableManager::DidFinishNavigation( | |
415 content::NavigationHandle* handle) { | |
416 if (handle->IsInMainFrame() && handle->HasCommitted() && | |
417 !handle->IsSamePage()) { | |
418 Reset(); | |
419 } | |
420 } | |
421 | |
422 void InstallableManager::WebContentsDestroyed() { | |
423 Reset(); | |
424 Observe(nullptr); | |
425 } | |
426 | |
427 const GURL& InstallableManager::manifest_url() const { | |
428 return manifest_->url; | |
429 } | |
430 | |
431 const content::Manifest& InstallableManager::manifest() const { | |
432 return manifest_->manifest; | |
433 } | |
434 | |
435 bool InstallableManager::is_installable() const { | |
436 return installable_->installable; | |
437 } | |
438 | |
439 // static | |
440 int InstallableManager::GetMinimumIconSizeInPx() { | |
441 return kIconMinimumSizeInPx; | |
442 } | |
OLD | NEW |