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 <utility> | |
8 | |
9 #include "base/bind.h" | |
10 #include "base/strings/string_util.h" | |
11 #include "chrome/browser/installable/installable_logging.h" | |
12 #include "chrome/browser/installable/installable_property.h" | |
13 #include "chrome/browser/manifest/manifest_icon_downloader.h" | |
14 #include "chrome/browser/manifest/manifest_icon_selector.h" | |
15 #include "chrome/browser/profiles/profile.h" | |
16 #include "content/public/browser/browser_context.h" | |
17 #include "content/public/browser/browser_thread.h" | |
18 #include "content/public/browser/navigation_handle.h" | |
19 #include "content/public/browser/service_worker_context.h" | |
20 #include "content/public/browser/storage_partition.h" | |
21 #include "third_party/WebKit/public/platform/WebDisplayMode.h" | |
22 | |
23 namespace { | |
24 | |
25 const char kPngExtension[] = ".png"; | |
26 | |
27 // This constant is the icon size on Android (48dp) multiplied by the scale | |
28 // factor of a Nexus 5 device (3x). For mobile and desktop platforms, a 144px | |
29 // icon is an approximate, appropriate lower bound. | |
30 // TODO(dominickn): consolidate with minimum_icon_size_in_dp across platforms. | |
31 const int kIconMinimumSizeInPx = 144; | |
32 | |
33 // Returns true if |manifest| specifies a PNG icon >= 144x144px (or 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(InstallableManager); | |
60 | |
61 InstallableManager::InstallableManager(content::WebContents* web_contents) | |
62 : content::WebContentsObserver(web_contents), | |
63 is_active_(false), | |
64 weak_factory_(this) { } | |
65 | |
66 InstallableManager::~InstallableManager() = default; | |
67 | |
68 bool InstallableManager::IsManifestValidForWebApp( | |
69 const content::Manifest& manifest) { | |
70 if (manifest.IsEmpty()) { | |
71 installable_prop_.error = MANIFEST_EMPTY; | |
72 return false; | |
73 } | |
74 | |
75 if (!manifest.start_url.is_valid()) { | |
76 installable_prop_.error = START_URL_NOT_VALID; | |
77 return false; | |
78 } | |
79 | |
80 if ((manifest.name.is_null() || manifest.name.string().empty()) && | |
81 (manifest.short_name.is_null() || manifest.short_name.string().empty())) { | |
82 installable_prop_.error = MANIFEST_MISSING_NAME_OR_SHORT_NAME; | |
83 return false; | |
84 } | |
85 | |
86 // TODO(dominickn,mlamouri): when Chrome supports "minimal-ui", it should be | |
87 // accepted. If we accept it today, it would fallback to "browser" and make | |
88 // this check moot. See https://crbug.com/604390. | |
89 if (manifest.display != blink::WebDisplayModeStandalone && | |
90 manifest.display != blink::WebDisplayModeFullscreen) { | |
91 installable_prop_.error = MANIFEST_DISPLAY_NOT_SUPPORTED; | |
92 return false; | |
93 } | |
94 | |
95 if (!DoesManifestContainRequiredIcon(manifest)) { | |
96 installable_prop_.error = MANIFEST_MISSING_SUITABLE_ICON; | |
97 return false; | |
98 } | |
99 | |
100 return true; | |
101 } | |
102 | |
103 void InstallableManager::GetData(const InstallableParams& params, | |
104 const InstallableCallback& callback) { | |
105 DCHECK_CURRENTLY_ON(content::BrowserThread::UI); | |
106 | |
107 // If we've already working on a task, or running a callback, return straight | |
108 // away. The new task will be dealt with once the current work completes. | |
109 tasks_.push_back({params, callback}); | |
110 if (is_active_) | |
111 return; | |
112 | |
113 is_active_ = true; | |
114 StartNextTask(); | |
115 } | |
116 | |
117 bool InstallableManager::IsIconFetched(const InstallableParams& params) const { | |
118 const auto it = icons_.find( | |
119 {params.ideal_icon_size_in_dp, params.minimum_icon_size_in_dp}); | |
120 return it != icons_.end() && it->second.fetched; | |
121 } | |
122 | |
123 void InstallableManager::SetIconFetched(const InstallableParams& params) { | |
124 GetIcon(params).fetched = true; | |
125 } | |
126 | |
127 IconProperty& InstallableManager::GetIcon(const InstallableParams& params) { | |
128 return icons_[{params.ideal_icon_size_in_dp, params.minimum_icon_size_in_dp}]; | |
129 } | |
130 | |
131 InstallableErrorCode InstallableManager::GetErrorCode( | |
132 const InstallableParams& params) { | |
133 if (manifest_prop_.error != NO_ERROR_DETECTED) | |
134 return manifest_prop_.error; | |
135 if (params.check_installable && | |
136 installable_prop_.error != NO_ERROR_DETECTED) { | |
137 return installable_prop_.error; | |
138 } | |
139 if (params.fetch_valid_icon) { | |
140 IconProperty& icon = GetIcon(params); | |
141 if (icon.error != NO_ERROR_DETECTED) | |
142 return icon.error; | |
143 } | |
144 | |
145 return NO_ERROR_DETECTED; | |
146 } | |
147 | |
148 content::WebContents* InstallableManager::GetWebContents() { | |
149 content::WebContents* contents = web_contents(); | |
150 if (!contents || contents->IsBeingDestroyed()) | |
151 return nullptr; | |
152 return contents; | |
153 } | |
154 | |
155 bool InstallableManager::IsComplete(const InstallableParams& params) const { | |
156 // Returns true if for all resources: | |
157 // a. the params did not request it, OR | |
158 // b. the resource has been fetched/checked. | |
159 return manifest_prop_.fetched && | |
160 (!params.check_installable || installable_prop_.fetched) && | |
161 (!params.fetch_valid_icon || IsIconFetched(params)); | |
162 } | |
163 | |
164 bool InstallableManager::IsContentsValid( | |
165 content::WebContents* web_contents) const { | |
166 if (!web_contents || !is_active_) | |
benwells
2016/07/29 04:36:54
Why are the contents not valid if the manager isn'
dominickn
2016/07/31 23:32:04
Deleted the method.
| |
167 return false; | |
168 | |
169 return true; | |
170 } | |
171 | |
172 void InstallableManager::Reset() { | |
173 // Prevent any outstanding callbacks to or from this object from being called. | |
174 weak_factory_.InvalidateWeakPtrs(); | |
175 tasks_.clear(); | |
176 icons_.clear(); | |
177 | |
178 manifest_prop_.Reset(); | |
179 installable_prop_.Reset(); | |
180 | |
181 is_active_ = false; | |
182 } | |
183 | |
184 void InstallableManager::SetManifestDependentTasksComplete() { | |
185 DCHECK(!tasks_.empty()); | |
186 const InstallableParams& params = tasks_[0].first; | |
187 | |
188 installable_prop_.fetched = true; | |
189 SetIconFetched(params); | |
190 } | |
191 | |
192 void InstallableManager::StartNextTask() { | |
193 // If there's nothing to do, exit. Resources remain cached so any future calls | |
194 // won't re-fetch anything that has already been retrieved. | |
195 if (tasks_.empty()) { | |
196 is_active_ = false; | |
197 return; | |
198 } | |
199 | |
200 is_active_ = true; | |
benwells
2016/07/29 04:36:53
Nit: I think you can delete this line, maybe add D
dominickn
2016/07/31 23:32:04
Done.
| |
201 WorkOnTask(); | |
202 } | |
203 | |
204 void InstallableManager::RunCallback(const Task& task, | |
205 InstallableErrorCode code) { | |
206 const InstallableParams& params = task.first; | |
207 IconProperty& icon = GetIcon(params); | |
208 InstallableData data = { | |
209 code, | |
210 manifest_url(), | |
211 manifest(), | |
212 params.fetch_valid_icon ? icon.url : GURL::EmptyGURL(), | |
213 params.fetch_valid_icon ? icon.icon.get() : nullptr, | |
214 params.check_installable ? is_installable() : false}; | |
215 | |
216 task.second.Run(data); | |
217 } | |
218 | |
219 void InstallableManager::WorkOnTask() { | |
220 DCHECK(!tasks_.empty()); | |
221 const Task& task = tasks_[0]; | |
222 const InstallableParams& params = task.first; | |
223 | |
224 InstallableErrorCode code = GetErrorCode(params); | |
225 if (code != NO_ERROR_DETECTED || IsComplete(params)) { | |
226 RunCallback(task, code); | |
227 tasks_.erase(tasks_.begin()); | |
228 StartNextTask(); | |
229 return; | |
230 } | |
231 | |
232 if (!manifest_prop_.fetched) { | |
233 FetchManifest(); | |
234 } else if (params.check_installable && !installable_prop_.fetched) { | |
235 CheckInstallable(); | |
236 } else if (params.fetch_valid_icon && !IsIconFetched(params)) { | |
237 CheckAndFetchBestIcon(); | |
238 } else { | |
239 NOTREACHED(); | |
240 } | |
241 } | |
242 | |
243 void InstallableManager::FetchManifest() { | |
244 DCHECK(!manifest_prop_.fetched); | |
245 | |
246 content::WebContents* web_contents = GetWebContents(); | |
247 DCHECK(web_contents); | |
248 | |
249 web_contents->GetManifest(base::Bind(&InstallableManager::OnDidGetManifest, | |
250 weak_factory_.GetWeakPtr())); | |
251 } | |
252 | |
253 void InstallableManager::OnDidGetManifest(const GURL& manifest_url, | |
254 const content::Manifest& manifest) { | |
255 if (!IsContentsValid(GetWebContents())) { | |
256 Reset(); | |
benwells
2016/07/29 04:36:54
Should you reset here, or just return?
dominickn
2016/07/31 23:32:04
I'm not really sure about the guarantees of WebCon
| |
257 } else if (manifest_url.is_empty()) { | |
258 manifest_prop_.error = NO_MANIFEST; | |
259 SetManifestDependentTasksComplete(); | |
260 } else if (manifest.IsEmpty()) { | |
261 manifest_prop_.error = MANIFEST_EMPTY; | |
262 SetManifestDependentTasksComplete(); | |
263 } | |
264 | |
265 manifest_prop_.url = manifest_url; | |
266 manifest_prop_.manifest = manifest; | |
267 manifest_prop_.fetched = true; | |
268 WorkOnTask(); | |
269 } | |
270 | |
271 void InstallableManager::CheckInstallable() { | |
272 DCHECK(!installable_prop_.fetched); | |
273 DCHECK(!manifest().IsEmpty()); | |
274 | |
275 if (IsManifestValidForWebApp(manifest())) { | |
276 CheckServiceWorker(); | |
277 } else { | |
278 installable_prop_.value = false; | |
279 installable_prop_.fetched = true; | |
280 WorkOnTask(); | |
281 } | |
282 } | |
283 | |
284 void InstallableManager::CheckServiceWorker() { | |
285 DCHECK(!installable_prop_.fetched); | |
286 DCHECK(!manifest().IsEmpty()); | |
287 | |
288 if (manifest().start_url.is_valid()) { | |
289 content::WebContents* web_contents = GetWebContents(); | |
290 | |
291 // Check to see if there is a single service worker controlling this page | |
292 // and the manifest's start url. | |
293 Profile* profile = | |
294 Profile::FromBrowserContext(web_contents->GetBrowserContext()); | |
295 content::StoragePartition* storage_partition = | |
296 content::BrowserContext::GetStoragePartition( | |
297 profile, web_contents->GetSiteInstance()); | |
298 DCHECK(storage_partition); | |
299 | |
300 storage_partition->GetServiceWorkerContext()->CheckHasServiceWorker( | |
301 web_contents->GetLastCommittedURL(), manifest().start_url, | |
302 base::Bind(&InstallableManager::OnDidCheckHasServiceWorker, | |
303 weak_factory_.GetWeakPtr())); | |
304 } else { | |
305 installable_prop_.value = false; | |
benwells
2016/07/29 04:36:53
Is there an error to be set in this case? Oh, hang
dominickn
2016/07/31 23:32:04
Ah yes, now that this method is only called from t
| |
306 installable_prop_.fetched = true; | |
307 WorkOnTask(); | |
308 } | |
309 } | |
310 | |
311 void InstallableManager::OnDidCheckHasServiceWorker(bool has_service_worker) { | |
312 if (!IsContentsValid(GetWebContents())) | |
313 Reset(); | |
benwells
2016/07/29 04:36:54
Ditto about reset vs return.
dominickn
2016/07/31 23:32:04
Done.
| |
314 | |
315 if (has_service_worker) { | |
316 installable_prop_.value = true; | |
317 } else { | |
318 installable_prop_.value = false; | |
319 installable_prop_.error = NO_MATCHING_SERVICE_WORKER; | |
320 } | |
321 | |
322 installable_prop_.fetched = true; | |
323 WorkOnTask(); | |
324 } | |
325 | |
326 void InstallableManager::CheckAndFetchBestIcon() { | |
327 DCHECK(!manifest().IsEmpty()); | |
328 DCHECK(!tasks_.empty()); | |
329 | |
330 const InstallableParams& params = tasks_[0].first; | |
331 IconProperty& icon = GetIcon(params); | |
332 icon.fetched = true; | |
333 | |
334 GURL icon_url = ManifestIconSelector::FindBestMatchingIcon( | |
335 manifest().icons, params.ideal_icon_size_in_dp, | |
336 params.minimum_icon_size_in_dp); | |
337 | |
338 if (icon_url.is_empty()) { | |
339 icon.error = NO_ACCEPTABLE_ICON; | |
340 } else { | |
341 bool can_download_icon = ManifestIconDownloader::Download( | |
342 GetWebContents(), icon_url, params.ideal_icon_size_in_dp, | |
343 params.minimum_icon_size_in_dp, | |
344 base::Bind(&InstallableManager::OnAppIconFetched, | |
345 weak_factory_.GetWeakPtr(), icon_url)); | |
346 if (can_download_icon) | |
347 return; | |
348 icon.error = CANNOT_DOWNLOAD_ICON; | |
349 } | |
350 | |
351 WorkOnTask(); | |
352 } | |
353 | |
354 void InstallableManager::OnAppIconFetched(const GURL icon_url, | |
355 const SkBitmap& bitmap) { | |
356 DCHECK(!tasks_.empty()); | |
357 const InstallableParams& params = tasks_[0].first; | |
358 IconProperty& icon = GetIcon(params); | |
359 | |
360 if (!IsContentsValid(GetWebContents())) { | |
361 Reset(); | |
362 } else if (bitmap.drawsNothing()) { | |
363 icon.error = NO_ICON_AVAILABLE; | |
364 } else { | |
365 icon.url = icon_url; | |
366 icon.icon.reset(new SkBitmap(bitmap)); | |
367 } | |
368 | |
369 WorkOnTask(); | |
370 } | |
371 | |
372 void InstallableManager::DidFinishNavigation( | |
373 content::NavigationHandle* handle) { | |
374 if (handle->IsInMainFrame() && handle->HasCommitted() && | |
375 !handle->IsSamePage()) { | |
376 Reset(); | |
377 } | |
378 } | |
379 | |
380 void InstallableManager::WebContentsDestroyed() { | |
381 Reset(); | |
382 Observe(nullptr); | |
383 } | |
384 | |
385 const GURL& InstallableManager::manifest_url() const { | |
386 return manifest_prop_.url; | |
387 } | |
388 | |
389 const content::Manifest& InstallableManager::manifest() const { | |
390 return manifest_prop_.manifest; | |
391 } | |
392 | |
393 bool InstallableManager::is_installable() const { | |
394 return installable_prop_.value; | |
395 } | |
396 | |
397 // static | |
398 int InstallableManager::GetMinimumIconSizeInPx() { | |
399 return kIconMinimumSizeInPx; | |
400 } | |
OLD | NEW |