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 } // 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 |
| 306 if (manifest_url.is_empty()) { |
| 307 manifest_->error = NO_MANIFEST; |
| 308 SetManifestDependentTasksComplete(); |
| 309 } else if (manifest.IsEmpty()) { |
| 310 manifest_->error = MANIFEST_EMPTY; |
| 311 SetManifestDependentTasksComplete(); |
| 312 } |
| 313 |
| 314 manifest_->url = manifest_url; |
| 315 manifest_->manifest = manifest; |
| 316 manifest_->fetched = true; |
| 317 WorkOnTask(); |
| 318 } |
| 319 |
| 320 void InstallableManager::CheckInstallable() { |
| 321 DCHECK(!installable_->fetched); |
| 322 DCHECK(!manifest().IsEmpty()); |
| 323 |
| 324 if (IsManifestValidForWebApp(manifest())) { |
| 325 CheckServiceWorker(); |
| 326 } else { |
| 327 installable_->installable = false; |
| 328 installable_->fetched = true; |
| 329 WorkOnTask(); |
| 330 } |
| 331 } |
| 332 |
| 333 void InstallableManager::CheckServiceWorker() { |
| 334 DCHECK(!installable_->fetched); |
| 335 DCHECK(!manifest().IsEmpty()); |
| 336 DCHECK(manifest().start_url.is_valid()); |
| 337 |
| 338 content::WebContents* web_contents = GetWebContents(); |
| 339 |
| 340 // Check to see if there is a single service worker controlling this page |
| 341 // and the manifest's start url. |
| 342 content::StoragePartition* storage_partition = |
| 343 content::BrowserContext::GetStoragePartition( |
| 344 Profile::FromBrowserContext(web_contents->GetBrowserContext()), |
| 345 web_contents->GetSiteInstance()); |
| 346 DCHECK(storage_partition); |
| 347 |
| 348 storage_partition->GetServiceWorkerContext()->CheckHasServiceWorker( |
| 349 web_contents->GetLastCommittedURL(), manifest().start_url, |
| 350 base::Bind(&InstallableManager::OnDidCheckHasServiceWorker, |
| 351 weak_factory_.GetWeakPtr())); |
| 352 } |
| 353 |
| 354 void InstallableManager::OnDidCheckHasServiceWorker(bool has_service_worker) { |
| 355 if (!GetWebContents()) |
| 356 return; |
| 357 |
| 358 if (has_service_worker) { |
| 359 installable_->installable = true; |
| 360 } else { |
| 361 installable_->installable = false; |
| 362 installable_->error = NO_MATCHING_SERVICE_WORKER; |
| 363 } |
| 364 |
| 365 installable_->fetched = true; |
| 366 WorkOnTask(); |
| 367 } |
| 368 |
| 369 void InstallableManager::CheckAndFetchBestIcon() { |
| 370 DCHECK(!manifest().IsEmpty()); |
| 371 DCHECK(!tasks_.empty()); |
| 372 |
| 373 const InstallableParams& params = tasks_[0].first; |
| 374 IconProperty& icon = GetIcon(params); |
| 375 icon.fetched = true; |
| 376 |
| 377 GURL icon_url = ManifestIconSelector::FindBestMatchingIcon( |
| 378 manifest().icons, params.ideal_icon_size_in_dp, |
| 379 params.minimum_icon_size_in_dp); |
| 380 |
| 381 if (icon_url.is_empty()) { |
| 382 icon.error = NO_ACCEPTABLE_ICON; |
| 383 } else { |
| 384 bool can_download_icon = ManifestIconDownloader::Download( |
| 385 GetWebContents(), icon_url, params.ideal_icon_size_in_dp, |
| 386 params.minimum_icon_size_in_dp, |
| 387 base::Bind(&InstallableManager::OnAppIconFetched, |
| 388 weak_factory_.GetWeakPtr(), icon_url)); |
| 389 if (can_download_icon) |
| 390 return; |
| 391 icon.error = CANNOT_DOWNLOAD_ICON; |
| 392 } |
| 393 |
| 394 WorkOnTask(); |
| 395 } |
| 396 |
| 397 void InstallableManager::OnAppIconFetched(const GURL icon_url, |
| 398 const SkBitmap& bitmap) { |
| 399 DCHECK(!tasks_.empty()); |
| 400 const InstallableParams& params = tasks_[0].first; |
| 401 IconProperty& icon = GetIcon(params); |
| 402 |
| 403 if (!GetWebContents()) |
| 404 return; |
| 405 |
| 406 if (bitmap.drawsNothing()) { |
| 407 icon.error = NO_ICON_AVAILABLE; |
| 408 } else { |
| 409 icon.url = icon_url; |
| 410 icon.icon.reset(new SkBitmap(bitmap)); |
| 411 } |
| 412 |
| 413 WorkOnTask(); |
| 414 } |
| 415 |
| 416 void InstallableManager::DidFinishNavigation( |
| 417 content::NavigationHandle* handle) { |
| 418 if (handle->IsInMainFrame() && handle->HasCommitted() && |
| 419 !handle->IsSamePage()) { |
| 420 Reset(); |
| 421 } |
| 422 } |
| 423 |
| 424 void InstallableManager::WebContentsDestroyed() { |
| 425 Reset(); |
| 426 Observe(nullptr); |
| 427 } |
| 428 |
| 429 const GURL& InstallableManager::manifest_url() const { |
| 430 return manifest_->url; |
| 431 } |
| 432 |
| 433 const content::Manifest& InstallableManager::manifest() const { |
| 434 return manifest_->manifest; |
| 435 } |
| 436 |
| 437 bool InstallableManager::is_installable() const { |
| 438 return installable_->installable; |
| 439 } |
| 440 |
| 441 // static |
| 442 int InstallableManager::GetMinimumIconSizeInPx() { |
| 443 return kIconMinimumSizeInPx; |
| 444 } |
OLD | NEW |