// 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 #include "base/prefs/pref_service.h" #include "base/strings/utf_string_conversions.h" #include "chrome/browser/bitmap_fetcher/bitmap_fetcher.h" #include "chrome/browser/bitmap_fetcher/bitmap_fetcher_delegate.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/launch_util.h" #include "chrome/browser/extensions/tab_helper.h" #include "chrome/browser/profiles/profile.h" #include "chrome/browser/ui/app_list/app_list_service.h" #include "chrome/browser/ui/app_list/app_list_util.h" #include "chrome/browser/ui/browser_finder.h" #include "chrome/browser/ui/browser_window.h" #include "chrome/browser/ui/host_desktop.h" #include "chrome/browser/web_applications/web_app.h" #include "chrome/common/extensions/extension_constants.h" #include "chrome/common/extensions/manifest_handlers/app_launch_info.h" #include "chrome/common/url_constants.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/extension_system.h" #include "extensions/browser/image_loader.h" #include "extensions/browser/notification_types.h" #include "extensions/browser/pref_names.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/load_flags.h" #include "net/base/registry_controlled_domains/registry_controlled_domain.h" #include "net/url_request/url_request.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/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/geometry/rect.h" #include "ui/gfx/image/canvas_image_source.h" #include "ui/gfx/image/image.h" #include "ui/gfx/image/image_family.h" #if defined(OS_MACOSX) #include "base/command_line.h" #include "chrome/browser/web_applications/web_app_mac.h" #include "chrome/common/chrome_switches.h" #endif #if defined(USE_ASH) #include "chrome/browser/ui/ash/launcher/chrome_launcher_controller.h" #endif namespace { using extensions::BookmarkAppHelper; // 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) {} ~GeneratedIconImageSource() override {} private: // gfx::CanvasImageSource overrides: 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 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); } std::set SizesToGenerate() { // Generate container icons from smaller icons. const int kIconSizesToGenerate[] = { extension_misc::EXTENSION_ICON_SMALL, extension_misc::EXTENSION_ICON_MEDIUM, extension_misc::EXTENSION_ICON_LARGE, }; return std::set(kIconSizesToGenerate, kIconSizesToGenerate + arraysize(kIconSizesToGenerate)); } void GenerateIcons( std::set generate_sizes, const GURL& app_url, SkColor generated_icon_color, std::map* bitmap_map) { // 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]; } // If no color has been specified, use a dark gray so it will stand out on the // black shelf. if (generated_icon_color == SK_ColorTRANSPARENT) generated_icon_color = SK_ColorDKGRAY; for (std::set::const_iterator it = generate_sizes.begin(); it != generate_sizes.end(); ++it) { extensions::BookmarkAppHelper::GenerateIcon( bitmap_map, *it, generated_icon_color, icon_letter); // Also generate the 2x resource for this size. extensions::BookmarkAppHelper::GenerateIcon( bitmap_map, *it * 2, generated_icon_color, icon_letter); } } void ReplaceWebAppIcons( std::map bitmap_map, WebApplicationInfo* web_app_info) { web_app_info->icons.clear(); // Populate the icon data into the WebApplicationInfo we are using to // install the bookmark app. for (const auto& pair : bitmap_map) { WebApplicationInfo::IconInfo icon_info; icon_info.data = pair.second.bitmap; icon_info.url = pair.second.source_url; icon_info.width = icon_info.data.width(); icon_info.height = icon_info.data.height(); web_app_info->icons.push_back(icon_info); } } // Class to handle installing a bookmark app. Handles downloading and decoding // the icons. class BookmarkAppInstaller : public base::RefCounted, public chrome::BitmapFetcherDelegate { public: BookmarkAppInstaller(ExtensionService* service, const WebApplicationInfo& web_app_info) : service_(service), web_app_info_(web_app_info) {} void Run() { for (const auto& icon : web_app_info_.icons) { if (icon.url.is_valid()) urls_to_download_.push_back(icon.url); } if (urls_to_download_.size()) { DownloadNextImage(); // Matched in OnFetchComplete. AddRef(); return; } FinishInstallation(); } private: friend class base::RefCounted; ~BookmarkAppInstaller() override {} // BitmapFetcherDelegate: void OnFetchComplete(const GURL& url, const SkBitmap* bitmap) override { if (bitmap && !bitmap->empty() && bitmap->width() == bitmap->height()) { downloaded_bitmaps_.push_back( BookmarkAppHelper::BitmapAndSource(url, *bitmap)); } if (urls_to_download_.size()) { DownloadNextImage(); return; } FinishInstallation(); Release(); } void DownloadNextImage() { DCHECK(urls_to_download_.size()); bitmap_fetcher_.reset( new chrome::BitmapFetcher(urls_to_download_.back(), this)); urls_to_download_.pop_back(); bitmap_fetcher_->Init( service_->profile()->GetRequestContext(), std::string(), net::URLRequest::CLEAR_REFERRER_ON_TRANSITION_FROM_SECURE_TO_INSECURE, net::LOAD_DO_NOT_SAVE_COOKIES | net::LOAD_DO_NOT_SEND_COOKIES); bitmap_fetcher_->Start(); } void FinishInstallation() { std::map size_map = BookmarkAppHelper::ResizeIconsAndGenerateMissing(downloaded_bitmaps_, SizesToGenerate(), &web_app_info_); BookmarkAppHelper::UpdateWebAppIconsWithoutChangingLinks(size_map, &web_app_info_); scoped_refptr installer( extensions::CrxInstaller::CreateSilent(service_)); installer->set_error_on_unsupported_requirements(true); installer->InstallWebApp(web_app_info_); } ExtensionService* service_; WebApplicationInfo web_app_info_; scoped_ptr bitmap_fetcher_; std::vector urls_to_download_; std::vector downloaded_bitmaps_; }; } // namespace namespace extensions { // static void BookmarkAppHelper::UpdateWebAppInfoFromManifest( const content::Manifest& manifest, WebApplicationInfo* web_app_info) { if (!manifest.short_name.is_null()) web_app_info->title = manifest.short_name.string(); // Give the full length name priority. if (!manifest.name.is_null()) web_app_info->title = manifest.name.string(); // Set the url based on the manifest value, if any. if (manifest.start_url.is_valid()) web_app_info->app_url = manifest.start_url; // If any icons are specified in the manifest, they take precedence over any // we picked up from the web_app stuff. if (!manifest.icons.empty()) { web_app_info->icons.clear(); for (const auto& icon : manifest.icons) { // TODO(benwells): Take the declared icon density and sizes into account. WebApplicationInfo::IconInfo info; info.url = icon.src; web_app_info->icons.push_back(info); } } } // static std::map BookmarkAppHelper::ConstrainBitmapsToSizes( const std::vector& bitmaps, const std::set& sizes) { std::map output_bitmaps; std::map ordered_bitmaps; for (std::vector::const_iterator it = bitmaps.begin(); it != bitmaps.end(); ++it) { DCHECK(it->bitmap.width() == it->bitmap.height()); ordered_bitmaps[it->bitmap.width()] = *it; } std::set::const_iterator sizes_it = sizes.begin(); std::map::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.bitmap.width() < *sizes_it)) { output_bitmaps[size] = bitmaps_it->second; // Resize the bitmap if it does not exactly match the desired size. if (output_bitmaps[size].bitmap.width() != size) { output_bitmaps[size].bitmap = skia::ImageOperations::Resize( output_bitmaps[size].bitmap, skia::ImageOperations::RESIZE_LANCZOS3, size, size); } } } return output_bitmaps; } // static void BookmarkAppHelper::GenerateIcon( std::map* 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].bitmap); } // static std::map BookmarkAppHelper::ResizeIconsAndGenerateMissing( std::vector icons, std::set sizes_to_generate, WebApplicationInfo* web_app_info) { // 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 allowed_sizes(extension_misc::kExtensionIconSizes, extension_misc::kExtensionIconSizes + extension_misc::kNumExtensionIconSizes); // 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 resized_bitmaps( ConstrainBitmapsToSizes(icons, allowed_sizes)); // Determine the color that will be used for the icon's background. For this // the dominant color of the first icon found is used. if (resized_bitmaps.size()) { color_utils::GridSampler sampler; web_app_info->generated_icon_color = color_utils::CalculateKMeanColorOfBitmap( resized_bitmaps.begin()->second.bitmap); } // Work out what icons we need to generate here. Icons are only generated if: // a. there is no icon in the required size, AND // b. there is no icon LARGER than the required size. // Larger icons will be scaled down and used at display time. std::set generate_sizes; for (int size : sizes_to_generate) { if (resized_bitmaps.lower_bound(size) == resized_bitmaps.end()) generate_sizes.insert(size); } GenerateIcons(generate_sizes, web_app_info->app_url, web_app_info->generated_icon_color, &resized_bitmaps); return resized_bitmaps; } // static void BookmarkAppHelper::UpdateWebAppIconsWithoutChangingLinks( std::map bitmap_map, WebApplicationInfo* web_app_info) { // First add in the icon data that have urls with the url / size data from the // original web app info, and the data from the new icons (if any). for (auto& icon : web_app_info->icons) { if (!icon.url.is_empty() && icon.data.empty()) { const auto& it = bitmap_map.find(icon.width); if (it != bitmap_map.end() && it->second.source_url == icon.url) icon.data = it->second.bitmap; } } // Now add in any icons from the updated list that don't have URLs. for (const auto& pair : bitmap_map) { if (pair.second.source_url.is_empty()) { WebApplicationInfo::IconInfo icon_info; icon_info.data = pair.second.bitmap; icon_info.width = pair.first; icon_info.height = pair.first; web_app_info->icons.push_back(icon_info); } } } BookmarkAppHelper::BitmapAndSource::BitmapAndSource() { } BookmarkAppHelper::BitmapAndSource::BitmapAndSource(const GURL& source_url_p, const SkBitmap& bitmap_p) : source_url(source_url_p), bitmap(bitmap_p) { } BookmarkAppHelper::BitmapAndSource::~BitmapAndSource() { } BookmarkAppHelper::BookmarkAppHelper(Profile* profile, WebApplicationInfo web_app_info, content::WebContents* contents) : profile_(profile), contents_(contents), web_app_info_(web_app_info), crx_installer_(extensions::CrxInstaller::CreateSilent( ExtensionSystem::Get(profile)->extension_service())) { web_app_info_.open_as_window = profile_->GetPrefs()->GetInteger( extensions::pref_names::kBookmarkAppCreationLaunchType) == extensions::LAUNCH_TYPE_WINDOW; // The default app title is the page title, which can be quite long. Limit the // default name used to something sensible. const int kMaxDefaultTitle = 40; if (web_app_info_.title.length() > kMaxDefaultTitle) { web_app_info_.title = web_app_info_.title.substr(0, kMaxDefaultTitle - 3) + base::UTF8ToUTF16("..."); } registrar_.Add(this, extensions::NOTIFICATION_CRX_INSTALLER_DONE, content::Source(crx_installer_.get())); registrar_.Add(this, extensions::NOTIFICATION_EXTENSION_INSTALL_ERROR, content::Source(crx_installer_.get())); crx_installer_->set_error_on_unsupported_requirements(true); } BookmarkAppHelper::~BookmarkAppHelper() {} void BookmarkAppHelper::Create(const CreateBookmarkAppCallback& callback) { callback_ = callback; if (contents_) { contents_->GetManifest(base::Bind(&BookmarkAppHelper::OnDidGetManifest, base::Unretained(this))); } else { OnIconsDownloaded(true, std::map >()); } } void BookmarkAppHelper::CreateFromAppBanner( const CreateBookmarkAppCallback& callback, const content::Manifest& manifest) { DCHECK(!manifest.short_name.is_null() || !manifest.name.is_null()); DCHECK(manifest.start_url.is_valid()); callback_ = callback; OnDidGetManifest(manifest); } void BookmarkAppHelper::OnDidGetManifest(const content::Manifest& manifest) { if (contents_->IsBeingDestroyed()) return; UpdateWebAppInfoFromManifest(manifest, &web_app_info_); // Add urls from the WebApplicationInfo. std::vector web_app_info_icon_urls; for (std::vector::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)))); favicon_downloader_->Start(); } void BookmarkAppHelper::OnIconsDownloaded( bool success, const std::map >& bitmaps) { // The tab has navigated away during the icon download. Cancel the bookmark // app creation. if (!success) { favicon_downloader_.reset(); callback_.Run(nullptr, web_app_info_); return; } std::vector downloaded_icons; for (FaviconDownloader::FaviconMap::const_iterator map_it = bitmaps.begin(); map_it != bitmaps.end(); ++map_it) { for (std::vector::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(BitmapAndSource(map_it->first, *bitmap_it)); } } // Add all existing icons from WebApplicationInfo. for (std::vector::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(BitmapAndSource(it->url, icon)); } } web_app_info_.generated_icon_color = SK_ColorTRANSPARENT; std::map size_to_icons = ResizeIconsAndGenerateMissing(downloaded_icons, SizesToGenerate(), &web_app_info_); ReplaceWebAppIcons(size_to_icons, &web_app_info_); favicon_downloader_.reset(); if (!contents_) { // The web contents can be null in tests. OnBubbleCompleted(true, web_app_info_); return; } Browser* browser = chrome::FindBrowserWithWebContents(contents_); if (!browser) { // The browser can be null in tests. OnBubbleCompleted(true, web_app_info_); return; } browser->window()->ShowBookmarkAppBubble( web_app_info_, base::Bind(&BookmarkAppHelper::OnBubbleCompleted, base::Unretained(this))); } void BookmarkAppHelper::OnBubbleCompleted( bool user_accepted, const WebApplicationInfo& web_app_info) { if (user_accepted) { web_app_info_ = web_app_info; crx_installer_->InstallWebApp(web_app_info_); } else { callback_.Run(nullptr, web_app_info_); } } void BookmarkAppHelper::FinishInstallation(const Extension* extension) { // Set the default 'open as' preference for use next time the dialog is // shown. extensions::LaunchType launch_type = web_app_info_.open_as_window ? extensions::LAUNCH_TYPE_WINDOW : extensions::LAUNCH_TYPE_REGULAR; profile_->GetPrefs()->SetInteger( extensions::pref_names::kBookmarkAppCreationLaunchType, launch_type); // Set the launcher type for the app. extensions::SetLaunchType(profile_, extension->id(), launch_type); if (!contents_) { // The web contents can be null in tests. callback_.Run(extension, web_app_info_); return; } Browser* browser = chrome::FindBrowserWithWebContents(contents_); if (!browser) { // The browser can be null in tests. callback_.Run(extension, web_app_info_); return; } // Pin the app to the relevant launcher depending on the OS. Profile* current_profile = profile_->GetOriginalProfile(); // On Mac, shortcuts are automatically created for hosted apps when they are // installed, so there is no need to create them again. #if !defined(OS_MACOSX) chrome::HostDesktopType desktop = browser->host_desktop_type(); if (desktop != chrome::HOST_DESKTOP_TYPE_ASH) { web_app::ShortcutLocations creation_locations; #if defined(OS_LINUX) creation_locations.on_desktop = true; #else creation_locations.on_desktop = false; #endif creation_locations.applications_menu_location = web_app::APP_MENU_LOCATION_SUBDIR_CHROMEAPPS; web_app::CreateShortcuts(web_app::SHORTCUT_CREATION_BY_USER, creation_locations, current_profile, extension); // Creating shortcuts in the start menu fails when the language is set // to certain languages (e.g. Hindi). To work around this, the taskbar / // quick launch icon is created separately to ensure it doesn't fail // due to the start menu shortcut creation failing. // See http://crbug.com/477297 and http://crbug.com/484577. creation_locations.on_desktop = false; creation_locations.applications_menu_location = web_app::APP_MENU_LOCATION_NONE; creation_locations.in_quick_launch_bar = true; web_app::CreateShortcuts(web_app::SHORTCUT_CREATION_BY_USER, creation_locations, current_profile, extension); #if defined(USE_ASH) } else { ChromeLauncherController::instance()->PinAppWithID(extension->id()); #endif } #endif #if defined(OS_MACOSX) if (!base::CommandLine::ForCurrentProcess()->HasSwitch( switches::kDisableHostedAppShimCreation)) { web_app::RevealAppShimInFinderForApp(current_profile, extension); } #endif callback_.Run(extension, web_app_info_); } void BookmarkAppHelper::Observe(int type, const content::NotificationSource& source, const content::NotificationDetails& details) { switch (type) { case extensions::NOTIFICATION_CRX_INSTALLER_DONE: { const Extension* extension = content::Details(details).ptr(); DCHECK(extension); DCHECK_EQ(AppLaunchInfo::GetLaunchWebURL(extension), web_app_info_.app_url); FinishInstallation(extension); break; } case extensions::NOTIFICATION_EXTENSION_INSTALL_ERROR: callback_.Run(nullptr, web_app_info_); break; default: NOTREACHED(); break; } } void CreateOrUpdateBookmarkApp(ExtensionService* service, WebApplicationInfo* web_app_info) { scoped_refptr installer( new BookmarkAppInstaller(service, *web_app_info)); installer->Run(); } void GetWebApplicationInfoFromApp( content::BrowserContext* browser_context, const extensions::Extension* extension, const base::Callback 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 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