// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "chrome/browser/favicon/favicon_handler.h" #include "build/build_config.h" #include <algorithm> #include <vector> #include "base/bind.h" #include "base/bind_helpers.h" #include "base/memory/ref_counted_memory.h" #include "chrome/browser/bookmarks/bookmark_service.h" #include "chrome/browser/favicon/favicon_service_factory.h" #include "chrome/browser/favicon/favicon_util.h" #include "chrome/browser/history/select_favicon_frames.h" #include "chrome/browser/profiles/profile.h" #include "content/public/browser/favicon_status.h" #include "content/public/browser/navigation_entry.h" #include "skia/ext/image_operations.h" #include "ui/gfx/codec/png_codec.h" #include "ui/gfx/image/image.h" #include "ui/gfx/image/image_skia.h" #include "ui/gfx/image/image_util.h" using content::FaviconURL; using content::NavigationEntry; namespace { // Size (along each axis) of a touch icon. This currently corresponds to // the apple touch icon for iPad. const int kTouchIconSize = 144; // Returns chrome::IconType the given icon_type corresponds to. chrome::IconType ToHistoryIconType(FaviconURL::IconType icon_type) { switch (icon_type) { case FaviconURL::FAVICON: return chrome::FAVICON; case FaviconURL::TOUCH_ICON: return chrome::TOUCH_ICON; case FaviconURL::TOUCH_PRECOMPOSED_ICON: return chrome::TOUCH_PRECOMPOSED_ICON; case FaviconURL::INVALID_ICON: return chrome::INVALID_ICON; } NOTREACHED(); return chrome::INVALID_ICON; } // Get the maximal icon size in pixels for a icon of type |icon_type| for the // current platform. int GetMaximalIconSize(chrome::IconType icon_type) { switch (icon_type) { case chrome::FAVICON: #if defined(OS_ANDROID) return 192; #else return gfx::ImageSkia::GetMaxSupportedScale() * gfx::kFaviconSize; #endif case chrome::TOUCH_ICON: case chrome::TOUCH_PRECOMPOSED_ICON: return kTouchIconSize; case chrome::INVALID_ICON: return 0; } NOTREACHED(); return 0; } bool DoUrlAndIconMatch(const FaviconURL& favicon_url, const GURL& url, chrome::IconType icon_type) { return favicon_url.icon_url == url && favicon_url.icon_type == static_cast<FaviconURL::IconType>(icon_type); } // Returns true if all of the icon URLs and icon types in |bitmap_results| are // identical and if they match the icon URL and icon type in |favicon_url|. // Returns false if |bitmap_results| is empty. bool DoUrlsAndIconsMatch( const FaviconURL& favicon_url, const std::vector<chrome::FaviconBitmapResult>& bitmap_results) { if (bitmap_results.empty()) return false; chrome::IconType icon_type = ToHistoryIconType(favicon_url.icon_type); for (size_t i = 0; i < bitmap_results.size(); ++i) { if (favicon_url.icon_url != bitmap_results[i].icon_url || icon_type != bitmap_results[i].icon_type) { return false; } } return true; } std::string UrlWithoutFragment(const GURL& gurl) { GURL::Replacements replacements; replacements.ClearRef(); return gurl.ReplaceComponents(replacements).spec(); } bool UrlMatches(const GURL& gurl_a, const GURL& gurl_b) { return UrlWithoutFragment(gurl_a) == UrlWithoutFragment(gurl_b); } // Return true if |bitmap_result| is expired. bool IsExpired(const chrome::FaviconBitmapResult& bitmap_result) { return bitmap_result.expired; } // Return true if |bitmap_result| is valid. bool IsValid(const chrome::FaviconBitmapResult& bitmap_result) { return bitmap_result.is_valid(); } // Returns true if at least one of the bitmaps in |bitmap_results| is expired or // if |bitmap_results| is missing favicons for |desired_size_in_dip| and one of // the scale factors in FaviconUtil::GetFaviconScaleFactors(). bool HasExpiredOrIncompleteResult( int desired_size_in_dip, const std::vector<chrome::FaviconBitmapResult>& bitmap_results) { // Check if at least one of the bitmaps is expired. std::vector<chrome::FaviconBitmapResult>::const_iterator it = std::find_if(bitmap_results.begin(), bitmap_results.end(), IsExpired); if (it != bitmap_results.end()) return true; // Any favicon size is good if the desired size is 0. if (desired_size_in_dip == 0) return false; // Check if the favicon for at least one of the scale factors is missing. // |bitmap_results| should always be complete for data inserted by // FaviconHandler as the FaviconHandler stores favicons resized to all // of FaviconUtil::GetFaviconScaleFactors() into the history backend. // Examples of when |bitmap_results| can be incomplete: // - Favicons inserted into the history backend by sync. // - Favicons for imported bookmarks. std::vector<gfx::Size> favicon_sizes; for (size_t i = 0; i < bitmap_results.size(); ++i) favicon_sizes.push_back(bitmap_results[i].pixel_size); std::vector<ui::ScaleFactor> scale_factors = FaviconUtil::GetFaviconScaleFactors(); for (size_t i = 0; i < scale_factors.size(); ++i) { int edge_size_in_pixel = floor( desired_size_in_dip * ui::GetImageScale(scale_factors[i])); std::vector<gfx::Size>::iterator it = std::find(favicon_sizes.begin(), favicon_sizes.end(), gfx::Size(edge_size_in_pixel, edge_size_in_pixel)); if (it == favicon_sizes.end()) return true; } return false; } // Returns true if at least one of |bitmap_results| is valid. bool HasValidResult( const std::vector<chrome::FaviconBitmapResult>& bitmap_results) { std::vector<chrome::FaviconBitmapResult>::const_iterator it = std::find_if(bitmap_results.begin(), bitmap_results.end(), IsValid); return it != bitmap_results.end(); } } // namespace //////////////////////////////////////////////////////////////////////////////// FaviconHandler::DownloadRequest::DownloadRequest() : icon_type(chrome::INVALID_ICON) { } FaviconHandler::DownloadRequest::~DownloadRequest() { } FaviconHandler::DownloadRequest::DownloadRequest( const GURL& url, const GURL& image_url, chrome::IconType icon_type) : url(url), image_url(image_url), icon_type(icon_type) { } //////////////////////////////////////////////////////////////////////////////// FaviconHandler::FaviconCandidate::FaviconCandidate() : score(0), icon_type(chrome::INVALID_ICON) { } FaviconHandler::FaviconCandidate::~FaviconCandidate() { } FaviconHandler::FaviconCandidate::FaviconCandidate( const GURL& url, const GURL& image_url, const gfx::Image& image, float score, chrome::IconType icon_type) : url(url), image_url(image_url), image(image), score(score), icon_type(icon_type) { } //////////////////////////////////////////////////////////////////////////////// FaviconHandler::FaviconHandler(Profile* profile, FaviconHandlerDelegate* delegate, Type icon_type) : got_favicon_from_history_(false), favicon_expired_or_incomplete_(false), icon_types_(icon_type == FAVICON ? chrome::FAVICON : chrome::TOUCH_ICON | chrome::TOUCH_PRECOMPOSED_ICON), profile_(profile), delegate_(delegate) { DCHECK(profile_); DCHECK(delegate_); } FaviconHandler::~FaviconHandler() { } void FaviconHandler::FetchFavicon(const GURL& url) { cancelable_task_tracker_.TryCancelAll(); url_ = url; favicon_expired_or_incomplete_ = got_favicon_from_history_ = false; image_urls_.clear(); // Request the favicon from the history service. In parallel to this the // renderer is going to notify us (well WebContents) when the favicon url is // available. if (GetFaviconService()) { GetFaviconForURL( url_, icon_types_, base::Bind(&FaviconHandler::OnFaviconDataForInitialURL, base::Unretained(this)), &cancelable_task_tracker_); } } FaviconService* FaviconHandler::GetFaviconService() { return FaviconServiceFactory::GetForProfile( profile_, Profile::EXPLICIT_ACCESS); } bool FaviconHandler::UpdateFaviconCandidate(const GURL& url, const GURL& image_url, const gfx::Image& image, float score, chrome::IconType icon_type) { bool update_candidate = false; bool exact_match = score == 1; if (preferred_icon_size() == 0) { // No preferred size, use this icon. update_candidate = true; exact_match = true; } else if (favicon_candidate_.icon_type == chrome::INVALID_ICON) { // No current candidate, use this. update_candidate = true; } else { if (exact_match) { // Exact match, use this. update_candidate = true; } else { // Compare against current candidate. if (score > favicon_candidate_.score) update_candidate = true; } } if (update_candidate) { favicon_candidate_ = FaviconCandidate( url, image_url, image, score, icon_type); } return exact_match; } void FaviconHandler::SetFavicon( const GURL& url, const GURL& icon_url, const gfx::Image& image, chrome::IconType icon_type) { if (GetFaviconService() && ShouldSaveFavicon(url)) SetHistoryFavicons(url, icon_url, icon_type, image); if (UrlMatches(url, url_) && icon_type == chrome::FAVICON) { NavigationEntry* entry = GetEntry(); if (entry) UpdateFavicon(entry, icon_url, image); } } void FaviconHandler::UpdateFavicon(NavigationEntry* entry, const std::vector<chrome::FaviconBitmapResult>& favicon_bitmap_results) { gfx::Image resized_image = FaviconUtil::SelectFaviconFramesFromPNGs( favicon_bitmap_results, FaviconUtil::GetFaviconScaleFactors(), preferred_icon_size()); // The history service sends back results for a single icon URL, so it does // not matter which result we get the |icon_url| from. const GURL icon_url = favicon_bitmap_results.empty() ? GURL() : favicon_bitmap_results[0].icon_url; UpdateFavicon(entry, icon_url, resized_image); } void FaviconHandler::UpdateFavicon(NavigationEntry* entry, const GURL& icon_url, const gfx::Image& image) { // No matter what happens, we need to mark the favicon as being set. entry->GetFavicon().valid = true; bool icon_url_changed = (entry->GetFavicon().url != icon_url); entry->GetFavicon().url = icon_url; if (image.IsEmpty()) return; gfx::Image image_with_adjusted_colorspace = image; FaviconUtil::SetFaviconColorSpace(&image_with_adjusted_colorspace); entry->GetFavicon().image = image_with_adjusted_colorspace; NotifyFaviconUpdated(icon_url_changed); } void FaviconHandler::OnUpdateFaviconURL( int32 page_id, const std::vector<FaviconURL>& candidates) { image_urls_.clear(); favicon_candidate_ = FaviconCandidate(); for (std::vector<FaviconURL>::const_iterator i = candidates.begin(); i != candidates.end(); ++i) { if (!i->icon_url.is_empty() && (i->icon_type & icon_types_)) image_urls_.push_back(*i); } // TODO(davemoore) Should clear on empty url. Currently we ignore it. // This appears to be what FF does as well. if (image_urls_.empty()) return; if (!GetFaviconService()) return; ProcessCurrentUrl(); } void FaviconHandler::ProcessCurrentUrl() { DCHECK(!image_urls_.empty()); NavigationEntry* entry = GetEntry(); if (!entry) return; // For FAVICON. if (current_candidate()->icon_type == FaviconURL::FAVICON) { if (!favicon_expired_or_incomplete_ && entry->GetFavicon().valid && DoUrlAndIconMatch(*current_candidate(), entry->GetFavicon().url, chrome::FAVICON)) return; } else if (!favicon_expired_or_incomplete_ && got_favicon_from_history_ && HasValidResult(history_results_) && DoUrlsAndIconsMatch(*current_candidate(), history_results_)) { return; } if (got_favicon_from_history_) DownloadFaviconOrAskHistory(entry->GetURL(), current_candidate()->icon_url, ToHistoryIconType(current_candidate()->icon_type)); } void FaviconHandler::OnDidDownloadFavicon( int id, const GURL& image_url, const std::vector<SkBitmap>& bitmaps, const std::vector<gfx::Size>& original_bitmap_sizes) { DownloadRequests::iterator i = download_requests_.find(id); if (i == download_requests_.end()) { // Currently WebContents notifies us of ANY downloads so that it is // possible to get here. return; } if (current_candidate() && DoUrlAndIconMatch(*current_candidate(), image_url, i->second.icon_type)) { float score = 0.0f; std::vector<ui::ScaleFactor> scale_factors = FaviconUtil::GetFaviconScaleFactors(); gfx::Image image(SelectFaviconFrames(bitmaps, original_bitmap_sizes, scale_factors, preferred_icon_size(), &score)); // The downloaded icon is still valid when there is no FaviconURL update // during the downloading. bool request_next_icon = true; if (!bitmaps.empty()) { request_next_icon = !UpdateFaviconCandidate( i->second.url, image_url, image, score, i->second.icon_type); } if (request_next_icon && GetEntry() && image_urls_.size() > 1) { // Remove the first member of image_urls_ and process the remaining. image_urls_.pop_front(); ProcessCurrentUrl(); } else if (favicon_candidate_.icon_type != chrome::INVALID_ICON) { // No more icons to request, set the favicon from the candidate. SetFavicon(favicon_candidate_.url, favicon_candidate_.image_url, favicon_candidate_.image, favicon_candidate_.icon_type); // Reset candidate. image_urls_.clear(); favicon_candidate_ = FaviconCandidate(); } } download_requests_.erase(i); } NavigationEntry* FaviconHandler::GetEntry() { NavigationEntry* entry = delegate_->GetActiveEntry(); if (entry && UrlMatches(entry->GetURL(), url_)) return entry; // If the URL has changed out from under us (as will happen with redirects) // return NULL. return NULL; } int FaviconHandler::DownloadFavicon(const GURL& image_url, int max_bitmap_size) { if (!image_url.is_valid()) { NOTREACHED(); return 0; } return delegate_->StartDownload(image_url, max_bitmap_size); } void FaviconHandler::UpdateFaviconMappingAndFetch( const GURL& page_url, const GURL& icon_url, chrome::IconType icon_type, const FaviconService::FaviconResultsCallback& callback, CancelableTaskTracker* tracker) { // TODO(pkotwicz): pass in all of |image_urls_| to // UpdateFaviconMappingsAndFetch(). std::vector<GURL> icon_urls; icon_urls.push_back(icon_url); GetFaviconService()->UpdateFaviconMappingsAndFetch( page_url, icon_urls, icon_type, preferred_icon_size(), callback, tracker); } void FaviconHandler::GetFavicon( const GURL& icon_url, chrome::IconType icon_type, const FaviconService::FaviconResultsCallback& callback, CancelableTaskTracker* tracker) { GetFaviconService()->GetFavicon( icon_url, icon_type, preferred_icon_size(), callback, tracker); } void FaviconHandler::GetFaviconForURL( const GURL& page_url, int icon_types, const FaviconService::FaviconResultsCallback& callback, CancelableTaskTracker* tracker) { GetFaviconService()->GetFaviconForURL( FaviconService::FaviconForURLParams(page_url, icon_types, preferred_icon_size()), callback, tracker); } void FaviconHandler::SetHistoryFavicons(const GURL& page_url, const GURL& icon_url, chrome::IconType icon_type, const gfx::Image& image) { GetFaviconService()->SetFavicons(page_url, icon_url, icon_type, image); } bool FaviconHandler::ShouldSaveFavicon(const GURL& url) { if (!profile_->IsOffTheRecord()) return true; // Otherwise store the favicon if the page is bookmarked. BookmarkService* bookmark_service = BookmarkService::FromBrowserContext(profile_); return bookmark_service && bookmark_service->IsBookmarked(url); } void FaviconHandler::NotifyFaviconUpdated(bool icon_url_changed) { delegate_->NotifyFaviconUpdated(icon_url_changed); } void FaviconHandler::OnFaviconDataForInitialURL( const std::vector<chrome::FaviconBitmapResult>& favicon_bitmap_results) { NavigationEntry* entry = GetEntry(); if (!entry) return; got_favicon_from_history_ = true; history_results_ = favicon_bitmap_results; bool has_results = !favicon_bitmap_results.empty(); favicon_expired_or_incomplete_ = has_results && HasExpiredOrIncompleteResult( preferred_icon_size(), favicon_bitmap_results); if (has_results && icon_types_ == chrome::FAVICON && !entry->GetFavicon().valid && (!current_candidate() || DoUrlsAndIconsMatch(*current_candidate(), favicon_bitmap_results))) { if (HasValidResult(favicon_bitmap_results)) { // The db knows the favicon (although it may be out of date) and the entry // doesn't have an icon. Set the favicon now, and if the favicon turns out // to be expired (or the wrong url) we'll fetch later on. This way the // user doesn't see a flash of the default favicon. UpdateFavicon(entry, favicon_bitmap_results); } else { // If |favicon_bitmap_results| does not have any valid results, treat the // favicon as if it's expired. // TODO(pkotwicz): Do something better. favicon_expired_or_incomplete_ = true; } } if (has_results && !favicon_expired_or_incomplete_) { if (current_candidate() && !DoUrlsAndIconsMatch(*current_candidate(), favicon_bitmap_results)) { // Mapping in the database is wrong. DownloadFavIconOrAskHistory will // update the mapping for this url and download the favicon if we don't // already have it. DownloadFaviconOrAskHistory(entry->GetURL(), current_candidate()->icon_url, static_cast<chrome::IconType>(current_candidate()->icon_type)); } } else if (current_candidate()) { // We know the official url for the favicon, by either don't have the // favicon or its expired. Continue on to DownloadFaviconOrAskHistory to // either download or check history again. DownloadFaviconOrAskHistory(entry->GetURL(), current_candidate()->icon_url, ToHistoryIconType(current_candidate()->icon_type)); } // else we haven't got the icon url. When we get it we'll ask the // renderer to download the icon. } void FaviconHandler::DownloadFaviconOrAskHistory( const GURL& page_url, const GURL& icon_url, chrome::IconType icon_type) { if (favicon_expired_or_incomplete_) { // We have the mapping, but the favicon is out of date. Download it now. ScheduleDownload(page_url, icon_url, icon_type); } else if (GetFaviconService()) { // We don't know the favicon, but we may have previously downloaded the // favicon for another page that shares the same favicon. Ask for the // favicon given the favicon URL. if (profile_->IsOffTheRecord()) { GetFavicon( icon_url, icon_type, base::Bind(&FaviconHandler::OnFaviconData, base::Unretained(this)), &cancelable_task_tracker_); } else { // Ask the history service for the icon. This does two things: // 1. Attempts to fetch the favicon data from the database. // 2. If the favicon exists in the database, this updates the database to // include the mapping between the page url and the favicon url. // This is asynchronous. The history service will call back when done. // Issue the request and associate the current page ID with it. UpdateFaviconMappingAndFetch( page_url, icon_url, icon_type, base::Bind(&FaviconHandler::OnFaviconData, base::Unretained(this)), &cancelable_task_tracker_); } } } void FaviconHandler::OnFaviconData( const std::vector<chrome::FaviconBitmapResult>& favicon_bitmap_results) { NavigationEntry* entry = GetEntry(); if (!entry) return; bool has_results = !favicon_bitmap_results.empty(); bool has_expired_or_incomplete_result = HasExpiredOrIncompleteResult( preferred_icon_size(), favicon_bitmap_results); if (has_results && icon_types_ == chrome::FAVICON) { if (HasValidResult(favicon_bitmap_results)) { // There is a favicon, set it now. If expired we'll download the current // one again, but at least the user will get some icon instead of the // default and most likely the current one is fine anyway. UpdateFavicon(entry, favicon_bitmap_results); } if (has_expired_or_incomplete_result) { // The favicon is out of date. Request the current one. ScheduleDownload(entry->GetURL(), entry->GetFavicon().url, chrome::FAVICON); } } else if (current_candidate() && (!has_results || has_expired_or_incomplete_result || !(DoUrlsAndIconsMatch(*current_candidate(), favicon_bitmap_results)))) { // We don't know the favicon, it is out of date or its type is not same as // one got from page. Request the current one. ScheduleDownload(entry->GetURL(), current_candidate()->icon_url, ToHistoryIconType(current_candidate()->icon_type)); } history_results_ = favicon_bitmap_results; } int FaviconHandler::ScheduleDownload( const GURL& url, const GURL& image_url, chrome::IconType icon_type) { // A max bitmap size is specified to avoid receiving huge bitmaps in // OnDidDownloadFavicon(). See FaviconHandlerDelegate::StartDownload() // for more details about the max bitmap size. const int download_id = DownloadFavicon(image_url, GetMaximalIconSize(icon_type)); if (download_id) { // Download ids should be unique. DCHECK(download_requests_.find(download_id) == download_requests_.end()); download_requests_[download_id] = DownloadRequest(url, image_url, icon_type); } return download_id; }