1// Copyright (c) 2012 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/extension_action.h"
6
7#include <algorithm>
8
9#include "base/base64.h"
10#include "base/bind.h"
11#include "base/logging.h"
12#include "base/message_loop/message_loop.h"
13#include "chrome/common/badge_util.h"
14#include "chrome/common/icon_with_badge_image_source.h"
15#include "extensions/common/constants.h"
16#include "grit/theme_resources.h"
17#include "grit/ui_resources.h"
18#include "ipc/ipc_message.h"
19#include "ipc/ipc_message_utils.h"
20#include "third_party/skia/include/core/SkBitmap.h"
21#include "third_party/skia/include/core/SkCanvas.h"
22#include "third_party/skia/include/core/SkPaint.h"
23#include "third_party/skia/include/effects/SkGradientShader.h"
24#include "ui/base/resource/resource_bundle.h"
25#include "ui/gfx/animation/animation_delegate.h"
26#include "ui/gfx/canvas.h"
27#include "ui/gfx/color_utils.h"
28#include "ui/gfx/image/image.h"
29#include "ui/gfx/image/image_skia.h"
30#include "ui/gfx/image/image_skia_source.h"
31#include "ui/gfx/ipc/gfx_param_traits.h"
32#include "ui/gfx/rect.h"
33#include "ui/gfx/size.h"
34#include "ui/gfx/skbitmap_operations.h"
35#include "url/gurl.h"
36
37namespace {
38
39class GetAttentionImageSource : public gfx::ImageSkiaSource {
40 public:
41  explicit GetAttentionImageSource(const gfx::ImageSkia& icon)
42      : icon_(icon) {}
43
44  // gfx::ImageSkiaSource overrides:
45  virtual gfx::ImageSkiaRep GetImageForScale(float scale) OVERRIDE {
46    gfx::ImageSkiaRep icon_rep = icon_.GetRepresentation(scale);
47    color_utils::HSL shift = {-1, 0, 0.5};
48    return gfx::ImageSkiaRep(
49        SkBitmapOperations::CreateHSLShiftedBitmap(icon_rep.sk_bitmap(), shift),
50        icon_rep.scale());
51  }
52
53 private:
54  const gfx::ImageSkia icon_;
55};
56
57struct IconRepresentationInfo {
58  // Size as a string that will be used to retrieve a representation value from
59  // SetIcon function arguments.
60  const char* size_string;
61  // Scale factor for which the represantion should be used.
62  ui::ScaleFactor scale;
63};
64
65const IconRepresentationInfo kIconSizes[] = {{"19", ui::SCALE_FACTOR_100P},
66                                             {"38", ui::SCALE_FACTOR_200P}};
67
68template <class T>
69bool HasValue(const std::map<int, T>& map, int tab_id) {
70  return map.find(tab_id) != map.end();
71}
72
73}  // namespace
74
75const int ExtensionAction::kDefaultTabId = -1;
76const int ExtensionAction::kPageActionIconMaxSize =
77    extension_misc::EXTENSION_ICON_ACTION;
78
79ExtensionAction::ExtensionAction(const std::string& extension_id,
80                                 extensions::ActionInfo::Type action_type,
81                                 const extensions::ActionInfo& manifest_data)
82    : extension_id_(extension_id), action_type_(action_type) {
83  // Page/script actions are hidden/disabled by default, and browser actions are
84  // visible/enabled by default.
85  SetIsVisible(kDefaultTabId,
86               action_type == extensions::ActionInfo::TYPE_BROWSER);
87  SetTitle(kDefaultTabId, manifest_data.default_title);
88  SetPopupUrl(kDefaultTabId, manifest_data.default_popup_url);
89  if (!manifest_data.default_icon.empty()) {
90    set_default_icon(make_scoped_ptr(new ExtensionIconSet(
91        manifest_data.default_icon)));
92  }
93  set_id(manifest_data.id);
94}
95
96ExtensionAction::~ExtensionAction() {
97}
98
99scoped_ptr<ExtensionAction> ExtensionAction::CopyForTest() const {
100  scoped_ptr<ExtensionAction> copy(
101      new ExtensionAction(extension_id_, action_type_,
102                          extensions::ActionInfo()));
103  copy->popup_url_ = popup_url_;
104  copy->title_ = title_;
105  copy->icon_ = icon_;
106  copy->badge_text_ = badge_text_;
107  copy->badge_background_color_ = badge_background_color_;
108  copy->badge_text_color_ = badge_text_color_;
109  copy->is_visible_ = is_visible_;
110  copy->id_ = id_;
111
112  if (default_icon_)
113    copy->default_icon_.reset(new ExtensionIconSet(*default_icon_));
114
115  return copy.Pass();
116}
117
118// static
119int ExtensionAction::GetIconSizeForType(
120    extensions::ActionInfo::Type type) {
121  switch (type) {
122    case extensions::ActionInfo::TYPE_BROWSER:
123    case extensions::ActionInfo::TYPE_PAGE:
124    case extensions::ActionInfo::TYPE_SYSTEM_INDICATOR:
125      // TODO(dewittj) Report the actual icon size of the system
126      // indicator.
127      return extension_misc::EXTENSION_ICON_ACTION;
128    default:
129      NOTREACHED();
130      return 0;
131  }
132}
133
134void ExtensionAction::SetPopupUrl(int tab_id, const GURL& url) {
135  // We store |url| even if it is empty, rather than removing a URL from the
136  // map.  If an extension has a default popup, and removes it for a tab via
137  // the API, we must remember that there is no popup for that specific tab.
138  // If we removed the tab's URL, GetPopupURL would incorrectly return the
139  // default URL.
140  SetValue(&popup_url_, tab_id, url);
141}
142
143bool ExtensionAction::HasPopup(int tab_id) const {
144  return !GetPopupUrl(tab_id).is_empty();
145}
146
147GURL ExtensionAction::GetPopupUrl(int tab_id) const {
148  return GetValue(&popup_url_, tab_id);
149}
150
151void ExtensionAction::SetIcon(int tab_id, const gfx::Image& image) {
152  SetValue(&icon_, tab_id, image.AsImageSkia());
153}
154
155bool ExtensionAction::ParseIconFromCanvasDictionary(
156    const base::DictionaryValue& dict,
157    gfx::ImageSkia* icon) {
158  // Try to extract an icon for each known scale.
159  for (size_t i = 0; i < arraysize(kIconSizes); i++) {
160    const base::BinaryValue* image_data;
161    std::string binary_string64;
162    IPC::Message pickle;
163    if (dict.GetBinary(kIconSizes[i].size_string, &image_data)) {
164      pickle = IPC::Message(image_data->GetBuffer(), image_data->GetSize());
165    } else if (dict.GetString(kIconSizes[i].size_string, &binary_string64)) {
166      std::string binary_string;
167      if (!base::Base64Decode(binary_string64, &binary_string))
168        return false;
169      pickle = IPC::Message(binary_string.c_str(), binary_string.length());
170    } else {
171      continue;
172    }
173    PickleIterator iter(pickle);
174    SkBitmap bitmap;
175    if (!IPC::ReadParam(&pickle, &iter, &bitmap))
176      return false;
177    CHECK(!bitmap.isNull());
178    float scale = ui::GetScaleForScaleFactor(kIconSizes[i].scale);
179    icon->AddRepresentation(gfx::ImageSkiaRep(bitmap, scale));
180  }
181  return true;
182}
183
184gfx::ImageSkia ExtensionAction::GetExplicitlySetIcon(int tab_id) const {
185  return GetValue(&icon_, tab_id);
186}
187
188bool ExtensionAction::SetIsVisible(int tab_id, bool new_visibility) {
189  const bool old_visibility = GetValue(&is_visible_, tab_id);
190
191  if (old_visibility == new_visibility)
192    return false;
193
194  SetValue(&is_visible_, tab_id, new_visibility);
195
196  return true;
197}
198
199void ExtensionAction::DeclarativeShow(int tab_id) {
200  DCHECK_NE(tab_id, kDefaultTabId);
201  ++declarative_show_count_[tab_id];  // Use default initialization to 0.
202}
203
204void ExtensionAction::UndoDeclarativeShow(int tab_id) {
205  int& show_count = declarative_show_count_[tab_id];
206  DCHECK_GT(show_count, 0);
207  if (--show_count == 0)
208    declarative_show_count_.erase(tab_id);
209}
210
211void ExtensionAction::DeclarativeSetIcon(int tab_id,
212                                         int priority,
213                                         const gfx::Image& icon) {
214  DCHECK_NE(tab_id, kDefaultTabId);
215  declarative_icon_[tab_id][priority].push_back(icon);
216}
217
218void ExtensionAction::UndoDeclarativeSetIcon(int tab_id,
219                                             int priority,
220                                             const gfx::Image& icon) {
221  std::vector<gfx::Image>& icons = declarative_icon_[tab_id][priority];
222  for (std::vector<gfx::Image>::iterator it = icons.begin(); it != icons.end();
223       ++it) {
224    if (it->AsImageSkia().BackedBySameObjectAs(icon.AsImageSkia())) {
225      icons.erase(it);
226      return;
227    }
228  }
229}
230
231const gfx::ImageSkia ExtensionAction::GetDeclarativeIcon(int tab_id) const {
232  if (declarative_icon_.find(tab_id) != declarative_icon_.end() &&
233      !declarative_icon_.find(tab_id)->second.rbegin()->second.empty()) {
234    return declarative_icon_.find(tab_id)->second.rbegin()
235        ->second.back().AsImageSkia();
236  }
237  return gfx::ImageSkia();
238}
239
240void ExtensionAction::ClearAllValuesForTab(int tab_id) {
241  popup_url_.erase(tab_id);
242  title_.erase(tab_id);
243  icon_.erase(tab_id);
244  badge_text_.erase(tab_id);
245  badge_text_color_.erase(tab_id);
246  badge_background_color_.erase(tab_id);
247  is_visible_.erase(tab_id);
248  // TODO(jyasskin): Erase the element from declarative_show_count_
249  // when the tab's closed.  There's a race between the
250  // LocationBarController and the ContentRulesRegistry on navigation,
251  // which prevents me from cleaning everything up now.
252}
253
254void ExtensionAction::PaintBadge(gfx::Canvas* canvas,
255                                 const gfx::Rect& bounds,
256                                 int tab_id) {
257  badge_util::PaintBadge(
258      canvas,
259      bounds,
260      GetBadgeText(tab_id),
261      GetBadgeTextColor(tab_id),
262      GetBadgeBackgroundColor(tab_id),
263      GetIconWidth(tab_id),
264      action_type());
265}
266
267gfx::ImageSkia ExtensionAction::GetIconWithBadge(
268    const gfx::ImageSkia& icon,
269    int tab_id,
270    const gfx::Size& spacing) const {
271  if (tab_id < 0)
272    return icon;
273
274  return gfx::ImageSkia(
275      new IconWithBadgeImageSource(icon,
276                                   icon.size(),
277                                   spacing,
278                                   GetBadgeText(tab_id),
279                                   GetBadgeTextColor(tab_id),
280                                   GetBadgeBackgroundColor(tab_id),
281                                   action_type()),
282     icon.size());
283}
284
285bool ExtensionAction::HasPopupUrl(int tab_id) const {
286  return HasValue(popup_url_, tab_id);
287}
288
289bool ExtensionAction::HasTitle(int tab_id) const {
290  return HasValue(title_, tab_id);
291}
292
293bool ExtensionAction::HasBadgeText(int tab_id) const {
294  return HasValue(badge_text_, tab_id);
295}
296
297bool ExtensionAction::HasBadgeBackgroundColor(int tab_id) const {
298  return HasValue(badge_background_color_, tab_id);
299}
300
301bool ExtensionAction::HasBadgeTextColor(int tab_id) const {
302  return HasValue(badge_text_color_, tab_id);
303}
304
305bool ExtensionAction::HasIsVisible(int tab_id) const {
306  return HasValue(is_visible_, tab_id);
307}
308
309bool ExtensionAction::HasIcon(int tab_id) const {
310  return HasValue(icon_, tab_id);
311}
312
313// Determines which icon would be returned by |GetIcon|, and returns its width.
314int ExtensionAction::GetIconWidth(int tab_id) const {
315  // If icon has been set, return its width.
316  gfx::ImageSkia icon = GetValue(&icon_, tab_id);
317  if (!icon.isNull())
318    return icon.width();
319  // If there is a default icon, the icon width will be set depending on our
320  // action type.
321  if (default_icon_)
322    return GetIconSizeForType(action_type());
323
324  // If no icon has been set and there is no default icon, we need favicon
325  // width.
326  return ui::ResourceBundle::GetSharedInstance().GetImageNamed(
327          IDR_EXTENSIONS_FAVICON).ToImageSkia()->width();
328}
329