1 // Copyright 2014 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/extensions/bookmark_app_helper.h" 6 7 #include <cctype> 8 9 #include "base/strings/utf_string_conversions.h" 10 #include "chrome/browser/extensions/crx_installer.h" 11 #include "chrome/browser/extensions/extension_service.h" 12 #include "chrome/browser/extensions/favicon_downloader.h" 13 #include "chrome/browser/extensions/tab_helper.h" 14 #include "chrome/common/extensions/extension_constants.h" 15 #include "chrome/common/extensions/manifest_handlers/app_launch_info.h" 16 #include "content/public/browser/notification_service.h" 17 #include "content/public/browser/notification_source.h" 18 #include "content/public/browser/web_contents.h" 19 #include "extensions/browser/image_loader.h" 20 #include "extensions/browser/notification_types.h" 21 #include "extensions/common/constants.h" 22 #include "extensions/common/extension.h" 23 #include "extensions/common/manifest_handlers/icons_handler.h" 24 #include "extensions/common/url_pattern.h" 25 #include "grit/platform_locale_settings.h" 26 #include "net/base/registry_controlled_domains/registry_controlled_domain.h" 27 #include "skia/ext/image_operations.h" 28 #include "skia/ext/platform_canvas.h" 29 #include "third_party/skia/include/core/SkBitmap.h" 30 #include "ui/base/l10n/l10n_util.h" 31 #include "ui/gfx/canvas.h" 32 #include "ui/gfx/color_analysis.h" 33 #include "ui/gfx/color_utils.h" 34 #include "ui/gfx/font.h" 35 #include "ui/gfx/font_list.h" 36 #include "ui/gfx/image/canvas_image_source.h" 37 #include "ui/gfx/image/image.h" 38 #include "ui/gfx/image/image_family.h" 39 #include "ui/gfx/rect.h" 40 41 namespace { 42 43 // Overlays a shortcut icon over the bottom left corner of a given image. 44 class GeneratedIconImageSource : public gfx::CanvasImageSource { 45 public: 46 explicit GeneratedIconImageSource(char letter, SkColor color, int output_size) 47 : gfx::CanvasImageSource(gfx::Size(output_size, output_size), false), 48 letter_(letter), 49 color_(color), 50 output_size_(output_size) {} 51 virtual ~GeneratedIconImageSource() {} 52 53 private: 54 // gfx::CanvasImageSource overrides: 55 virtual void Draw(gfx::Canvas* canvas) OVERRIDE { 56 const unsigned char kLuminanceThreshold = 190; 57 const int icon_size = output_size_ * 3 / 4; 58 const int icon_inset = output_size_ / 8; 59 const size_t border_radius = output_size_ / 16; 60 const size_t font_size = output_size_ * 7 / 16; 61 62 std::string font_name = 63 l10n_util::GetStringUTF8(IDS_SANS_SERIF_FONT_FAMILY); 64 #if defined(OS_CHROMEOS) 65 const std::string kChromeOSFontFamily = "Noto Sans"; 66 font_name = kChromeOSFontFamily; 67 #endif 68 69 // Draw a rounded rect of the given |color|. 70 SkPaint background_paint; 71 background_paint.setFlags(SkPaint::kAntiAlias_Flag); 72 background_paint.setColor(color_); 73 74 gfx::Rect icon_rect(icon_inset, icon_inset, icon_size, icon_size); 75 canvas->DrawRoundRect(icon_rect, border_radius, background_paint); 76 77 // The text rect's size needs to be odd to center the text correctly. 78 gfx::Rect text_rect(icon_inset, icon_inset, icon_size + 1, icon_size + 1); 79 // Draw the letter onto the rounded rect. The letter's color depends on the 80 // luminance of |color|. 81 unsigned char luminance = color_utils::GetLuminanceForColor(color_); 82 canvas->DrawStringRectWithFlags( 83 base::string16(1, std::toupper(letter_)), 84 gfx::FontList(gfx::Font(font_name, font_size)), 85 luminance > kLuminanceThreshold ? SK_ColorBLACK : SK_ColorWHITE, 86 text_rect, 87 gfx::Canvas::TEXT_ALIGN_CENTER); 88 } 89 90 char letter_; 91 92 SkColor color_; 93 94 int output_size_; 95 96 DISALLOW_COPY_AND_ASSIGN(GeneratedIconImageSource); 97 }; 98 99 void OnIconsLoaded( 100 WebApplicationInfo web_app_info, 101 const base::Callback<void(const WebApplicationInfo&)> callback, 102 const gfx::ImageFamily& image_family) { 103 for (gfx::ImageFamily::const_iterator it = image_family.begin(); 104 it != image_family.end(); 105 ++it) { 106 WebApplicationInfo::IconInfo icon_info; 107 icon_info.data = *it->ToSkBitmap(); 108 icon_info.width = icon_info.data.width(); 109 icon_info.height = icon_info.data.height(); 110 web_app_info.icons.push_back(icon_info); 111 } 112 callback.Run(web_app_info); 113 } 114 115 } // namespace 116 117 namespace extensions { 118 119 // static 120 std::map<int, SkBitmap> BookmarkAppHelper::ConstrainBitmapsToSizes( 121 const std::vector<SkBitmap>& bitmaps, 122 const std::set<int>& sizes) { 123 std::map<int, SkBitmap> output_bitmaps; 124 std::map<int, SkBitmap> ordered_bitmaps; 125 for (std::vector<SkBitmap>::const_iterator it = bitmaps.begin(); 126 it != bitmaps.end(); 127 ++it) { 128 DCHECK(it->width() == it->height()); 129 ordered_bitmaps[it->width()] = *it; 130 } 131 132 std::set<int>::const_iterator sizes_it = sizes.begin(); 133 std::map<int, SkBitmap>::const_iterator bitmaps_it = ordered_bitmaps.begin(); 134 while (sizes_it != sizes.end() && bitmaps_it != ordered_bitmaps.end()) { 135 int size = *sizes_it; 136 // Find the closest not-smaller bitmap. 137 bitmaps_it = ordered_bitmaps.lower_bound(size); 138 ++sizes_it; 139 // Ensure the bitmap is valid and smaller than the next allowed size. 140 if (bitmaps_it != ordered_bitmaps.end() && 141 (sizes_it == sizes.end() || bitmaps_it->second.width() < *sizes_it)) { 142 // Resize the bitmap if it does not exactly match the desired size. 143 output_bitmaps[size] = bitmaps_it->second.width() == size 144 ? bitmaps_it->second 145 : skia::ImageOperations::Resize( 146 bitmaps_it->second, 147 skia::ImageOperations::RESIZE_LANCZOS3, 148 size, 149 size); 150 } 151 } 152 return output_bitmaps; 153 } 154 155 // static 156 void BookmarkAppHelper::GenerateIcon(std::map<int, SkBitmap>* bitmaps, 157 int output_size, 158 SkColor color, 159 char letter) { 160 // Do nothing if there is already an icon of |output_size|. 161 if (bitmaps->count(output_size)) 162 return; 163 164 gfx::ImageSkia icon_image( 165 new GeneratedIconImageSource(letter, color, output_size), 166 gfx::Size(output_size, output_size)); 167 icon_image.bitmap()->deepCopyTo(&(*bitmaps)[output_size]); 168 } 169 170 BookmarkAppHelper::BookmarkAppHelper(ExtensionService* service, 171 WebApplicationInfo web_app_info, 172 content::WebContents* contents) 173 : web_app_info_(web_app_info), 174 crx_installer_(extensions::CrxInstaller::CreateSilent(service)) { 175 registrar_.Add(this, 176 extensions::NOTIFICATION_CRX_INSTALLER_DONE, 177 content::Source<CrxInstaller>(crx_installer_.get())); 178 179 registrar_.Add(this, 180 extensions::NOTIFICATION_EXTENSION_INSTALL_ERROR, 181 content::Source<CrxInstaller>(crx_installer_.get())); 182 183 crx_installer_->set_error_on_unsupported_requirements(true); 184 185 if (!contents) 186 return; 187 188 // Add urls from the WebApplicationInfo. 189 std::vector<GURL> web_app_info_icon_urls; 190 for (std::vector<WebApplicationInfo::IconInfo>::const_iterator it = 191 web_app_info_.icons.begin(); 192 it != web_app_info_.icons.end(); 193 ++it) { 194 if (it->url.is_valid()) 195 web_app_info_icon_urls.push_back(it->url); 196 } 197 198 favicon_downloader_.reset( 199 new FaviconDownloader(contents, 200 web_app_info_icon_urls, 201 base::Bind(&BookmarkAppHelper::OnIconsDownloaded, 202 base::Unretained(this)))); 203 } 204 205 BookmarkAppHelper::~BookmarkAppHelper() {} 206 207 void BookmarkAppHelper::Create(const CreateBookmarkAppCallback& callback) { 208 callback_ = callback; 209 210 if (favicon_downloader_.get()) 211 favicon_downloader_->Start(); 212 else 213 OnIconsDownloaded(true, std::map<GURL, std::vector<SkBitmap> >()); 214 } 215 216 void BookmarkAppHelper::OnIconsDownloaded( 217 bool success, 218 const std::map<GURL, std::vector<SkBitmap> >& bitmaps) { 219 // The tab has navigated away during the icon download. Cancel the bookmark 220 // app creation. 221 if (!success) { 222 favicon_downloader_.reset(); 223 callback_.Run(NULL, web_app_info_); 224 return; 225 } 226 227 // Add the downloaded icons. Extensions only allow certain icon sizes. First 228 // populate icons that match the allowed sizes exactly and then downscale 229 // remaining icons to the closest allowed size that doesn't yet have an icon. 230 std::set<int> allowed_sizes(extension_misc::kExtensionIconSizes, 231 extension_misc::kExtensionIconSizes + 232 extension_misc::kNumExtensionIconSizes); 233 std::vector<SkBitmap> downloaded_icons; 234 for (FaviconDownloader::FaviconMap::const_iterator map_it = bitmaps.begin(); 235 map_it != bitmaps.end(); 236 ++map_it) { 237 for (std::vector<SkBitmap>::const_iterator bitmap_it = 238 map_it->second.begin(); 239 bitmap_it != map_it->second.end(); 240 ++bitmap_it) { 241 if (bitmap_it->empty() || bitmap_it->width() != bitmap_it->height()) 242 continue; 243 244 downloaded_icons.push_back(*bitmap_it); 245 } 246 } 247 248 // Add all existing icons from WebApplicationInfo. 249 for (std::vector<WebApplicationInfo::IconInfo>::const_iterator it = 250 web_app_info_.icons.begin(); 251 it != web_app_info_.icons.end(); 252 ++it) { 253 const SkBitmap& icon = it->data; 254 if (!icon.drawsNothing() && icon.width() == icon.height()) 255 downloaded_icons.push_back(icon); 256 } 257 258 web_app_info_.icons.clear(); 259 260 // If there are icons that don't match the accepted icon sizes, find the 261 // closest bigger icon to the accepted sizes and resize the icon to it. An 262 // icon will be resized and used for at most one size. 263 std::map<int, SkBitmap> resized_bitmaps( 264 ConstrainBitmapsToSizes(downloaded_icons, allowed_sizes)); 265 266 // Generate container icons from smaller icons. 267 const int kIconSizesToGenerate[] = {extension_misc::EXTENSION_ICON_SMALL, 268 extension_misc::EXTENSION_ICON_MEDIUM, }; 269 const std::set<int> generate_sizes( 270 kIconSizesToGenerate, 271 kIconSizesToGenerate + arraysize(kIconSizesToGenerate)); 272 273 // Only generate icons if larger icons don't exist. This means the app 274 // launcher and the taskbar will do their best downsizing large icons and 275 // these icons are only generated as a last resort against upscaling a smaller 276 // icon. 277 if (resized_bitmaps.lower_bound(*generate_sizes.rbegin()) == 278 resized_bitmaps.end()) { 279 GURL app_url = web_app_info_.app_url; 280 281 // The letter that will be painted on the generated icon. 282 char icon_letter = ' '; 283 std::string domain_and_registry( 284 net::registry_controlled_domains::GetDomainAndRegistry( 285 app_url, 286 net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES)); 287 if (!domain_and_registry.empty()) { 288 icon_letter = domain_and_registry[0]; 289 } else if (!app_url.host().empty()) { 290 icon_letter = app_url.host()[0]; 291 } 292 293 // The color that will be used for the icon's background. 294 SkColor background_color = SK_ColorBLACK; 295 if (resized_bitmaps.size()) { 296 color_utils::GridSampler sampler; 297 background_color = color_utils::CalculateKMeanColorOfBitmap( 298 resized_bitmaps.begin()->second); 299 } 300 301 for (std::set<int>::const_iterator it = generate_sizes.begin(); 302 it != generate_sizes.end(); 303 ++it) { 304 GenerateIcon(&resized_bitmaps, *it, background_color, icon_letter); 305 // Also generate the 2x resource for this size. 306 GenerateIcon(&resized_bitmaps, *it * 2, background_color, icon_letter); 307 } 308 } 309 310 // Populate the icon data into the WebApplicationInfo we are using to 311 // install the bookmark app. 312 for (std::map<int, SkBitmap>::const_iterator resized_bitmaps_it = 313 resized_bitmaps.begin(); 314 resized_bitmaps_it != resized_bitmaps.end(); 315 ++resized_bitmaps_it) { 316 WebApplicationInfo::IconInfo icon_info; 317 icon_info.data = resized_bitmaps_it->second; 318 icon_info.width = icon_info.data.width(); 319 icon_info.height = icon_info.data.height(); 320 web_app_info_.icons.push_back(icon_info); 321 } 322 323 // Install the app. 324 crx_installer_->InstallWebApp(web_app_info_); 325 favicon_downloader_.reset(); 326 } 327 328 void BookmarkAppHelper::Observe(int type, 329 const content::NotificationSource& source, 330 const content::NotificationDetails& details) { 331 switch (type) { 332 case extensions::NOTIFICATION_CRX_INSTALLER_DONE: { 333 const Extension* extension = 334 content::Details<const Extension>(details).ptr(); 335 DCHECK(extension); 336 DCHECK_EQ(AppLaunchInfo::GetLaunchWebURL(extension), 337 web_app_info_.app_url); 338 callback_.Run(extension, web_app_info_); 339 break; 340 } 341 case extensions::NOTIFICATION_EXTENSION_INSTALL_ERROR: 342 callback_.Run(NULL, web_app_info_); 343 break; 344 default: 345 NOTREACHED(); 346 break; 347 } 348 } 349 350 void CreateOrUpdateBookmarkApp(ExtensionService* service, 351 WebApplicationInfo& web_app_info) { 352 scoped_refptr<extensions::CrxInstaller> installer( 353 extensions::CrxInstaller::CreateSilent(service)); 354 installer->set_error_on_unsupported_requirements(true); 355 installer->InstallWebApp(web_app_info); 356 } 357 358 void GetWebApplicationInfoFromApp( 359 content::BrowserContext* browser_context, 360 const extensions::Extension* extension, 361 const base::Callback<void(const WebApplicationInfo&)> callback) { 362 if (!extension->from_bookmark()) { 363 callback.Run(WebApplicationInfo()); 364 return; 365 } 366 367 WebApplicationInfo web_app_info; 368 web_app_info.app_url = AppLaunchInfo::GetLaunchWebURL(extension); 369 web_app_info.title = base::UTF8ToUTF16(extension->non_localized_name()); 370 web_app_info.description = base::UTF8ToUTF16(extension->description()); 371 372 std::vector<extensions::ImageLoader::ImageRepresentation> info_list; 373 for (size_t i = 0; i < extension_misc::kNumExtensionIconSizes; ++i) { 374 int size = extension_misc::kExtensionIconSizes[i]; 375 extensions::ExtensionResource resource = 376 extensions::IconsInfo::GetIconResource( 377 extension, size, ExtensionIconSet::MATCH_EXACTLY); 378 if (!resource.empty()) { 379 info_list.push_back(extensions::ImageLoader::ImageRepresentation( 380 resource, 381 extensions::ImageLoader::ImageRepresentation::ALWAYS_RESIZE, 382 gfx::Size(size, size), 383 ui::SCALE_FACTOR_100P)); 384 } 385 } 386 387 extensions::ImageLoader::Get(browser_context)->LoadImageFamilyAsync( 388 extension, info_list, base::Bind(&OnIconsLoaded, web_app_info, callback)); 389 } 390 391 bool IsValidBookmarkAppUrl(const GURL& url) { 392 URLPattern origin_only_pattern(Extension::kValidWebExtentSchemes); 393 origin_only_pattern.SetMatchAllURLs(true); 394 return url.is_valid() && origin_only_pattern.MatchesURL(url); 395 } 396 397 } // namespace extensions 398