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