// 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