// Copyright 2014 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/extensions/bookmark_app_helper.h" #include <cctype> #include "base/strings/utf_string_conversions.h" #include "chrome/browser/chrome_notification_types.h" #include "chrome/browser/extensions/crx_installer.h" #include "chrome/browser/extensions/extension_service.h" #include "chrome/browser/extensions/favicon_downloader.h" #include "chrome/browser/extensions/tab_helper.h" #include "chrome/common/extensions/extension_constants.h" #include "chrome/common/extensions/manifest_handlers/app_launch_info.h" #include "content/public/browser/notification_service.h" #include "content/public/browser/notification_source.h" #include "content/public/browser/web_contents.h" #include "extensions/browser/image_loader.h" #include "extensions/common/constants.h" #include "extensions/common/extension.h" #include "extensions/common/manifest_handlers/icons_handler.h" #include "extensions/common/url_pattern.h" #include "grit/platform_locale_settings.h" #include "net/base/registry_controlled_domains/registry_controlled_domain.h" #include "skia/ext/image_operations.h" #include "skia/ext/platform_canvas.h" #include "third_party/skia/include/core/SkBitmap.h" #include "ui/base/l10n/l10n_util.h" #include "ui/base/resource/resource_bundle.h" #include "ui/gfx/canvas.h" #include "ui/gfx/color_analysis.h" #include "ui/gfx/color_utils.h" #include "ui/gfx/font.h" #include "ui/gfx/font_list.h" #include "ui/gfx/image/canvas_image_source.h" #include "ui/gfx/image/image.h" #include "ui/gfx/image/image_family.h" #include "ui/gfx/rect.h" namespace { // Overlays a shortcut icon over the bottom left corner of a given image. class GeneratedIconImageSource : public gfx::CanvasImageSource { public: explicit GeneratedIconImageSource(char letter, SkColor color, int output_size) : gfx::CanvasImageSource(gfx::Size(output_size, output_size), false), letter_(letter), color_(color), output_size_(output_size) {} virtual ~GeneratedIconImageSource() {} private: // gfx::CanvasImageSource overrides: virtual void Draw(gfx::Canvas* canvas) OVERRIDE { const unsigned char kLuminanceThreshold = 190; const int icon_size = output_size_ * 3 / 4; const int icon_inset = output_size_ / 8; const size_t border_radius = output_size_ / 16; const size_t font_size = output_size_ * 7 / 16; std::string font_name = l10n_util::GetStringUTF8(IDS_SANS_SERIF_FONT_FAMILY); #if defined(OS_CHROMEOS) const std::string kChromeOSFontFamily = "Noto Sans"; font_name = kChromeOSFontFamily; #endif // Draw a rounded rect of the given |color|. SkPaint background_paint; background_paint.setFlags(SkPaint::kAntiAlias_Flag); background_paint.setColor(color_); gfx::Rect icon_rect(icon_inset, icon_inset, icon_size, icon_size); canvas->DrawRoundRect(icon_rect, border_radius, background_paint); // The text rect's size needs to be odd to center the text correctly. gfx::Rect text_rect(icon_inset, icon_inset, icon_size + 1, icon_size + 1); // Draw the letter onto the rounded rect. The letter's color depends on the // luminance of |color|. unsigned char luminance = color_utils::GetLuminanceForColor(color_); canvas->DrawStringRectWithFlags( base::string16(1, std::toupper(letter_)), gfx::FontList(gfx::Font(font_name, font_size)), luminance > kLuminanceThreshold ? SK_ColorBLACK : SK_ColorWHITE, text_rect, gfx::Canvas::TEXT_ALIGN_CENTER); } char letter_; SkColor color_; int output_size_; DISALLOW_COPY_AND_ASSIGN(GeneratedIconImageSource); }; void OnIconsLoaded( WebApplicationInfo web_app_info, const base::Callback<void(const WebApplicationInfo&)> callback, const gfx::ImageFamily& image_family) { for (gfx::ImageFamily::const_iterator it = image_family.begin(); it != image_family.end(); ++it) { WebApplicationInfo::IconInfo icon_info; icon_info.data = *it->ToSkBitmap(); icon_info.width = icon_info.data.width(); icon_info.height = icon_info.data.height(); web_app_info.icons.push_back(icon_info); } callback.Run(web_app_info); } } // namespace namespace extensions { // static std::map<int, SkBitmap> BookmarkAppHelper::ConstrainBitmapsToSizes( const std::vector<SkBitmap>& bitmaps, const std::set<int>& sizes) { std::map<int, SkBitmap> output_bitmaps; std::map<int, SkBitmap> ordered_bitmaps; for (std::vector<SkBitmap>::const_iterator it = bitmaps.begin(); it != bitmaps.end(); ++it) { DCHECK(it->width() == it->height()); ordered_bitmaps[it->width()] = *it; } std::set<int>::const_iterator sizes_it = sizes.begin(); std::map<int, SkBitmap>::const_iterator bitmaps_it = ordered_bitmaps.begin(); while (sizes_it != sizes.end() && bitmaps_it != ordered_bitmaps.end()) { int size = *sizes_it; // Find the closest not-smaller bitmap. bitmaps_it = ordered_bitmaps.lower_bound(size); ++sizes_it; // Ensure the bitmap is valid and smaller than the next allowed size. if (bitmaps_it != ordered_bitmaps.end() && (sizes_it == sizes.end() || bitmaps_it->second.width() < *sizes_it)) { // Resize the bitmap if it does not exactly match the desired size. output_bitmaps[size] = bitmaps_it->second.width() == size ? bitmaps_it->second : skia::ImageOperations::Resize( bitmaps_it->second, skia::ImageOperations::RESIZE_LANCZOS3, size, size); } } return output_bitmaps; } // static void BookmarkAppHelper::GenerateIcon(std::map<int, SkBitmap>* bitmaps, int output_size, SkColor color, char letter) { // Do nothing if there is already an icon of |output_size|. if (bitmaps->count(output_size)) return; gfx::ImageSkia icon_image( new GeneratedIconImageSource(letter, color, output_size), gfx::Size(output_size, output_size)); icon_image.bitmap()->deepCopyTo(&(*bitmaps)[output_size]); } BookmarkAppHelper::BookmarkAppHelper(ExtensionService* service, WebApplicationInfo web_app_info, content::WebContents* contents) : web_app_info_(web_app_info), crx_installer_(extensions::CrxInstaller::CreateSilent(service)) { registrar_.Add(this, chrome::NOTIFICATION_CRX_INSTALLER_DONE, content::Source<CrxInstaller>(crx_installer_.get())); registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_INSTALL_ERROR, content::Source<CrxInstaller>(crx_installer_.get())); crx_installer_->set_error_on_unsupported_requirements(true); if (!contents) return; // Add urls from the WebApplicationInfo. std::vector<GURL> web_app_info_icon_urls; for (std::vector<WebApplicationInfo::IconInfo>::const_iterator it = web_app_info_.icons.begin(); it != web_app_info_.icons.end(); ++it) { if (it->url.is_valid()) web_app_info_icon_urls.push_back(it->url); } favicon_downloader_.reset( new FaviconDownloader(contents, web_app_info_icon_urls, base::Bind(&BookmarkAppHelper::OnIconsDownloaded, base::Unretained(this)))); } BookmarkAppHelper::~BookmarkAppHelper() {} void BookmarkAppHelper::Create(const CreateBookmarkAppCallback& callback) { callback_ = callback; if (favicon_downloader_.get()) favicon_downloader_->Start(); else OnIconsDownloaded(true, std::map<GURL, std::vector<SkBitmap> >()); } void BookmarkAppHelper::OnIconsDownloaded( bool success, const std::map<GURL, std::vector<SkBitmap> >& bitmaps) { // The tab has navigated away during the icon download. Cancel the bookmark // app creation. if (!success) { favicon_downloader_.reset(); callback_.Run(NULL, web_app_info_); return; } // Add the downloaded icons. Extensions only allow certain icon sizes. First // populate icons that match the allowed sizes exactly and then downscale // remaining icons to the closest allowed size that doesn't yet have an icon. std::set<int> allowed_sizes(extension_misc::kExtensionIconSizes, extension_misc::kExtensionIconSizes + extension_misc::kNumExtensionIconSizes); std::vector<SkBitmap> downloaded_icons; for (FaviconDownloader::FaviconMap::const_iterator map_it = bitmaps.begin(); map_it != bitmaps.end(); ++map_it) { for (std::vector<SkBitmap>::const_iterator bitmap_it = map_it->second.begin(); bitmap_it != map_it->second.end(); ++bitmap_it) { if (bitmap_it->empty() || bitmap_it->width() != bitmap_it->height()) continue; downloaded_icons.push_back(*bitmap_it); } } // Add all existing icons from WebApplicationInfo. for (std::vector<WebApplicationInfo::IconInfo>::const_iterator it = web_app_info_.icons.begin(); it != web_app_info_.icons.end(); ++it) { const SkBitmap& icon = it->data; if (!icon.drawsNothing() && icon.width() == icon.height()) downloaded_icons.push_back(icon); } web_app_info_.icons.clear(); // If there are icons that don't match the accepted icon sizes, find the // closest bigger icon to the accepted sizes and resize the icon to it. An // icon will be resized and used for at most one size. std::map<int, SkBitmap> resized_bitmaps( ConstrainBitmapsToSizes(downloaded_icons, allowed_sizes)); // Generate container icons from smaller icons. const int kIconSizesToGenerate[] = {extension_misc::EXTENSION_ICON_SMALL, extension_misc::EXTENSION_ICON_MEDIUM, }; const std::set<int> generate_sizes( kIconSizesToGenerate, kIconSizesToGenerate + arraysize(kIconSizesToGenerate)); // Only generate icons if larger icons don't exist. This means the app // launcher and the taskbar will do their best downsizing large icons and // these icons are only generated as a last resort against upscaling a smaller // icon. if (resized_bitmaps.lower_bound(*generate_sizes.rbegin()) == resized_bitmaps.end()) { GURL app_url = web_app_info_.app_url; // The letter that will be painted on the generated icon. char icon_letter = ' '; std::string domain_and_registry( net::registry_controlled_domains::GetDomainAndRegistry( app_url, net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES)); if (!domain_and_registry.empty()) { icon_letter = domain_and_registry[0]; } else if (!app_url.host().empty()) { icon_letter = app_url.host()[0]; } // The color that will be used for the icon's background. SkColor background_color = SK_ColorBLACK; if (resized_bitmaps.size()) { color_utils::GridSampler sampler; background_color = color_utils::CalculateKMeanColorOfBitmap( resized_bitmaps.begin()->second); } for (std::set<int>::const_iterator it = generate_sizes.begin(); it != generate_sizes.end(); ++it) { GenerateIcon(&resized_bitmaps, *it, background_color, icon_letter); // Also generate the 2x resource for this size. GenerateIcon(&resized_bitmaps, *it * 2, background_color, icon_letter); } } // Populate the icon data into the WebApplicationInfo we are using to // install the bookmark app. for (std::map<int, SkBitmap>::const_iterator resized_bitmaps_it = resized_bitmaps.begin(); resized_bitmaps_it != resized_bitmaps.end(); ++resized_bitmaps_it) { WebApplicationInfo::IconInfo icon_info; icon_info.data = resized_bitmaps_it->second; icon_info.width = icon_info.data.width(); icon_info.height = icon_info.data.height(); web_app_info_.icons.push_back(icon_info); } // Install the app. crx_installer_->InstallWebApp(web_app_info_); favicon_downloader_.reset(); } void BookmarkAppHelper::Observe(int type, const content::NotificationSource& source, const content::NotificationDetails& details) { switch (type) { case chrome::NOTIFICATION_CRX_INSTALLER_DONE: { const Extension* extension = content::Details<const Extension>(details).ptr(); DCHECK(extension); DCHECK_EQ(AppLaunchInfo::GetLaunchWebURL(extension), web_app_info_.app_url); callback_.Run(extension, web_app_info_); break; } case chrome::NOTIFICATION_EXTENSION_INSTALL_ERROR: callback_.Run(NULL, web_app_info_); break; default: NOTREACHED(); break; } } void CreateOrUpdateBookmarkApp(ExtensionService* service, WebApplicationInfo& web_app_info) { scoped_refptr<extensions::CrxInstaller> installer( extensions::CrxInstaller::CreateSilent(service)); installer->set_error_on_unsupported_requirements(true); installer->InstallWebApp(web_app_info); } void GetWebApplicationInfoFromApp( content::BrowserContext* browser_context, const extensions::Extension* extension, const base::Callback<void(const WebApplicationInfo&)> callback) { if (!extension->from_bookmark()) { callback.Run(WebApplicationInfo()); return; } WebApplicationInfo web_app_info; web_app_info.app_url = AppLaunchInfo::GetLaunchWebURL(extension); web_app_info.title = base::UTF8ToUTF16(extension->non_localized_name()); web_app_info.description = base::UTF8ToUTF16(extension->description()); std::vector<extensions::ImageLoader::ImageRepresentation> info_list; for (size_t i = 0; i < extension_misc::kNumExtensionIconSizes; ++i) { int size = extension_misc::kExtensionIconSizes[i]; extensions::ExtensionResource resource = extensions::IconsInfo::GetIconResource( extension, size, ExtensionIconSet::MATCH_EXACTLY); if (!resource.empty()) { info_list.push_back(extensions::ImageLoader::ImageRepresentation( resource, extensions::ImageLoader::ImageRepresentation::ALWAYS_RESIZE, gfx::Size(size, size), ui::SCALE_FACTOR_100P)); } } extensions::ImageLoader::Get(browser_context)->LoadImageFamilyAsync( extension, info_list, base::Bind(&OnIconsLoaded, web_app_info, callback)); } bool IsValidBookmarkAppUrl(const GURL& url) { URLPattern origin_only_pattern(Extension::kValidWebExtentSchemes); origin_only_pattern.SetMatchAllURLs(true); return url.is_valid() && origin_only_pattern.MatchesURL(url); } } // namespace extensions