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

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

Issue 2160513002: Extract AppBannerDataFetcher into an InstallableManager. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: s/checker/manager; collapse valid manifest + SW into one Created 4 years, 4 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_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 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698