bookmark_app_helper.cc revision 5f1c94371a64b3196d4be9466099bb892df9b88e
1effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch// Copyright 2014 The Chromium Authors. All rights reserved.
2effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch// Use of this source code is governed by a BSD-style license that can be
3effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch// found in the LICENSE file.
4effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch
5effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch#include "chrome/browser/extensions/bookmark_app_helper.h"
6effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch
7cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)#include <cctype>
8cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)
9effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch#include "base/strings/utf_string_conversions.h"
10effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch#include "chrome/browser/extensions/crx_installer.h"
11effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch#include "chrome/browser/extensions/extension_service.h"
12effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch#include "chrome/browser/extensions/favicon_downloader.h"
13effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch#include "chrome/browser/extensions/tab_helper.h"
14effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch#include "chrome/common/extensions/extension_constants.h"
15effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch#include "chrome/common/extensions/manifest_handlers/app_launch_info.h"
16effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch#include "content/public/browser/notification_service.h"
17effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch#include "content/public/browser/notification_source.h"
18effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch#include "content/public/browser/web_contents.h"
196d86b77056ed63eb6871182f42a9fd5f07550f90Torne (Richard Coles)#include "extensions/browser/image_loader.h"
205f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)#include "extensions/browser/notification_types.h"
21cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)#include "extensions/common/constants.h"
22effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch#include "extensions/common/extension.h"
23c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch#include "extensions/common/manifest_handlers/icons_handler.h"
24c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch#include "extensions/common/url_pattern.h"
25cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)#include "grit/platform_locale_settings.h"
26cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
27effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch#include "skia/ext/image_operations.h"
28effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch#include "skia/ext/platform_canvas.h"
29effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch#include "third_party/skia/include/core/SkBitmap.h"
30cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)#include "ui/base/l10n/l10n_util.h"
31cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)#include "ui/base/resource/resource_bundle.h"
32cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)#include "ui/gfx/canvas.h"
33effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch#include "ui/gfx/color_analysis.h"
34cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)#include "ui/gfx/color_utils.h"
35cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)#include "ui/gfx/font.h"
36cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)#include "ui/gfx/font_list.h"
37cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)#include "ui/gfx/image/canvas_image_source.h"
38effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch#include "ui/gfx/image/image.h"
395c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu#include "ui/gfx/image/image_family.h"
40cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)#include "ui/gfx/rect.h"
415c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu
425c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liunamespace {
435c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu
44cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)// Overlays a shortcut icon over the bottom left corner of a given image.
45cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)class GeneratedIconImageSource : public gfx::CanvasImageSource {
46cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles) public:
47cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)  explicit GeneratedIconImageSource(char letter, SkColor color, int output_size)
48cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)      : gfx::CanvasImageSource(gfx::Size(output_size, output_size), false),
49cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)        letter_(letter),
50cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)        color_(color),
51cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)        output_size_(output_size) {}
52cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)  virtual ~GeneratedIconImageSource() {}
53cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)
54cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles) private:
55cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)  // gfx::CanvasImageSource overrides:
56cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)  virtual void Draw(gfx::Canvas* canvas) OVERRIDE {
57cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)    const unsigned char kLuminanceThreshold = 190;
58cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)    const int icon_size = output_size_ * 3 / 4;
59cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)    const int icon_inset = output_size_ / 8;
60cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)    const size_t border_radius = output_size_ / 16;
61cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)    const size_t font_size = output_size_ * 7 / 16;
62cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)
63cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)    std::string font_name =
64cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)        l10n_util::GetStringUTF8(IDS_SANS_SERIF_FONT_FAMILY);
65cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)#if defined(OS_CHROMEOS)
66cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)    const std::string kChromeOSFontFamily = "Noto Sans";
67cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)    font_name = kChromeOSFontFamily;
68cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)#endif
69cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)
70cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)    // Draw a rounded rect of the given |color|.
71cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)    SkPaint background_paint;
72cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)    background_paint.setFlags(SkPaint::kAntiAlias_Flag);
73cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)    background_paint.setColor(color_);
74cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)
75cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)    gfx::Rect icon_rect(icon_inset, icon_inset, icon_size, icon_size);
76cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)    canvas->DrawRoundRect(icon_rect, border_radius, background_paint);
77cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)
78cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)    // The text rect's size needs to be odd to center the text correctly.
79cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)    gfx::Rect text_rect(icon_inset, icon_inset, icon_size + 1, icon_size + 1);
80cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)    // Draw the letter onto the rounded rect. The letter's color depends on the
81cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)    // luminance of |color|.
82cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)    unsigned char luminance = color_utils::GetLuminanceForColor(color_);
83cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)    canvas->DrawStringRectWithFlags(
84cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)        base::string16(1, std::toupper(letter_)),
85cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)        gfx::FontList(gfx::Font(font_name, font_size)),
86cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)        luminance > kLuminanceThreshold ? SK_ColorBLACK : SK_ColorWHITE,
87cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)        text_rect,
88cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)        gfx::Canvas::TEXT_ALIGN_CENTER);
89cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)  }
90cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)
91cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)  char letter_;
92cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)
93cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)  SkColor color_;
94cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)
95cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)  int output_size_;
96cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)
97cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)  DISALLOW_COPY_AND_ASSIGN(GeneratedIconImageSource);
98cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)};
99cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)
1005c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liuvoid OnIconsLoaded(
1015c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu    WebApplicationInfo web_app_info,
1025c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu    const base::Callback<void(const WebApplicationInfo&)> callback,
1035c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu    const gfx::ImageFamily& image_family) {
1045c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu  for (gfx::ImageFamily::const_iterator it = image_family.begin();
1055c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu       it != image_family.end();
1065c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu       ++it) {
1075c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu    WebApplicationInfo::IconInfo icon_info;
1085c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu    icon_info.data = *it->ToSkBitmap();
1095c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu    icon_info.width = icon_info.data.width();
1105c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu    icon_info.height = icon_info.data.height();
1115c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu    web_app_info.icons.push_back(icon_info);
1125c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu  }
1135c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu  callback.Run(web_app_info);
1145c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu}
1155c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu
1165c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu}  // namespace
117effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch
118effb81e5f8246d0db0270817048dc992db66e9fbBen Murdochnamespace extensions {
119effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch
120effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch// static
121effb81e5f8246d0db0270817048dc992db66e9fbBen Murdochstd::map<int, SkBitmap> BookmarkAppHelper::ConstrainBitmapsToSizes(
122effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch    const std::vector<SkBitmap>& bitmaps,
123effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch    const std::set<int>& sizes) {
124effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  std::map<int, SkBitmap> output_bitmaps;
125effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  std::map<int, SkBitmap> ordered_bitmaps;
126effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  for (std::vector<SkBitmap>::const_iterator it = bitmaps.begin();
127effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch       it != bitmaps.end();
128effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch       ++it) {
129effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch    DCHECK(it->width() == it->height());
130effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch    ordered_bitmaps[it->width()] = *it;
131effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  }
132effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch
133effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  std::set<int>::const_iterator sizes_it = sizes.begin();
134effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  std::map<int, SkBitmap>::const_iterator bitmaps_it = ordered_bitmaps.begin();
135effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  while (sizes_it != sizes.end() && bitmaps_it != ordered_bitmaps.end()) {
136effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch    int size = *sizes_it;
137effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch    // Find the closest not-smaller bitmap.
138effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch    bitmaps_it = ordered_bitmaps.lower_bound(size);
139effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch    ++sizes_it;
140effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch    // Ensure the bitmap is valid and smaller than the next allowed size.
141effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch    if (bitmaps_it != ordered_bitmaps.end() &&
142effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch        (sizes_it == sizes.end() || bitmaps_it->second.width() < *sizes_it)) {
143effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch      // Resize the bitmap if it does not exactly match the desired size.
144effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch      output_bitmaps[size] = bitmaps_it->second.width() == size
145effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch                                 ? bitmaps_it->second
146effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch                                 : skia::ImageOperations::Resize(
147effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch                                       bitmaps_it->second,
148effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch                                       skia::ImageOperations::RESIZE_LANCZOS3,
149effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch                                       size,
150effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch                                       size);
151effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch    }
152effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  }
153effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  return output_bitmaps;
154effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch}
155effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch
156effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch// static
157cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)void BookmarkAppHelper::GenerateIcon(std::map<int, SkBitmap>* bitmaps,
158cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)                                     int output_size,
159cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)                                     SkColor color,
160cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)                                     char letter) {
161cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)  // Do nothing if there is already an icon of |output_size|.
162cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)  if (bitmaps->count(output_size))
163effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch    return;
164effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch
165cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)  gfx::ImageSkia icon_image(
166cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)      new GeneratedIconImageSource(letter, color, output_size),
167cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)      gfx::Size(output_size, output_size));
168cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)  icon_image.bitmap()->deepCopyTo(&(*bitmaps)[output_size]);
169effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch}
170effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch
171effb81e5f8246d0db0270817048dc992db66e9fbBen MurdochBookmarkAppHelper::BookmarkAppHelper(ExtensionService* service,
172effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch                                     WebApplicationInfo web_app_info,
173effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch                                     content::WebContents* contents)
174effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch    : web_app_info_(web_app_info),
175effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch      crx_installer_(extensions::CrxInstaller::CreateSilent(service)) {
176effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  registrar_.Add(this,
1775f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)                 extensions::NOTIFICATION_CRX_INSTALLER_DONE,
178effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch                 content::Source<CrxInstaller>(crx_installer_.get()));
179effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch
180effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  registrar_.Add(this,
1815f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)                 extensions::NOTIFICATION_EXTENSION_INSTALL_ERROR,
182effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch                 content::Source<CrxInstaller>(crx_installer_.get()));
183effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch
184effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  crx_installer_->set_error_on_unsupported_requirements(true);
185effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch
18646d4c2bc3267f3f028f39e7e311b0f89aba2e4fdTorne (Richard Coles)  if (!contents)
18746d4c2bc3267f3f028f39e7e311b0f89aba2e4fdTorne (Richard Coles)    return;
18846d4c2bc3267f3f028f39e7e311b0f89aba2e4fdTorne (Richard Coles)
189effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  // Add urls from the WebApplicationInfo.
190effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  std::vector<GURL> web_app_info_icon_urls;
191effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  for (std::vector<WebApplicationInfo::IconInfo>::const_iterator it =
192effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch           web_app_info_.icons.begin();
193effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch       it != web_app_info_.icons.end();
194effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch       ++it) {
195effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch    if (it->url.is_valid())
196effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch      web_app_info_icon_urls.push_back(it->url);
197effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  }
198effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch
199effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  favicon_downloader_.reset(
200effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch      new FaviconDownloader(contents,
201effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch                            web_app_info_icon_urls,
202effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch                            base::Bind(&BookmarkAppHelper::OnIconsDownloaded,
203effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch                                       base::Unretained(this))));
204effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch}
205effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch
206effb81e5f8246d0db0270817048dc992db66e9fbBen MurdochBookmarkAppHelper::~BookmarkAppHelper() {}
207effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch
208effb81e5f8246d0db0270817048dc992db66e9fbBen Murdochvoid BookmarkAppHelper::Create(const CreateBookmarkAppCallback& callback) {
209effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  callback_ = callback;
21046d4c2bc3267f3f028f39e7e311b0f89aba2e4fdTorne (Richard Coles)
21146d4c2bc3267f3f028f39e7e311b0f89aba2e4fdTorne (Richard Coles)  if (favicon_downloader_.get())
21246d4c2bc3267f3f028f39e7e311b0f89aba2e4fdTorne (Richard Coles)    favicon_downloader_->Start();
21346d4c2bc3267f3f028f39e7e311b0f89aba2e4fdTorne (Richard Coles)  else
21446d4c2bc3267f3f028f39e7e311b0f89aba2e4fdTorne (Richard Coles)    OnIconsDownloaded(true, std::map<GURL, std::vector<SkBitmap> >());
215effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch}
216effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch
217effb81e5f8246d0db0270817048dc992db66e9fbBen Murdochvoid BookmarkAppHelper::OnIconsDownloaded(
218effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch    bool success,
219effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch    const std::map<GURL, std::vector<SkBitmap> >& bitmaps) {
220effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  // The tab has navigated away during the icon download. Cancel the bookmark
221effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  // app creation.
222effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  if (!success) {
223effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch    favicon_downloader_.reset();
224effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch    callback_.Run(NULL, web_app_info_);
225effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch    return;
226effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  }
227effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch
228effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  // Add the downloaded icons. Extensions only allow certain icon sizes. First
229effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  // populate icons that match the allowed sizes exactly and then downscale
230effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  // remaining icons to the closest allowed size that doesn't yet have an icon.
231effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  std::set<int> allowed_sizes(extension_misc::kExtensionIconSizes,
232effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch                              extension_misc::kExtensionIconSizes +
233effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch                                  extension_misc::kNumExtensionIconSizes);
234effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  std::vector<SkBitmap> downloaded_icons;
235effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  for (FaviconDownloader::FaviconMap::const_iterator map_it = bitmaps.begin();
236effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch       map_it != bitmaps.end();
237effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch       ++map_it) {
238effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch    for (std::vector<SkBitmap>::const_iterator bitmap_it =
239effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch             map_it->second.begin();
240effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch         bitmap_it != map_it->second.end();
241effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch         ++bitmap_it) {
242effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch      if (bitmap_it->empty() || bitmap_it->width() != bitmap_it->height())
243effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch        continue;
244effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch
245effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch      downloaded_icons.push_back(*bitmap_it);
246effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch    }
247effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  }
248effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch
24946d4c2bc3267f3f028f39e7e311b0f89aba2e4fdTorne (Richard Coles)  // Add all existing icons from WebApplicationInfo.
25046d4c2bc3267f3f028f39e7e311b0f89aba2e4fdTorne (Richard Coles)  for (std::vector<WebApplicationInfo::IconInfo>::const_iterator it =
25146d4c2bc3267f3f028f39e7e311b0f89aba2e4fdTorne (Richard Coles)           web_app_info_.icons.begin();
25246d4c2bc3267f3f028f39e7e311b0f89aba2e4fdTorne (Richard Coles)       it != web_app_info_.icons.end();
25346d4c2bc3267f3f028f39e7e311b0f89aba2e4fdTorne (Richard Coles)       ++it) {
25446d4c2bc3267f3f028f39e7e311b0f89aba2e4fdTorne (Richard Coles)    const SkBitmap& icon = it->data;
25546d4c2bc3267f3f028f39e7e311b0f89aba2e4fdTorne (Richard Coles)    if (!icon.drawsNothing() && icon.width() == icon.height())
25646d4c2bc3267f3f028f39e7e311b0f89aba2e4fdTorne (Richard Coles)      downloaded_icons.push_back(icon);
25746d4c2bc3267f3f028f39e7e311b0f89aba2e4fdTorne (Richard Coles)  }
25846d4c2bc3267f3f028f39e7e311b0f89aba2e4fdTorne (Richard Coles)
25946d4c2bc3267f3f028f39e7e311b0f89aba2e4fdTorne (Richard Coles)  web_app_info_.icons.clear();
26046d4c2bc3267f3f028f39e7e311b0f89aba2e4fdTorne (Richard Coles)
261effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  // If there are icons that don't match the accepted icon sizes, find the
262effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  // closest bigger icon to the accepted sizes and resize the icon to it. An
263effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  // icon will be resized and used for at most one size.
264effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  std::map<int, SkBitmap> resized_bitmaps(
265effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch      ConstrainBitmapsToSizes(downloaded_icons, allowed_sizes));
266effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch
267effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  // Generate container icons from smaller icons.
268effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  const int kIconSizesToGenerate[] = {extension_misc::EXTENSION_ICON_SMALL,
269effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch                                      extension_misc::EXTENSION_ICON_MEDIUM, };
270effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  const std::set<int> generate_sizes(
271effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch      kIconSizesToGenerate,
272effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch      kIconSizesToGenerate + arraysize(kIconSizesToGenerate));
273effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch
274effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  // Only generate icons if larger icons don't exist. This means the app
275effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  // launcher and the taskbar will do their best downsizing large icons and
276cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)  // these icons are only generated as a last resort against upscaling a smaller
277cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)  // icon.
278effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  if (resized_bitmaps.lower_bound(*generate_sizes.rbegin()) ==
279effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch      resized_bitmaps.end()) {
280cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)    GURL app_url = web_app_info_.app_url;
281cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)
282cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)    // The letter that will be painted on the generated icon.
283cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)    char icon_letter = ' ';
284cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)    std::string domain_and_registry(
285cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)        net::registry_controlled_domains::GetDomainAndRegistry(
286cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)            app_url,
287cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)            net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES));
288cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)    if (!domain_and_registry.empty()) {
289cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)      icon_letter = domain_and_registry[0];
290cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)    } else if (!app_url.host().empty()) {
291cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)      icon_letter = app_url.host()[0];
292cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)    }
293cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)
294cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)    // The color that will be used for the icon's background.
295cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)    SkColor background_color = SK_ColorBLACK;
296cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)    if (resized_bitmaps.size()) {
297cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)      color_utils::GridSampler sampler;
29846d4c2bc3267f3f028f39e7e311b0f89aba2e4fdTorne (Richard Coles)      background_color = color_utils::CalculateKMeanColorOfBitmap(
29946d4c2bc3267f3f028f39e7e311b0f89aba2e4fdTorne (Richard Coles)          resized_bitmaps.begin()->second);
300cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)    }
301cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)
302cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)    for (std::set<int>::const_iterator it = generate_sizes.begin();
303cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)         it != generate_sizes.end();
304effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch         ++it) {
305cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)      GenerateIcon(&resized_bitmaps, *it, background_color, icon_letter);
306cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)      // Also generate the 2x resource for this size.
307cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)      GenerateIcon(&resized_bitmaps, *it * 2, background_color, icon_letter);
308effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch    }
309effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  }
310effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch
311effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  // Populate the icon data into the WebApplicationInfo we are using to
312effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  // install the bookmark app.
313effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  for (std::map<int, SkBitmap>::const_iterator resized_bitmaps_it =
314effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch           resized_bitmaps.begin();
315effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch       resized_bitmaps_it != resized_bitmaps.end();
316effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch       ++resized_bitmaps_it) {
317effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch    WebApplicationInfo::IconInfo icon_info;
318effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch    icon_info.data = resized_bitmaps_it->second;
319effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch    icon_info.width = icon_info.data.width();
320effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch    icon_info.height = icon_info.data.height();
321effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch    web_app_info_.icons.push_back(icon_info);
322effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  }
323effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch
324effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  // Install the app.
325effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  crx_installer_->InstallWebApp(web_app_info_);
326effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  favicon_downloader_.reset();
327effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch}
328effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch
329effb81e5f8246d0db0270817048dc992db66e9fbBen Murdochvoid BookmarkAppHelper::Observe(int type,
330effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch                                const content::NotificationSource& source,
331effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch                                const content::NotificationDetails& details) {
332effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  switch (type) {
3335f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    case extensions::NOTIFICATION_CRX_INSTALLER_DONE: {
334effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch      const Extension* extension =
335effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch          content::Details<const Extension>(details).ptr();
336effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch      DCHECK(extension);
337effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch      DCHECK_EQ(AppLaunchInfo::GetLaunchWebURL(extension),
338effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch                web_app_info_.app_url);
339effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch      callback_.Run(extension, web_app_info_);
340effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch      break;
341effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch    }
3425f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    case extensions::NOTIFICATION_EXTENSION_INSTALL_ERROR:
343effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch      callback_.Run(NULL, web_app_info_);
344effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch      break;
345effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch    default:
346effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch      NOTREACHED();
347effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch      break;
348effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  }
349effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch}
350effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch
351e5d81f57cb97b3b6b7fccc9c5610d21eb81db09dBen Murdochvoid CreateOrUpdateBookmarkApp(ExtensionService* service,
352e5d81f57cb97b3b6b7fccc9c5610d21eb81db09dBen Murdoch                               WebApplicationInfo& web_app_info) {
353e5d81f57cb97b3b6b7fccc9c5610d21eb81db09dBen Murdoch  scoped_refptr<extensions::CrxInstaller> installer(
354e5d81f57cb97b3b6b7fccc9c5610d21eb81db09dBen Murdoch      extensions::CrxInstaller::CreateSilent(service));
355e5d81f57cb97b3b6b7fccc9c5610d21eb81db09dBen Murdoch  installer->set_error_on_unsupported_requirements(true);
356e5d81f57cb97b3b6b7fccc9c5610d21eb81db09dBen Murdoch  installer->InstallWebApp(web_app_info);
357e5d81f57cb97b3b6b7fccc9c5610d21eb81db09dBen Murdoch}
358e5d81f57cb97b3b6b7fccc9c5610d21eb81db09dBen Murdoch
3595c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liuvoid GetWebApplicationInfoFromApp(
3605c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu    content::BrowserContext* browser_context,
3615c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu    const extensions::Extension* extension,
3625c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu    const base::Callback<void(const WebApplicationInfo&)> callback) {
3635c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu  if (!extension->from_bookmark()) {
3645c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu    callback.Run(WebApplicationInfo());
3655c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu    return;
3665c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu  }
3675c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu
3685c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu  WebApplicationInfo web_app_info;
3695c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu  web_app_info.app_url = AppLaunchInfo::GetLaunchWebURL(extension);
3705c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu  web_app_info.title = base::UTF8ToUTF16(extension->non_localized_name());
3715c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu  web_app_info.description = base::UTF8ToUTF16(extension->description());
3725c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu
3735c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu  std::vector<extensions::ImageLoader::ImageRepresentation> info_list;
3745c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu  for (size_t i = 0; i < extension_misc::kNumExtensionIconSizes; ++i) {
3755c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu    int size = extension_misc::kExtensionIconSizes[i];
3765c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu    extensions::ExtensionResource resource =
3775c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu        extensions::IconsInfo::GetIconResource(
3785c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu            extension, size, ExtensionIconSet::MATCH_EXACTLY);
3795c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu    if (!resource.empty()) {
3805c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu      info_list.push_back(extensions::ImageLoader::ImageRepresentation(
3815c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu          resource,
3825c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu          extensions::ImageLoader::ImageRepresentation::ALWAYS_RESIZE,
3835c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu          gfx::Size(size, size),
3845c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu          ui::SCALE_FACTOR_100P));
3855c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu    }
3865c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu  }
3875c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu
3885c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu  extensions::ImageLoader::Get(browser_context)->LoadImageFamilyAsync(
3895c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu      extension, info_list, base::Bind(&OnIconsLoaded, web_app_info, callback));
3905c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu}
3915c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu
392c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdochbool IsValidBookmarkAppUrl(const GURL& url) {
393c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  URLPattern origin_only_pattern(Extension::kValidWebExtentSchemes);
394c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  origin_only_pattern.SetMatchAllURLs(true);
395c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  return url.is_valid() && origin_only_pattern.MatchesURL(url);
396c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch}
397c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
398effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch}  // namespace extensions
399