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
41namespace {
42
43// Overlays a shortcut icon over the bottom left corner of a given image.
44class 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
99void 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
117namespace extensions {
118
119// static
120std::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
156void 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
170BookmarkAppHelper::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
205BookmarkAppHelper::~BookmarkAppHelper() {}
206
207void 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
216void 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
328void 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
350void 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
358void 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
391bool 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