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_checker.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 int kIconSizeUnset = -1; | |
| 22 | |
| 23 const char kPngExtension[] = ".png"; | |
| 24 | |
| 25 // TODO(dominickn): consolidate this constant with minimum_icon_size_in_dp | |
| 26 // across platforms. | |
| 27 const int kIconMinimumSizeInPx = 144; | |
|
gone
2016/07/20 22:21:35
Always weird to see this number in C++ land... Com
dominickn
2016/07/21 00:24:47
So this was set at 128 for desktop platforms, and
gone
2016/07/21 00:49:27
Acknowledged.
| |
| 28 | |
| 29 // Returns true if |manifest| specifies a PNG icon that is at least 144x144px | |
| 30 // (or has size "any"). | |
| 31 bool DoesManifestContainRequiredIcon(const content::Manifest& manifest) { | |
| 32 for (const auto& icon : manifest.icons) { | |
| 33 // The type field is optional. If it isn't present, fall back on checking | |
| 34 // the src extension, and allow the icon if the extension ends with png. | |
| 35 if (!base::EqualsASCII(icon.type.string(), "image/png") && | |
| 36 !(icon.type.is_null() && | |
| 37 base::EndsWith(icon.src.ExtractFileName(), kPngExtension, | |
| 38 base::CompareCase::INSENSITIVE_ASCII))) | |
| 39 continue; | |
| 40 | |
| 41 for (const auto& size : icon.sizes) { | |
| 42 if (size.IsEmpty()) // "any" | |
| 43 return true; | |
| 44 if (size.width() >= kIconMinimumSizeInPx && | |
| 45 size.height() >= kIconMinimumSizeInPx) { | |
| 46 return true; | |
| 47 } | |
| 48 } | |
| 49 } | |
| 50 | |
| 51 return false; | |
| 52 } | |
| 53 | |
| 54 } // anonymous namespace | |
| 55 | |
| 56 DEFINE_WEB_CONTENTS_USER_DATA_KEY(installable::InstallableChecker); | |
| 57 | |
| 58 namespace installable { | |
| 59 | |
| 60 InstallableParams::InstallableParams() | |
| 61 : ideal_icon_size_in_dp(kIconSizeUnset), | |
| 62 minimum_icon_size_in_dp(kIconSizeUnset), | |
| 63 has_valid_webapp_manifest(false), | |
| 64 has_service_worker(false), | |
| 65 has_valid_icon(false) { } | |
| 66 | |
| 67 InstallableChecker::InstallableChecker(content::WebContents* web_contents) | |
| 68 : content::WebContentsObserver(web_contents), | |
| 69 status_(DORMANT), | |
| 70 processing_error_(NoErrorDetected), | |
| 71 manifest_error_(NoErrorDetected), | |
| 72 valid_webapp_manifest_error_(NoErrorDetected), | |
| 73 service_worker_error_(NoErrorDetected), | |
| 74 icon_error_(NoErrorDetected), | |
| 75 ideal_icon_size_in_dp_(kIconSizeUnset), | |
| 76 minimum_icon_size_in_dp_(kIconSizeUnset), | |
| 77 has_valid_webapp_manifest_(false), | |
| 78 has_service_worker_(false), | |
| 79 weak_factory_(this) {} | |
| 80 | |
| 81 InstallableChecker::~InstallableChecker() { } | |
| 82 | |
| 83 bool InstallableChecker::IsManifestValidForWebApp( | |
| 84 const content::Manifest& manifest) { | |
| 85 if (manifest.IsEmpty()) { | |
| 86 valid_webapp_manifest_error_ = ManifestEmpty; | |
| 87 return false; | |
| 88 } | |
| 89 | |
| 90 if (!manifest.start_url.is_valid()) { | |
| 91 valid_webapp_manifest_error_ = StartUrlNotValid; | |
| 92 return false; | |
| 93 } | |
| 94 | |
| 95 if ((manifest.name.is_null() || manifest.name.string().empty()) && | |
| 96 (manifest.short_name.is_null() || manifest.short_name.string().empty())) { | |
| 97 valid_webapp_manifest_error_ = ManifestMissingNameOrShortName; | |
| 98 return false; | |
| 99 } | |
| 100 | |
| 101 // TODO(dominickn,mlamouri): when Chrome supports "minimal-ui", it should be | |
| 102 // accepted. If we accept it today, it would fallback to "browser" and make | |
| 103 // this check moot. See https://crbug.com/604390. | |
| 104 if (manifest.display != blink::WebDisplayModeStandalone && | |
| 105 manifest.display != blink::WebDisplayModeFullscreen) { | |
| 106 valid_webapp_manifest_error_ = ManifestDisplayIsNotStandaloneOrFullscreen; | |
| 107 return false; | |
| 108 } | |
| 109 | |
| 110 if (!DoesManifestContainRequiredIcon(manifest)) { | |
| 111 valid_webapp_manifest_error_ = ManifestMissingSuitableIcon; | |
| 112 return false; | |
| 113 } | |
| 114 | |
| 115 return true; | |
| 116 } | |
| 117 | |
| 118 void InstallableChecker::Start(const InstallableParams& params, | |
| 119 const InstallableCallback& callback) { | |
| 120 DCHECK_CURRENTLY_ON(content::BrowserThread::UI); | |
| 121 | |
| 122 // Always reset processing_error_, as it records events like navigation which | |
| 123 // are outside of fetching/validating resources. | |
| 124 processing_error_ = NoErrorDetected; | |
| 125 | |
| 126 // If we've already working on a task, or running callbacks, add the new task | |
| 127 // to the pending list and return. It will be dealt with once the current work | |
| 128 // completes. We keep the pending list separate in case this call was | |
| 129 // initiated during an InstallableCallback invocation, which modifies the | |
| 130 // tasks_ list. | |
| 131 if (IsActive() || HasFlag(RUNNING_CALLBACKS)) { | |
| 132 pending_tasks_.push_back({params, callback}); | |
| 133 return; | |
| 134 } | |
| 135 | |
| 136 tasks_.push_back({params, callback}); | |
| 137 SetFlag(STARTED); | |
| 138 StartTask(); | |
| 139 } | |
| 140 | |
| 141 void InstallableChecker::Cancel() { | |
| 142 // Clear the STARTED flag to signal that we should stop work immediately. | |
| 143 // Callers of this method should immediately call back to FetchResource(), | |
| 144 // which will terminate the current task and run callbacks. | |
|
gone
2016/07/20 22:21:35
Is there any way to enforce that FetchResource() i
dominickn
2016/07/21 00:24:47
This is tricky, because it should call FetchResour
gone
2016/07/21 00:49:27
Acknowledged.
| |
| 145 ClearFlag(STARTED); | |
| 146 } | |
| 147 | |
| 148 bool InstallableChecker::DoesIconSizeMatch( | |
| 149 const InstallableParams& params) const { | |
| 150 return (ideal_icon_size_in_dp_ == params.ideal_icon_size_in_dp) && | |
| 151 (minimum_icon_size_in_dp_ == params.minimum_icon_size_in_dp); | |
| 152 } | |
| 153 | |
| 154 ErrorCode InstallableChecker::GetErrorCode(const InstallableParams& params) { | |
| 155 if (processing_error_ != NoErrorDetected) | |
| 156 return processing_error_; | |
| 157 if (manifest_error_ != NoErrorDetected) | |
|
gone
2016/07/20 22:21:35
should we do else if all the way down?
dominickn
2016/07/21 00:24:47
I didn't because every branch returns (saves a wor
gone
2016/07/21 00:49:27
Nah, keep it the way you like.
| |
| 158 return manifest_error_; | |
| 159 if (params.has_valid_webapp_manifest && | |
| 160 valid_webapp_manifest_error_ != NoErrorDetected) { | |
| 161 return valid_webapp_manifest_error_; | |
| 162 } | |
| 163 if (params.has_service_worker && service_worker_error_ != NoErrorDetected) | |
| 164 return service_worker_error_; | |
| 165 if (params.has_valid_icon && icon_error_ != NoErrorDetected) | |
| 166 return icon_error_; | |
| 167 | |
| 168 return NoErrorDetected; | |
| 169 } | |
| 170 | |
| 171 content::WebContents* InstallableChecker::GetWebContents() { | |
| 172 content::WebContents* contents = web_contents(); | |
| 173 if (!contents || contents->IsBeingDestroyed()) | |
| 174 return nullptr; | |
| 175 return contents; | |
| 176 } | |
| 177 | |
| 178 bool InstallableChecker::IsComplete(const InstallableParams& params) const { | |
| 179 // Returns true if for all resources: | |
| 180 // a. the params did not request it, OR | |
| 181 // b. the resource has been retrieved. | |
| 182 return (HasFlag(MANIFEST_FETCHED)) && | |
| 183 (!params.has_valid_webapp_manifest || HasFlag(MANIFEST_VALIDATED)) && | |
|
gone
2016/07/20 22:21:35
just reading the line straight makes it seem like
dominickn
2016/07/21 00:24:47
Changed to params.check_*
| |
| 184 (!params.has_service_worker || HasFlag(SERVICE_WORKER_CHECKED)) && | |
| 185 (!params.has_valid_icon || | |
| 186 (HasFlag(ICON_FETCHED) && DoesIconSizeMatch(params))); | |
| 187 } | |
| 188 | |
| 189 bool InstallableChecker::IsRunning(content::WebContents* web_contents) { | |
| 190 if (!web_contents) { | |
| 191 processing_error_ = RendererExiting; | |
| 192 return false; | |
| 193 } | |
| 194 | |
| 195 if (!IsActive()) { | |
| 196 processing_error_ = UserNavigatedBeforeBannerShown; | |
| 197 return false; | |
| 198 } | |
| 199 | |
| 200 return true; | |
| 201 } | |
| 202 | |
| 203 void InstallableChecker::Reset() { | |
| 204 status_ = DORMANT; | |
| 205 processing_error_ = NoErrorDetected; | |
| 206 manifest_error_ = NoErrorDetected; | |
| 207 valid_webapp_manifest_error_ = NoErrorDetected; | |
| 208 service_worker_error_ = NoErrorDetected; | |
| 209 icon_error_ = NoErrorDetected; | |
| 210 | |
| 211 ideal_icon_size_in_dp_ = kIconSizeUnset; | |
| 212 minimum_icon_size_in_dp_ = kIconSizeUnset; | |
| 213 | |
| 214 tasks_.clear(); | |
| 215 pending_tasks_.clear(); | |
| 216 | |
| 217 manifest_url_ = GURL(); | |
| 218 manifest_ = content::Manifest(); | |
| 219 icon_url_ = GURL(); | |
| 220 icon_.reset(nullptr); | |
| 221 has_valid_webapp_manifest_ = false; | |
| 222 has_service_worker_ = false; | |
| 223 } | |
| 224 | |
| 225 void InstallableChecker::RunCallbacks() { | |
| 226 // Post a callback and delete it from the list of tasks if: | |
| 227 // - the STARTED status bit is missing. This means that we've either finished | |
| 228 // all possible checks, or we have canceled the pipeline | |
| 229 // - the params that the callback was started with have been satisfied. | |
| 230 SetFlag(RUNNING_CALLBACKS); | |
| 231 for (auto it = tasks_.begin(); it != tasks_.end();) { | |
| 232 if (!IsActive() || IsComplete(it->first)) { | |
|
gone
2016/07/20 22:21:35
Can you add a comment here describing the scenario
dominickn
2016/07/21 00:24:47
Done.
| |
| 233 InstallableResult result = {GetErrorCode(it->first), | |
| 234 manifest_url_, | |
| 235 manifest_, | |
| 236 icon_url_, | |
| 237 icon_.get(), | |
| 238 has_valid_webapp_manifest_, | |
| 239 has_service_worker_}; | |
| 240 // We must run this directly to guarantee the callback gets consistent | |
| 241 // results. If we PostTask, a second Task may begin that invalidates the | |
| 242 // icon object before the callback gets a chance to use it. | |
| 243 it->second.Run(result); | |
| 244 it = tasks_.erase(it); | |
| 245 } else { | |
|
gone
2016/07/20 22:21:35
indentation here is wonky
dominickn
2016/07/21 00:24:47
Done.
| |
| 246 ++it; | |
| 247 } | |
| 248 } | |
| 249 ClearFlag(RUNNING_CALLBACKS); | |
| 250 } | |
| 251 | |
| 252 void InstallableChecker::StartTask() { | |
| 253 RunCallbacks(); | |
| 254 | |
| 255 if (!pending_tasks_.empty()) { | |
| 256 // Shift any pending tasks to the end of tasks_. | |
| 257 tasks_.insert(tasks_.end(), pending_tasks_.begin(), pending_tasks_.end()); | |
| 258 pending_tasks_.clear(); | |
| 259 } | |
| 260 | |
| 261 // If there's nothing to do, exit. Resources remain cached so any future calls | |
| 262 // won't re-fetch anything that has already been retrieved. | |
| 263 if (tasks_.empty()) { | |
| 264 ClearFlag(STARTED); | |
| 265 return; | |
| 266 } | |
| 267 | |
| 268 // If we are requesting an icon, and the requested size differs from any | |
| 269 // previously fetched (or if we haven't yet fetched an icon), reset the icon | |
| 270 // state. This has no impact on the other resources, so don't reset them. | |
| 271 const InstallableParams& params = tasks_[0].first; | |
| 272 if (params.has_valid_icon && !DoesIconSizeMatch(params)) { | |
| 273 ideal_icon_size_in_dp_ = params.ideal_icon_size_in_dp; | |
| 274 minimum_icon_size_in_dp_ = params.minimum_icon_size_in_dp; | |
| 275 icon_url_ = GURL(); | |
| 276 icon_.reset(nullptr); | |
| 277 icon_error_ = NoErrorDetected; | |
| 278 ClearFlag(ICON_FETCHED); | |
| 279 } | |
| 280 | |
| 281 FetchResource(); | |
| 282 } | |
| 283 | |
| 284 void InstallableChecker::FetchResource() { | |
| 285 DCHECK(!tasks_.empty()); | |
| 286 const InstallableParams& params = tasks_[0].first; | |
| 287 | |
| 288 // Cancel if there is an error code for any resource requested by params. | |
| 289 if (GetErrorCode(params) != NoErrorDetected) | |
| 290 Cancel(); | |
| 291 | |
| 292 // If not active, then Cancel() has been called. Go straight back to | |
| 293 // StartTask, which will clear the current task. Otherwise, if we fall through | |
| 294 // to the else case, we've fetched everything necessary for this task, so | |
| 295 // call StartTask to run its callback and start the next task. | |
| 296 if (!IsActive()) | |
| 297 StartTask(); | |
| 298 else if (!HasFlag(MANIFEST_FETCHED)) | |
| 299 FetchManifest(); | |
| 300 else if (params.has_valid_webapp_manifest && !HasFlag(MANIFEST_VALIDATED)) | |
| 301 CheckValidWebappManifest(); | |
| 302 else if (params.has_service_worker && !HasFlag(SERVICE_WORKER_CHECKED)) | |
| 303 CheckServiceWorker(); | |
| 304 else if (params.has_valid_icon && !HasFlag(ICON_FETCHED)) | |
| 305 ExtractAndFetchBestIcon(); | |
| 306 else | |
| 307 StartTask(); | |
| 308 } | |
| 309 | |
| 310 void InstallableChecker::FetchManifest() { | |
| 311 DCHECK(!HasFlag(MANIFEST_FETCHED)); | |
| 312 DCHECK(manifest_.IsEmpty()); | |
| 313 DCHECK(manifest_url_.is_empty()); | |
| 314 | |
| 315 content::WebContents* web_contents = GetWebContents(); | |
| 316 DCHECK(web_contents); | |
| 317 | |
| 318 web_contents->GetManifest(base::Bind(&InstallableChecker::OnDidGetManifest, | |
| 319 weak_factory_.GetWeakPtr())); | |
| 320 } | |
| 321 | |
| 322 void InstallableChecker::OnDidGetManifest(const GURL& manifest_url, | |
| 323 const content::Manifest& manifest) { | |
| 324 if (!IsRunning(GetWebContents())) | |
| 325 Cancel(); | |
| 326 else if (manifest_url.is_empty()) | |
| 327 manifest_error_ = NoManifest; | |
| 328 else if (manifest.IsEmpty()) | |
| 329 manifest_error_ = ManifestEmpty; | |
| 330 | |
| 331 manifest_url_ = manifest_url; | |
| 332 manifest_ = manifest; | |
| 333 | |
| 334 SetFlag(MANIFEST_FETCHED); | |
| 335 FetchResource(); | |
| 336 } | |
| 337 | |
| 338 void InstallableChecker::CheckValidWebappManifest() { | |
| 339 DCHECK(!HasFlag(MANIFEST_VALIDATED)); | |
| 340 DCHECK(!manifest_.IsEmpty()); | |
| 341 | |
| 342 has_valid_webapp_manifest_ = IsManifestValidForWebApp(manifest_); | |
| 343 if (!has_valid_webapp_manifest_) | |
| 344 Cancel(); | |
| 345 | |
| 346 SetFlag(MANIFEST_VALIDATED); | |
| 347 FetchResource(); | |
| 348 } | |
| 349 | |
| 350 void InstallableChecker::CheckServiceWorker() { | |
| 351 DCHECK(!HasFlag(SERVICE_WORKER_CHECKED)); | |
| 352 DCHECK(!manifest_.IsEmpty()); | |
| 353 | |
| 354 content::WebContents* web_contents = GetWebContents(); | |
| 355 | |
| 356 // Check to see if there is a single service worker controlling this page | |
| 357 // and the manifest's start url. | |
| 358 Profile* profile = | |
| 359 Profile::FromBrowserContext(web_contents->GetBrowserContext()); | |
| 360 content::StoragePartition* storage_partition = | |
| 361 content::BrowserContext::GetStoragePartition( | |
| 362 profile, web_contents->GetSiteInstance()); | |
| 363 DCHECK(storage_partition); | |
| 364 | |
| 365 storage_partition->GetServiceWorkerContext()->CheckHasServiceWorker( | |
| 366 web_contents->GetLastCommittedURL(), manifest_.start_url, | |
| 367 base::Bind(&InstallableChecker::OnDidCheckHasServiceWorker, | |
| 368 weak_factory_.GetWeakPtr())); | |
| 369 } | |
| 370 | |
| 371 void InstallableChecker::OnDidCheckHasServiceWorker(bool has_service_worker) { | |
| 372 if (!IsRunning(GetWebContents())) | |
| 373 Cancel(); | |
| 374 | |
| 375 has_service_worker_ = has_service_worker; | |
| 376 if (!has_service_worker) | |
| 377 service_worker_error_ = NoMatchingServiceWorker; | |
| 378 | |
| 379 SetFlag(SERVICE_WORKER_CHECKED); | |
| 380 FetchResource(); | |
| 381 } | |
| 382 | |
| 383 void InstallableChecker::ExtractAndFetchBestIcon() { | |
| 384 // icon_url_ and icon_ should have both been reset if this method is called. | |
| 385 DCHECK(!HasFlag(ICON_FETCHED)); | |
| 386 DCHECK(!manifest_.IsEmpty()); | |
| 387 DCHECK(icon_url_.is_empty()); | |
| 388 DCHECK(icon_.get() == nullptr); | |
| 389 DCHECK_GT(ideal_icon_size_in_dp_, 0); | |
| 390 DCHECK_GT(minimum_icon_size_in_dp_, 0); | |
| 391 | |
| 392 GURL icon_url = ManifestIconSelector::FindBestMatchingIcon( | |
| 393 manifest_.icons, ideal_icon_size_in_dp_, minimum_icon_size_in_dp_); | |
| 394 | |
| 395 // First, check the icon URL. If it exists, see if the ManifestIconDownloader | |
| 396 // is able to download it. If it can, it will call OnAppIconFetched. Any | |
| 397 // conditional failures will set the ICON_FETCHED bit and fall through back to | |
| 398 // FetchResource. | |
| 399 if (icon_url.is_empty()) { | |
| 400 icon_error_ = NoIconMatchingRequirements; | |
| 401 } else { | |
| 402 bool can_download_icon = ManifestIconDownloader::Download( | |
| 403 GetWebContents(), icon_url, ideal_icon_size_in_dp_, | |
| 404 minimum_icon_size_in_dp_, | |
| 405 base::Bind(&InstallableChecker::OnAppIconFetched, | |
| 406 weak_factory_.GetWeakPtr(), icon_url)); | |
| 407 if (can_download_icon) | |
| 408 return; | |
| 409 icon_error_ = CannotDownloadIcon; | |
| 410 } | |
| 411 | |
| 412 SetFlag(ICON_FETCHED); | |
| 413 FetchResource(); | |
| 414 } | |
| 415 | |
| 416 void InstallableChecker::OnAppIconFetched(const GURL icon_url, | |
| 417 const SkBitmap& bitmap) { | |
| 418 if (!IsRunning(GetWebContents())) { | |
| 419 Cancel(); | |
| 420 } else if (bitmap.drawsNothing()) { | |
| 421 icon_error_ = NoIconAvailable; | |
| 422 } else { | |
| 423 icon_url_ = icon_url; | |
| 424 icon_.reset(new SkBitmap(bitmap)); | |
| 425 } | |
| 426 | |
| 427 SetFlag(ICON_FETCHED); | |
| 428 FetchResource(); | |
| 429 } | |
| 430 | |
| 431 void InstallableChecker::DidFinishNavigation( | |
| 432 content::NavigationHandle* handle) { | |
| 433 if (handle->IsInMainFrame() && handle->HasCommitted() && | |
| 434 !handle->IsErrorPage() && !handle->IsSamePage()) { | |
| 435 Reset(); | |
| 436 } | |
| 437 } | |
| 438 | |
| 439 void InstallableChecker::WebContentsDestroyed() { | |
| 440 Reset(); | |
| 441 Observe(nullptr); | |
| 442 } | |
| 443 | |
| 444 // static | |
| 445 int InstallableChecker::GetMinimumIconSizeInPx() { | |
| 446 return kIconMinimumSizeInPx; | |
| 447 } | |
| 448 | |
| 449 } // namespace installable | |
| OLD | NEW |