Chromium Code Reviews| 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 InstallableManager::ManifestProperty::~ManifestProperty() = default; | |
|
benwells
2016/08/03 01:17:28
Nit: is this needed?
dominickn
2016/08/03 06:38:04
It was at this time (complex structs must have a d
| |
| 58 | |
| 59 InstallableManager::IconProperty::~IconProperty() = default; | |
| 60 | |
| 61 InstallableManager::IconProperty::IconProperty() : | |
| 62 error(NO_ERROR_DETECTED), url(), icon(), fetched(false) { } | |
| 63 | |
| 64 InstallableManager::IconProperty::IconProperty( | |
| 65 InstallableManager::IconProperty&& other) = default; | |
| 66 | |
| 67 InstallableManager::IconProperty& InstallableManager::IconProperty::operator=( | |
| 68 IconProperty&& other) = default; | |
| 69 | |
| 70 InstallableManager::InstallableManager(content::WebContents* web_contents) | |
| 71 : content::WebContentsObserver(web_contents), | |
| 72 manifest_(new ManifestProperty()), | |
| 73 installable_(new InstallableProperty()), | |
| 74 is_active_(false), | |
| 75 weak_factory_(this) { } | |
| 76 | |
| 77 InstallableManager::~InstallableManager() = default; | |
| 78 | |
| 79 bool InstallableManager::IsManifestValidForWebApp( | |
| 80 const content::Manifest& manifest) { | |
| 81 if (manifest.IsEmpty()) { | |
| 82 installable_->error = MANIFEST_EMPTY; | |
| 83 return false; | |
| 84 } | |
| 85 | |
| 86 if (!manifest.start_url.is_valid()) { | |
| 87 installable_->error = START_URL_NOT_VALID; | |
| 88 return false; | |
| 89 } | |
| 90 | |
| 91 if ((manifest.name.is_null() || manifest.name.string().empty()) && | |
| 92 (manifest.short_name.is_null() || manifest.short_name.string().empty())) { | |
| 93 installable_->error = MANIFEST_MISSING_NAME_OR_SHORT_NAME; | |
| 94 return false; | |
| 95 } | |
| 96 | |
| 97 // TODO(dominickn,mlamouri): when Chrome supports "minimal-ui", it should be | |
| 98 // accepted. If we accept it today, it would fallback to "browser" and make | |
| 99 // this check moot. See https://crbug.com/604390. | |
| 100 if (manifest.display != blink::WebDisplayModeStandalone && | |
| 101 manifest.display != blink::WebDisplayModeFullscreen) { | |
| 102 installable_->error = MANIFEST_DISPLAY_NOT_SUPPORTED; | |
| 103 return false; | |
| 104 } | |
| 105 | |
| 106 if (!DoesManifestContainRequiredIcon(manifest)) { | |
| 107 installable_->error = MANIFEST_MISSING_SUITABLE_ICON; | |
| 108 return false; | |
| 109 } | |
| 110 | |
| 111 return true; | |
| 112 } | |
| 113 | |
| 114 void InstallableManager::GetData(const InstallableParams& params, | |
| 115 const InstallableCallback& callback) { | |
| 116 DCHECK_CURRENTLY_ON(content::BrowserThread::UI); | |
| 117 | |
| 118 // Return immediately if we've already working on a task. The new task will be | |
| 119 // looked at once the current task is finished. | |
| 120 tasks_.push_back({params, callback}); | |
| 121 if (is_active_) | |
| 122 return; | |
| 123 | |
| 124 is_active_ = true; | |
| 125 StartNextTask(); | |
| 126 } | |
| 127 | |
| 128 bool InstallableManager::IsIconFetched(const InstallableParams& params) const { | |
| 129 const auto it = icons_.find( | |
| 130 {params.ideal_icon_size_in_dp, params.minimum_icon_size_in_dp}); | |
| 131 return it != icons_.end() && it->second.fetched; | |
| 132 } | |
| 133 | |
| 134 void InstallableManager::SetIconFetched(const InstallableParams& params) { | |
| 135 GetIcon(params).fetched = true; | |
| 136 } | |
| 137 | |
| 138 InstallableManager::IconProperty& InstallableManager::GetIcon( | |
| 139 const InstallableParams& params) { | |
| 140 return icons_[{params.ideal_icon_size_in_dp, params.minimum_icon_size_in_dp}]; | |
| 141 } | |
| 142 | |
| 143 InstallableErrorCode InstallableManager::GetErrorCode( | |
| 144 const InstallableParams& params) { | |
| 145 if (manifest_->error != NO_ERROR_DETECTED) | |
| 146 return manifest_->error; | |
| 147 | |
| 148 if (params.check_installable && installable_->error != NO_ERROR_DETECTED) | |
| 149 return installable_->error; | |
| 150 | |
| 151 if (params.fetch_valid_icon) { | |
| 152 IconProperty& icon = GetIcon(params); | |
| 153 if (icon.error != NO_ERROR_DETECTED) | |
| 154 return icon.error; | |
| 155 } | |
| 156 | |
| 157 return NO_ERROR_DETECTED; | |
| 158 } | |
| 159 | |
| 160 content::WebContents* InstallableManager::GetWebContents() { | |
| 161 content::WebContents* contents = web_contents(); | |
| 162 if (!contents || contents->IsBeingDestroyed()) | |
| 163 return nullptr; | |
| 164 return contents; | |
| 165 } | |
| 166 | |
| 167 bool InstallableManager::IsComplete(const InstallableParams& params) const { | |
| 168 // Returns true if for all resources: | |
| 169 // a. the params did not request it, OR | |
| 170 // b. the resource has been fetched/checked. | |
| 171 return manifest_->fetched && | |
| 172 (!params.check_installable || installable_->fetched) && | |
| 173 (!params.fetch_valid_icon || IsIconFetched(params)); | |
| 174 } | |
| 175 | |
| 176 void InstallableManager::Reset() { | |
| 177 // Prevent any outstanding callbacks to or from this object from being called. | |
| 178 weak_factory_.InvalidateWeakPtrs(); | |
| 179 tasks_.clear(); | |
| 180 icons_.clear(); | |
| 181 | |
| 182 manifest_.reset(new ManifestProperty()); | |
| 183 installable_.reset(new InstallableProperty()); | |
| 184 | |
| 185 is_active_ = false; | |
| 186 } | |
| 187 | |
| 188 void InstallableManager::SetManifestDependentTasksComplete() { | |
| 189 DCHECK(!tasks_.empty()); | |
| 190 const InstallableParams& params = tasks_[0].first; | |
| 191 | |
| 192 installable_->fetched = true; | |
| 193 SetIconFetched(params); | |
| 194 } | |
| 195 | |
| 196 void InstallableManager::StartNextTask() { | |
| 197 // If there's nothing to do, exit. Resources remain cached so any future calls | |
| 198 // won't re-fetch anything that has already been retrieved. | |
| 199 if (tasks_.empty()) { | |
| 200 is_active_ = false; | |
| 201 return; | |
| 202 } | |
| 203 | |
| 204 DCHECK(is_active_); | |
| 205 WorkOnTask(); | |
| 206 } | |
| 207 | |
| 208 void InstallableManager::RunCallback(const Task& task, | |
| 209 InstallableErrorCode code) { | |
| 210 const InstallableParams& params = task.first; | |
| 211 IconProperty& icon = GetIcon(params); | |
| 212 InstallableData data = { | |
| 213 code, | |
| 214 manifest_url(), | |
| 215 manifest(), | |
| 216 params.fetch_valid_icon ? icon.url : GURL::EmptyGURL(), | |
| 217 params.fetch_valid_icon ? icon.icon.get() : nullptr, | |
| 218 params.check_installable ? is_installable() : false}; | |
| 219 | |
| 220 task.second.Run(data); | |
| 221 } | |
| 222 | |
| 223 void InstallableManager::WorkOnTask() { | |
| 224 DCHECK(!tasks_.empty()); | |
| 225 const Task& task = tasks_[0]; | |
| 226 const InstallableParams& params = task.first; | |
| 227 | |
| 228 InstallableErrorCode code = GetErrorCode(params); | |
| 229 if (code != NO_ERROR_DETECTED || IsComplete(params)) { | |
| 230 RunCallback(task, code); | |
| 231 tasks_.erase(tasks_.begin()); | |
| 232 StartNextTask(); | |
| 233 return; | |
| 234 } | |
| 235 | |
| 236 if (!manifest_->fetched) | |
| 237 FetchManifest(); | |
| 238 else if (params.check_installable && !installable_->fetched) | |
| 239 CheckInstallable(); | |
| 240 else if (params.fetch_valid_icon && !IsIconFetched(params)) | |
| 241 CheckAndFetchBestIcon(); | |
| 242 else | |
| 243 NOTREACHED(); | |
| 244 } | |
| 245 | |
| 246 void InstallableManager::FetchManifest() { | |
| 247 DCHECK(!manifest_->fetched); | |
| 248 | |
| 249 content::WebContents* web_contents = GetWebContents(); | |
| 250 DCHECK(web_contents); | |
| 251 | |
| 252 web_contents->GetManifest(base::Bind(&InstallableManager::OnDidGetManifest, | |
| 253 weak_factory_.GetWeakPtr())); | |
| 254 } | |
| 255 | |
| 256 void InstallableManager::OnDidGetManifest(const GURL& manifest_url, | |
| 257 const content::Manifest& manifest) { | |
| 258 if (!GetWebContents()) { | |
| 259 return; | |
| 260 } else if (manifest_url.is_empty()) { | |
| 261 manifest_->error = NO_MANIFEST; | |
| 262 SetManifestDependentTasksComplete(); | |
| 263 } else if (manifest.IsEmpty()) { | |
| 264 manifest_->error = MANIFEST_EMPTY; | |
| 265 SetManifestDependentTasksComplete(); | |
| 266 } | |
| 267 | |
| 268 manifest_->url = manifest_url; | |
| 269 manifest_->manifest = manifest; | |
| 270 manifest_->fetched = true; | |
| 271 WorkOnTask(); | |
| 272 } | |
| 273 | |
| 274 void InstallableManager::CheckInstallable() { | |
| 275 DCHECK(!installable_->fetched); | |
| 276 DCHECK(!manifest().IsEmpty()); | |
| 277 | |
| 278 if (IsManifestValidForWebApp(manifest())) { | |
| 279 CheckServiceWorker(); | |
| 280 } else { | |
| 281 installable_->installable = false; | |
| 282 installable_->fetched = true; | |
| 283 WorkOnTask(); | |
| 284 } | |
| 285 } | |
| 286 | |
| 287 void InstallableManager::CheckServiceWorker() { | |
| 288 DCHECK(!installable_->fetched); | |
| 289 DCHECK(!manifest().IsEmpty()); | |
| 290 DCHECK(manifest().start_url.is_valid()); | |
| 291 | |
| 292 content::WebContents* web_contents = GetWebContents(); | |
| 293 | |
| 294 // Check to see if there is a single service worker controlling this page | |
| 295 // and the manifest's start url. | |
| 296 content::StoragePartition* storage_partition = | |
| 297 content::BrowserContext::GetStoragePartition( | |
| 298 Profile::FromBrowserContext(web_contents->GetBrowserContext()), | |
| 299 web_contents->GetSiteInstance()); | |
| 300 DCHECK(storage_partition); | |
| 301 | |
| 302 storage_partition->GetServiceWorkerContext()->CheckHasServiceWorker( | |
| 303 web_contents->GetLastCommittedURL(), manifest().start_url, | |
| 304 base::Bind(&InstallableManager::OnDidCheckHasServiceWorker, | |
| 305 weak_factory_.GetWeakPtr())); | |
| 306 } | |
| 307 | |
| 308 void InstallableManager::OnDidCheckHasServiceWorker(bool has_service_worker) { | |
| 309 if (!GetWebContents()) | |
| 310 return; | |
| 311 | |
| 312 if (has_service_worker) { | |
| 313 installable_->installable = true; | |
| 314 } else { | |
| 315 installable_->installable = false; | |
| 316 installable_->error = NO_MATCHING_SERVICE_WORKER; | |
| 317 } | |
| 318 | |
| 319 installable_->fetched = true; | |
| 320 WorkOnTask(); | |
| 321 } | |
| 322 | |
| 323 void InstallableManager::CheckAndFetchBestIcon() { | |
| 324 DCHECK(!manifest().IsEmpty()); | |
| 325 DCHECK(!tasks_.empty()); | |
| 326 | |
| 327 const InstallableParams& params = tasks_[0].first; | |
| 328 IconProperty& icon = GetIcon(params); | |
| 329 icon.fetched = true; | |
| 330 | |
| 331 GURL icon_url = ManifestIconSelector::FindBestMatchingIcon( | |
| 332 manifest().icons, params.ideal_icon_size_in_dp, | |
| 333 params.minimum_icon_size_in_dp); | |
| 334 | |
| 335 if (icon_url.is_empty()) { | |
| 336 icon.error = NO_ACCEPTABLE_ICON; | |
| 337 } else { | |
| 338 bool can_download_icon = ManifestIconDownloader::Download( | |
| 339 GetWebContents(), icon_url, params.ideal_icon_size_in_dp, | |
| 340 params.minimum_icon_size_in_dp, | |
| 341 base::Bind(&InstallableManager::OnAppIconFetched, | |
| 342 weak_factory_.GetWeakPtr(), icon_url)); | |
| 343 if (can_download_icon) | |
| 344 return; | |
| 345 icon.error = CANNOT_DOWNLOAD_ICON; | |
| 346 } | |
| 347 | |
| 348 WorkOnTask(); | |
| 349 } | |
| 350 | |
| 351 void InstallableManager::OnAppIconFetched(const GURL icon_url, | |
| 352 const SkBitmap& bitmap) { | |
| 353 DCHECK(!tasks_.empty()); | |
| 354 const InstallableParams& params = tasks_[0].first; | |
| 355 IconProperty& icon = GetIcon(params); | |
| 356 | |
| 357 if (!GetWebContents()) { | |
| 358 return; | |
| 359 } else if (bitmap.drawsNothing()) { | |
| 360 icon.error = NO_ICON_AVAILABLE; | |
| 361 } else { | |
| 362 icon.url = icon_url; | |
| 363 icon.icon.reset(new SkBitmap(bitmap)); | |
| 364 } | |
| 365 | |
| 366 WorkOnTask(); | |
| 367 } | |
| 368 | |
| 369 void InstallableManager::DidFinishNavigation( | |
| 370 content::NavigationHandle* handle) { | |
| 371 if (handle->IsInMainFrame() && handle->HasCommitted() && | |
| 372 !handle->IsSamePage()) { | |
| 373 Reset(); | |
| 374 } | |
| 375 } | |
| 376 | |
| 377 void InstallableManager::WebContentsDestroyed() { | |
| 378 Reset(); | |
| 379 Observe(nullptr); | |
| 380 } | |
| 381 | |
| 382 const GURL& InstallableManager::manifest_url() const { | |
| 383 return manifest_->url; | |
| 384 } | |
| 385 | |
| 386 const content::Manifest& InstallableManager::manifest() const { | |
| 387 return manifest_->manifest; | |
| 388 } | |
| 389 | |
| 390 bool InstallableManager::is_installable() const { | |
| 391 return installable_->installable; | |
| 392 } | |
| 393 | |
| 394 // static | |
| 395 int InstallableManager::GetMinimumIconSizeInPx() { | |
| 396 return kIconMinimumSizeInPx; | |
| 397 } | |
| OLD | NEW |