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/bind.h"
10#include "base/logging.h"
11#include "base/message_loop/message_loop.h"
12#include "chrome/common/badge_util.h"
13#include "chrome/common/extensions/extension_constants.h"
14#include "chrome/common/icon_with_badge_image_source.h"
15#include "grit/theme_resources.h"
16#include "grit/ui_resources.h"
17#include "third_party/skia/include/core/SkBitmap.h"
18#include "third_party/skia/include/core/SkBitmapDevice.h"
19#include "third_party/skia/include/core/SkCanvas.h"
20#include "third_party/skia/include/core/SkPaint.h"
21#include "third_party/skia/include/effects/SkGradientShader.h"
22#include "ui/base/resource/resource_bundle.h"
23#include "ui/gfx/animation/animation_delegate.h"
24#include "ui/gfx/canvas.h"
25#include "ui/gfx/color_utils.h"
26#include "ui/gfx/image/image.h"
27#include "ui/gfx/image/image_skia.h"
28#include "ui/gfx/image/image_skia_source.h"
29#include "ui/gfx/rect.h"
30#include "ui/gfx/size.h"
31#include "ui/gfx/skbitmap_operations.h"
32#include "url/gurl.h"
33
34namespace {
35
36class GetAttentionImageSource : public gfx::ImageSkiaSource {
37 public:
38  explicit GetAttentionImageSource(const gfx::ImageSkia& icon)
39      : icon_(icon) {}
40
41  // gfx::ImageSkiaSource overrides:
42  virtual gfx::ImageSkiaRep GetImageForScale(float scale) OVERRIDE {
43    gfx::ImageSkiaRep icon_rep = icon_.GetRepresentation(scale);
44    color_utils::HSL shift = {-1, 0, 0.5};
45    return gfx::ImageSkiaRep(
46        SkBitmapOperations::CreateHSLShiftedBitmap(icon_rep.sk_bitmap(), shift),
47        icon_rep.scale());
48  }
49
50 private:
51  const gfx::ImageSkia icon_;
52};
53
54}  // namespace
55
56// TODO(tbarzic): Merge AnimationIconImageSource and IconAnimation together.
57// Source for painting animated skia image.
58class AnimatedIconImageSource : public gfx::ImageSkiaSource {
59 public:
60  AnimatedIconImageSource(
61      const gfx::ImageSkia& image,
62      base::WeakPtr<ExtensionAction::IconAnimation> animation)
63      : image_(image),
64        animation_(animation) {
65  }
66
67 private:
68  virtual ~AnimatedIconImageSource() {}
69
70  virtual gfx::ImageSkiaRep GetImageForScale(float scale) OVERRIDE {
71    gfx::ImageSkiaRep original_rep = image_.GetRepresentation(scale);
72    if (!animation_.get())
73      return original_rep;
74
75    // Original representation's scale factor may be different from scale
76    // factor passed to this method. We want to use the former (since we are
77    // using bitmap for that scale).
78    return gfx::ImageSkiaRep(
79        animation_->Apply(original_rep.sk_bitmap()), original_rep.scale());
80  }
81
82  gfx::ImageSkia image_;
83  base::WeakPtr<ExtensionAction::IconAnimation> animation_;
84
85  DISALLOW_COPY_AND_ASSIGN(AnimatedIconImageSource);
86};
87
88const int ExtensionAction::kDefaultTabId = -1;
89// 100ms animation at 50fps (so 5 animation frames in total).
90const int kIconFadeInDurationMs = 100;
91const int kIconFadeInFramesPerSecond = 50;
92
93ExtensionAction::IconAnimation::IconAnimation()
94    : gfx::LinearAnimation(kIconFadeInDurationMs, kIconFadeInFramesPerSecond,
95                          NULL),
96      weak_ptr_factory_(this) {}
97
98ExtensionAction::IconAnimation::~IconAnimation() {
99  // Make sure observers don't access *this after its destructor has started.
100  weak_ptr_factory_.InvalidateWeakPtrs();
101  // In case the animation was destroyed before it finished (likely due to
102  // delays in timer scheduling), make sure it's fully visible.
103  FOR_EACH_OBSERVER(Observer, observers_, OnIconChanged());
104}
105
106const SkBitmap& ExtensionAction::IconAnimation::Apply(
107    const SkBitmap& icon) const {
108  DCHECK_GT(icon.width(), 0);
109  DCHECK_GT(icon.height(), 0);
110
111  if (!device_.get() ||
112      (device_->width() != icon.width()) ||
113      (device_->height() != icon.height())) {
114    device_.reset(new SkBitmapDevice(
115      SkBitmap::kARGB_8888_Config, icon.width(), icon.height(), true));
116  }
117
118  SkCanvas canvas(device_.get());
119  canvas.clear(SK_ColorWHITE);
120  SkPaint paint;
121  paint.setAlpha(CurrentValueBetween(0, 255));
122  canvas.drawBitmap(icon, 0, 0, &paint);
123  return device_->accessBitmap(false);
124}
125
126base::WeakPtr<ExtensionAction::IconAnimation>
127ExtensionAction::IconAnimation::AsWeakPtr() {
128  return weak_ptr_factory_.GetWeakPtr();
129}
130
131void ExtensionAction::IconAnimation::AddObserver(
132    ExtensionAction::IconAnimation::Observer* observer) {
133  observers_.AddObserver(observer);
134}
135
136void ExtensionAction::IconAnimation::RemoveObserver(
137    ExtensionAction::IconAnimation::Observer* observer) {
138  observers_.RemoveObserver(observer);
139}
140
141void ExtensionAction::IconAnimation::AnimateToState(double state) {
142  FOR_EACH_OBSERVER(Observer, observers_, OnIconChanged());
143}
144
145ExtensionAction::IconAnimation::ScopedObserver::ScopedObserver(
146    const base::WeakPtr<IconAnimation>& icon_animation,
147    Observer* observer)
148    : icon_animation_(icon_animation),
149      observer_(observer) {
150  if (icon_animation.get())
151    icon_animation->AddObserver(observer);
152}
153
154ExtensionAction::IconAnimation::ScopedObserver::~ScopedObserver() {
155  if (icon_animation_.get())
156    icon_animation_->RemoveObserver(observer_);
157}
158
159ExtensionAction::ExtensionAction(
160    const std::string& extension_id,
161    extensions::ActionInfo::Type action_type,
162    const extensions::ActionInfo& manifest_data)
163    : extension_id_(extension_id),
164      action_type_(action_type),
165      has_changed_(false) {
166  // Page/script actions are hidden/disabled by default, and browser actions are
167  // visible/enabled by default.
168  SetAppearance(kDefaultTabId,
169                action_type == extensions::ActionInfo::TYPE_BROWSER ?
170                ExtensionAction::ACTIVE : ExtensionAction::INVISIBLE);
171  SetTitle(kDefaultTabId, manifest_data.default_title);
172  SetPopupUrl(kDefaultTabId, manifest_data.default_popup_url);
173  if (!manifest_data.default_icon.empty()) {
174    set_default_icon(make_scoped_ptr(new ExtensionIconSet(
175        manifest_data.default_icon)));
176  }
177  set_id(manifest_data.id);
178}
179
180ExtensionAction::~ExtensionAction() {
181}
182
183scoped_ptr<ExtensionAction> ExtensionAction::CopyForTest() const {
184  scoped_ptr<ExtensionAction> copy(
185      new ExtensionAction(extension_id_, action_type_,
186                          extensions::ActionInfo()));
187  copy->popup_url_ = popup_url_;
188  copy->title_ = title_;
189  copy->icon_ = icon_;
190  copy->badge_text_ = badge_text_;
191  copy->badge_background_color_ = badge_background_color_;
192  copy->badge_text_color_ = badge_text_color_;
193  copy->appearance_ = appearance_;
194  copy->icon_animation_ = icon_animation_;
195  copy->id_ = id_;
196
197  if (default_icon_)
198    copy->default_icon_.reset(new ExtensionIconSet(*default_icon_));
199
200  return copy.Pass();
201}
202
203// static
204int ExtensionAction::GetIconSizeForType(
205    extensions::ActionInfo::Type type) {
206  switch (type) {
207    case extensions::ActionInfo::TYPE_BROWSER:
208    case extensions::ActionInfo::TYPE_PAGE:
209    case extensions::ActionInfo::TYPE_SYSTEM_INDICATOR:
210      // TODO(dewittj) Report the actual icon size of the system
211      // indicator.
212      return extension_misc::EXTENSION_ICON_ACTION;
213    case extensions::ActionInfo::TYPE_SCRIPT_BADGE:
214      return extension_misc::EXTENSION_ICON_BITTY;
215    default:
216      NOTREACHED();
217      return 0;
218  }
219}
220
221void ExtensionAction::SetPopupUrl(int tab_id, const GURL& url) {
222  // We store |url| even if it is empty, rather than removing a URL from the
223  // map.  If an extension has a default popup, and removes it for a tab via
224  // the API, we must remember that there is no popup for that specific tab.
225  // If we removed the tab's URL, GetPopupURL would incorrectly return the
226  // default URL.
227  SetValue(&popup_url_, tab_id, url);
228}
229
230bool ExtensionAction::HasPopup(int tab_id) const {
231  return !GetPopupUrl(tab_id).is_empty();
232}
233
234GURL ExtensionAction::GetPopupUrl(int tab_id) const {
235  return GetValue(&popup_url_, tab_id);
236}
237
238void ExtensionAction::SetIcon(int tab_id, const gfx::Image& image) {
239  SetValue(&icon_, tab_id, image.AsImageSkia());
240}
241
242gfx::Image ExtensionAction::ApplyAttentionAndAnimation(
243    const gfx::ImageSkia& original_icon,
244    int tab_id) const {
245  gfx::ImageSkia icon = original_icon;
246  if (GetValue(&appearance_, tab_id) == WANTS_ATTENTION)
247    icon = gfx::ImageSkia(new GetAttentionImageSource(icon), icon.size());
248
249  return gfx::Image(ApplyIconAnimation(tab_id, icon));
250}
251
252gfx::ImageSkia ExtensionAction::GetExplicitlySetIcon(int tab_id) const {
253  return GetValue(&icon_, tab_id);
254}
255
256bool ExtensionAction::SetAppearance(int tab_id, Appearance new_appearance) {
257  const Appearance old_appearance = GetValue(&appearance_, tab_id);
258
259  if (old_appearance == new_appearance)
260    return false;
261
262  SetValue(&appearance_, tab_id, new_appearance);
263
264  // When showing a script badge for the first time on a web page, fade it in.
265  // Other transitions happen instantly.
266  if (old_appearance == INVISIBLE && tab_id != kDefaultTabId &&
267      action_type_ == extensions::ActionInfo::TYPE_SCRIPT_BADGE) {
268    RunIconAnimation(tab_id);
269  }
270
271  return true;
272}
273
274void ExtensionAction::DeclarativeShow(int tab_id) {
275  DCHECK_NE(tab_id, kDefaultTabId);
276  ++declarative_show_count_[tab_id];  // Use default initialization to 0.
277}
278
279void ExtensionAction::UndoDeclarativeShow(int tab_id) {
280  int& show_count = declarative_show_count_[tab_id];
281  DCHECK_GT(show_count, 0);
282  if (--show_count == 0)
283    declarative_show_count_.erase(tab_id);
284}
285
286void ExtensionAction::ClearAllValuesForTab(int tab_id) {
287  popup_url_.erase(tab_id);
288  title_.erase(tab_id);
289  icon_.erase(tab_id);
290  badge_text_.erase(tab_id);
291  badge_text_color_.erase(tab_id);
292  badge_background_color_.erase(tab_id);
293  appearance_.erase(tab_id);
294  // TODO(jyasskin): Erase the element from declarative_show_count_
295  // when the tab's closed.  There's a race between the
296  // PageActionController and the ContentRulesRegistry on navigation,
297  // which prevents me from cleaning everything up now.
298  icon_animation_.erase(tab_id);
299}
300
301void ExtensionAction::PaintBadge(gfx::Canvas* canvas,
302                                 const gfx::Rect& bounds,
303                                 int tab_id) {
304  badge_util::PaintBadge(
305      canvas,
306      bounds,
307      GetBadgeText(tab_id),
308      GetBadgeTextColor(tab_id),
309      GetBadgeBackgroundColor(tab_id),
310      GetIconWidth(tab_id),
311      action_type());
312}
313
314gfx::ImageSkia ExtensionAction::GetIconWithBadge(
315    const gfx::ImageSkia& icon,
316    int tab_id,
317    const gfx::Size& spacing) const {
318  if (tab_id < 0)
319    return icon;
320
321  return gfx::ImageSkia(
322      new IconWithBadgeImageSource(icon,
323                                   icon.size(),
324                                   spacing,
325                                   GetBadgeText(tab_id),
326                                   GetBadgeTextColor(tab_id),
327                                   GetBadgeBackgroundColor(tab_id),
328                                   action_type()),
329     icon.size());
330}
331
332// Determines which icon would be returned by |GetIcon|, and returns its width.
333int ExtensionAction::GetIconWidth(int tab_id) const {
334  // If icon has been set, return its width.
335  gfx::ImageSkia icon = GetValue(&icon_, tab_id);
336  if (!icon.isNull())
337    return icon.width();
338  // If there is a default icon, the icon width will be set depending on our
339  // action type.
340  if (default_icon_)
341    return GetIconSizeForType(action_type());
342
343  // If no icon has been set and there is no default icon, we need favicon
344  // width.
345  return ui::ResourceBundle::GetSharedInstance().GetImageNamed(
346          IDR_EXTENSIONS_FAVICON).ToImageSkia()->width();
347}
348
349base::WeakPtr<ExtensionAction::IconAnimation> ExtensionAction::GetIconAnimation(
350    int tab_id) const {
351  std::map<int, base::WeakPtr<IconAnimation> >::iterator it =
352      icon_animation_.find(tab_id);
353  if (it == icon_animation_.end())
354    return base::WeakPtr<ExtensionAction::IconAnimation>();
355  if (it->second.get())
356    return it->second;
357
358  // Take this opportunity to remove all the NULL IconAnimations from
359  // icon_animation_.
360  icon_animation_.erase(it);
361  for (it = icon_animation_.begin(); it != icon_animation_.end();) {
362    if (it->second.get()) {
363      ++it;
364    } else {
365      // The WeakPtr is null; remove it from the map.
366      icon_animation_.erase(it++);
367    }
368  }
369  return base::WeakPtr<ExtensionAction::IconAnimation>();
370}
371
372gfx::ImageSkia ExtensionAction::ApplyIconAnimation(
373    int tab_id,
374    const gfx::ImageSkia& icon) const {
375  base::WeakPtr<IconAnimation> animation = GetIconAnimation(tab_id);
376  if (animation.get() == NULL)
377    return icon;
378
379  return gfx::ImageSkia(new AnimatedIconImageSource(icon, animation),
380                        icon.size());
381}
382
383namespace {
384// Used to create a Callback owning an IconAnimation.
385void DestroyIconAnimation(scoped_ptr<ExtensionAction::IconAnimation>) {}
386}
387void ExtensionAction::RunIconAnimation(int tab_id) {
388  scoped_ptr<IconAnimation> icon_animation(new IconAnimation());
389  icon_animation_[tab_id] = icon_animation->AsWeakPtr();
390  icon_animation->Start();
391  // After the icon is finished fading in (plus some padding to handle random
392  // timer delays), destroy it. We use a delayed task so that the Animation is
393  // deleted even if it hasn't finished by the time the MessageLoop is
394  // destroyed.
395  base::MessageLoop::current()->PostDelayedTask(
396      FROM_HERE,
397      base::Bind(&DestroyIconAnimation, base::Passed(&icon_animation)),
398      base::TimeDelta::FromMilliseconds(kIconFadeInDurationMs * 2));
399}
400