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_icon_factory.h"
6
7#include "base/command_line.h"
8#include "base/files/file_util.h"
9#include "base/json/json_file_value_serializer.h"
10#include "base/message_loop/message_loop.h"
11#include "base/path_service.h"
12#include "chrome/browser/extensions/extension_action.h"
13#include "chrome/browser/extensions/extension_action_manager.h"
14#include "chrome/browser/extensions/extension_service.h"
15#include "chrome/browser/extensions/test_extension_system.h"
16#include "chrome/common/chrome_paths.h"
17#include "chrome/test/base/testing_profile.h"
18#include "content/public/test/test_browser_thread.h"
19#include "extensions/common/extension.h"
20#include "grit/theme_resources.h"
21#include "skia/ext/image_operations.h"
22#include "testing/gtest/include/gtest/gtest.h"
23#include "ui/base/resource/resource_bundle.h"
24#include "ui/gfx/codec/png_codec.h"
25#include "ui/gfx/image/image_skia.h"
26#include "ui/gfx/skia_util.h"
27
28#if defined(OS_CHROMEOS)
29#include "chrome/browser/chromeos/login/users/scoped_test_user_manager.h"
30#include "chrome/browser/chromeos/settings/cros_settings.h"
31#include "chrome/browser/chromeos/settings/device_settings_service.h"
32#endif
33
34using content::BrowserThread;
35
36namespace extensions {
37namespace {
38
39bool ImageRepsAreEqual(const gfx::ImageSkiaRep& image_rep1,
40                       const gfx::ImageSkiaRep& image_rep2) {
41  return image_rep1.scale() == image_rep2.scale() &&
42         gfx::BitmapsAreEqual(image_rep1.sk_bitmap(), image_rep2.sk_bitmap());
43}
44
45gfx::Image EnsureImageSize(const gfx::Image& original, int size) {
46  const SkBitmap* original_bitmap = original.ToSkBitmap();
47  if (original_bitmap->width() == size && original_bitmap->height() == size)
48    return original;
49
50  SkBitmap resized = skia::ImageOperations::Resize(
51      *original.ToSkBitmap(), skia::ImageOperations::RESIZE_LANCZOS3,
52      size, size);
53  return gfx::Image::CreateFrom1xBitmap(resized);
54}
55
56gfx::ImageSkiaRep CreateBlankRep(int size_dip, float scale) {
57  SkBitmap bitmap;
58  bitmap.allocN32Pixels(static_cast<int>(size_dip * scale),
59                        static_cast<int>(size_dip * scale));
60  bitmap.eraseColor(SkColorSetARGB(0, 0, 0, 0));
61  return gfx::ImageSkiaRep(bitmap, scale);
62}
63
64gfx::Image LoadIcon(const std::string& filename) {
65  base::FilePath path;
66  PathService::Get(chrome::DIR_TEST_DATA, &path);
67  path = path.AppendASCII("extensions/api_test").AppendASCII(filename);
68
69  std::string file_contents;
70  base::ReadFileToString(path, &file_contents);
71  const unsigned char* data =
72      reinterpret_cast<const unsigned char*>(file_contents.data());
73
74  SkBitmap bitmap;
75  gfx::PNGCodec::Decode(data, file_contents.length(), &bitmap);
76
77  return gfx::Image::CreateFrom1xBitmap(bitmap);
78}
79
80class ExtensionActionIconFactoryTest
81    : public testing::Test,
82      public ExtensionActionIconFactory::Observer {
83 public:
84  ExtensionActionIconFactoryTest()
85      : quit_in_icon_updated_(false),
86        ui_thread_(BrowserThread::UI, &ui_loop_),
87        file_thread_(BrowserThread::FILE),
88        io_thread_(BrowserThread::IO) {
89  }
90
91  virtual ~ExtensionActionIconFactoryTest() {}
92
93  void WaitForIconUpdate() {
94    quit_in_icon_updated_ = true;
95    base::MessageLoop::current()->Run();
96    quit_in_icon_updated_ = false;
97  }
98
99  scoped_refptr<Extension> CreateExtension(const char* name,
100                                           Manifest::Location location) {
101    // Create and load an extension.
102    base::FilePath test_file;
103    if (!PathService::Get(chrome::DIR_TEST_DATA, &test_file)) {
104      EXPECT_FALSE(true);
105      return NULL;
106    }
107    test_file = test_file.AppendASCII("extensions/api_test").AppendASCII(name);
108    int error_code = 0;
109    std::string error;
110    JSONFileValueSerializer serializer(test_file.AppendASCII("manifest.json"));
111    scoped_ptr<base::DictionaryValue> valid_value(
112        static_cast<base::DictionaryValue*>(serializer.Deserialize(&error_code,
113                                                                   &error)));
114    EXPECT_EQ(0, error_code) << error;
115    if (error_code != 0)
116      return NULL;
117
118    EXPECT_TRUE(valid_value.get());
119    if (!valid_value)
120      return NULL;
121
122    scoped_refptr<Extension> extension =
123        Extension::Create(test_file, location, *valid_value,
124                          Extension::NO_FLAGS, &error);
125    EXPECT_TRUE(extension.get()) << error;
126    if (extension.get())
127      extension_service_->AddExtension(extension.get());
128    return extension;
129  }
130
131  // testing::Test overrides:
132  virtual void SetUp() OVERRIDE {
133    file_thread_.Start();
134    io_thread_.Start();
135    profile_.reset(new TestingProfile);
136    CommandLine command_line(CommandLine::NO_PROGRAM);
137    extension_service_ = static_cast<extensions::TestExtensionSystem*>(
138        extensions::ExtensionSystem::Get(profile_.get()))->
139        CreateExtensionService(&command_line, base::FilePath(), false);
140  }
141
142  virtual void TearDown() OVERRIDE {
143    profile_.reset();  // Get all DeleteSoon calls sent to ui_loop_.
144    ui_loop_.RunUntilIdle();
145  }
146
147  // ExtensionActionIconFactory::Observer overrides:
148  virtual void OnIconUpdated() OVERRIDE {
149    if (quit_in_icon_updated_)
150      base::MessageLoop::current()->Quit();
151  }
152
153  gfx::ImageSkia GetFavicon() {
154    return *ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
155        IDR_EXTENSIONS_FAVICON);
156  }
157
158  ExtensionAction* GetBrowserAction(const Extension& extension) {
159    return ExtensionActionManager::Get(profile())->GetBrowserAction(extension);
160  }
161
162  TestingProfile* profile() { return profile_.get(); }
163
164 private:
165  bool quit_in_icon_updated_;
166  base::MessageLoop ui_loop_;
167  content::TestBrowserThread ui_thread_;
168  content::TestBrowserThread file_thread_;
169  content::TestBrowserThread io_thread_;
170  scoped_ptr<TestingProfile> profile_;
171  ExtensionService* extension_service_;
172
173#if defined OS_CHROMEOS
174  chromeos::ScopedTestDeviceSettingsService test_device_settings_service_;
175  chromeos::ScopedTestCrosSettings test_cros_settings_;
176  chromeos::ScopedTestUserManager test_user_manager_;
177#endif
178
179  DISALLOW_COPY_AND_ASSIGN(ExtensionActionIconFactoryTest);
180};
181
182// If there is no default icon, and the icon has not been set using |SetIcon|,
183// the factory should return favicon.
184TEST_F(ExtensionActionIconFactoryTest, NoIcons) {
185  // Load an extension that has browser action without default icon set in the
186  // manifest and does not call |SetIcon| by default.
187  scoped_refptr<Extension> extension(CreateExtension(
188      "browser_action/no_icon", Manifest::INVALID_LOCATION));
189  ASSERT_TRUE(extension.get() != NULL);
190  ExtensionAction* browser_action = GetBrowserAction(*extension.get());
191  ASSERT_TRUE(browser_action);
192  ASSERT_FALSE(browser_action->default_icon());
193  ASSERT_TRUE(browser_action->GetExplicitlySetIcon(0 /*tab id*/).isNull());
194
195  gfx::ImageSkia favicon = GetFavicon();
196
197  ExtensionActionIconFactory icon_factory(
198      profile(), extension.get(), browser_action, this);
199
200  gfx::Image icon = icon_factory.GetIcon(0);
201
202  EXPECT_TRUE(ImageRepsAreEqual(
203      favicon.GetRepresentation(1.0f),
204      icon.ToImageSkia()->GetRepresentation(1.0f)));
205}
206
207// If the icon has been set using |SetIcon|, the factory should return that
208// icon.
209TEST_F(ExtensionActionIconFactoryTest, AfterSetIcon) {
210  // Load an extension that has browser action without default icon set in the
211  // manifest and does not call |SetIcon| by default (but has an browser action
212  // icon resource).
213  scoped_refptr<Extension> extension(CreateExtension(
214      "browser_action/no_icon", Manifest::INVALID_LOCATION));
215  ASSERT_TRUE(extension.get() != NULL);
216  ExtensionAction* browser_action = GetBrowserAction(*extension.get());
217  ASSERT_TRUE(browser_action);
218  ASSERT_FALSE(browser_action->default_icon());
219  ASSERT_TRUE(browser_action->GetExplicitlySetIcon(0 /*tab id*/).isNull());
220
221  gfx::Image set_icon = LoadIcon("browser_action/no_icon/icon.png");
222  ASSERT_FALSE(set_icon.IsEmpty());
223
224  browser_action->SetIcon(0, set_icon);
225
226  ASSERT_FALSE(browser_action->GetExplicitlySetIcon(0 /*tab id*/).isNull());
227
228  ExtensionActionIconFactory icon_factory(
229      profile(), extension.get(), browser_action, this);
230
231  gfx::Image icon = icon_factory.GetIcon(0);
232
233  EXPECT_TRUE(ImageRepsAreEqual(
234      set_icon.ToImageSkia()->GetRepresentation(1.0f),
235      icon.ToImageSkia()->GetRepresentation(1.0f)));
236
237  // It should still return favicon for another tabs.
238  icon = icon_factory.GetIcon(1);
239
240  EXPECT_TRUE(ImageRepsAreEqual(
241      GetFavicon().GetRepresentation(1.0f),
242      icon.ToImageSkia()->GetRepresentation(1.0f)));
243}
244
245// If there is a default icon, and the icon has not been set using |SetIcon|,
246// the factory should return the default icon.
247TEST_F(ExtensionActionIconFactoryTest, DefaultIcon) {
248  // Load an extension that has browser action without default icon set in the
249  // manifest and does not call |SetIcon| by default (but has an browser action
250  // icon resource).
251  scoped_refptr<Extension> extension(CreateExtension(
252      "browser_action/no_icon", Manifest::INVALID_LOCATION));
253  ASSERT_TRUE(extension.get() != NULL);
254  ExtensionAction* browser_action = GetBrowserAction(*extension.get());
255  ASSERT_TRUE(browser_action);
256  ASSERT_FALSE(browser_action->default_icon());
257  ASSERT_TRUE(browser_action->GetExplicitlySetIcon(0 /*tab id*/).isNull());
258
259  gfx::Image default_icon =
260      EnsureImageSize(LoadIcon("browser_action/no_icon/icon.png"), 19);
261  ASSERT_FALSE(default_icon.IsEmpty());
262
263  scoped_ptr<ExtensionIconSet> default_icon_set(new ExtensionIconSet());
264  default_icon_set->Add(19, "icon.png");
265
266  browser_action->set_default_icon(default_icon_set.Pass());
267  ASSERT_TRUE(browser_action->default_icon());
268
269  ExtensionActionIconFactory icon_factory(
270      profile(), extension.get(), browser_action, this);
271
272  gfx::Image icon = icon_factory.GetIcon(0);
273
274  // The icon should be loaded asynchronously. Initially a transparent icon
275  // should be returned.
276  EXPECT_TRUE(ImageRepsAreEqual(
277      CreateBlankRep(19, 1.0f),
278      icon.ToImageSkia()->GetRepresentation(1.0f)));
279
280  WaitForIconUpdate();
281
282  icon = icon_factory.GetIcon(0);
283
284  // The default icon representation should be loaded at this point.
285  EXPECT_TRUE(ImageRepsAreEqual(
286      default_icon.ToImageSkia()->GetRepresentation(1.0f),
287      icon.ToImageSkia()->GetRepresentation(1.0f)));
288
289  // The same icon should be returned for the other tabs.
290  icon = icon_factory.GetIcon(1);
291
292  EXPECT_TRUE(ImageRepsAreEqual(
293      default_icon.ToImageSkia()->GetRepresentation(1.0f),
294      icon.ToImageSkia()->GetRepresentation(1.0f)));
295
296}
297
298}  // namespace
299}  // namespace extensions
300