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 "extensions/browser/extension_icon_image.h"
6
7#include <vector>
8
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 "content/public/browser/notification_service.h"
13#include "content/public/test/test_browser_context.h"
14#include "content/public/test/test_browser_thread.h"
15#include "extensions/browser/extensions_test.h"
16#include "extensions/browser/image_loader.h"
17#include "extensions/common/extension.h"
18#include "extensions/common/extension_paths.h"
19#include "extensions/common/manifest.h"
20#include "extensions/common/manifest_handlers/icons_handler.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/image/image_skia_source.h"
25#include "ui/gfx/skia_util.h"
26
27using content::BrowserThread;
28
29namespace extensions {
30namespace {
31
32SkBitmap CreateBlankBitmapForScale(int size_dip, ui::ScaleFactor scale_factor) {
33  SkBitmap bitmap;
34  const float scale = ui::GetScaleForScaleFactor(scale_factor);
35  bitmap.allocN32Pixels(static_cast<int>(size_dip * scale),
36                        static_cast<int>(size_dip * scale));
37  bitmap.eraseColor(SkColorSetARGB(0, 0, 0, 0));
38  return bitmap;
39}
40
41SkBitmap EnsureBitmapSize(const SkBitmap& original, int size) {
42  if (original.width() == size && original.height() == size)
43    return original;
44
45  SkBitmap resized = skia::ImageOperations::Resize(
46      original, skia::ImageOperations::RESIZE_LANCZOS3, size, size);
47  return resized;
48}
49
50// Used to test behavior including images defined by an image skia source.
51// |GetImageForScale| simply returns image representation from the image given
52// in the ctor.
53class MockImageSkiaSource : public gfx::ImageSkiaSource {
54 public:
55  explicit MockImageSkiaSource(const gfx::ImageSkia& image)
56      : image_(image) {
57  }
58  virtual ~MockImageSkiaSource() {}
59
60  virtual gfx::ImageSkiaRep GetImageForScale(float scale) OVERRIDE {
61    return image_.GetRepresentation(scale);
62  }
63
64 private:
65  gfx::ImageSkia image_;
66};
67
68// Helper class for synchronously loading extension image resource.
69class TestImageLoader {
70 public:
71  explicit TestImageLoader(const Extension* extension)
72      : extension_(extension),
73        waiting_(false),
74        image_loaded_(false) {
75  }
76  virtual ~TestImageLoader() {}
77
78  void OnImageLoaded(const gfx::Image& image) {
79    image_ = image;
80    image_loaded_ = true;
81    if (waiting_)
82      base::MessageLoop::current()->Quit();
83  }
84
85  SkBitmap LoadBitmap(const std::string& path,
86                      int size) {
87    image_loaded_ = false;
88
89    image_loader_.LoadImageAsync(
90        extension_, extension_->GetResource(path), gfx::Size(size, size),
91        base::Bind(&TestImageLoader::OnImageLoaded,
92                   base::Unretained(this)));
93
94    // If |image_| still hasn't been loaded (i.e. it is being loaded
95    // asynchronously), wait for it.
96    if (!image_loaded_) {
97      waiting_ = true;
98      base::MessageLoop::current()->Run();
99      waiting_ = false;
100    }
101
102    EXPECT_TRUE(image_loaded_);
103
104    return image_.IsEmpty() ? SkBitmap() : *image_.ToSkBitmap();
105  }
106
107 private:
108  const Extension* extension_;
109  bool waiting_;
110  bool image_loaded_;
111  gfx::Image image_;
112  ImageLoader image_loader_;
113
114  DISALLOW_COPY_AND_ASSIGN(TestImageLoader);
115};
116
117class ExtensionIconImageTest : public ExtensionsTest,
118                               public IconImage::Observer {
119 public:
120  ExtensionIconImageTest()
121      : image_loaded_count_(0),
122        quit_in_image_loaded_(false),
123        ui_thread_(BrowserThread::UI, &ui_loop_),
124        file_thread_(BrowserThread::FILE),
125        io_thread_(BrowserThread::IO),
126        notification_service_(content::NotificationService::Create()) {}
127
128  virtual ~ExtensionIconImageTest() {}
129
130  void WaitForImageLoad() {
131    quit_in_image_loaded_ = true;
132    base::MessageLoop::current()->Run();
133    quit_in_image_loaded_ = false;
134  }
135
136  int ImageLoadedCount() {
137    int result = image_loaded_count_;
138    image_loaded_count_ = 0;
139    return result;
140  }
141
142  scoped_refptr<Extension> CreateExtension(const char* name,
143                                           Manifest::Location location) {
144    // Create and load an extension.
145    base::FilePath test_file;
146    if (!PathService::Get(DIR_TEST_DATA, &test_file)) {
147      EXPECT_FALSE(true);
148      return NULL;
149    }
150    test_file = test_file.AppendASCII(name);
151    int error_code = 0;
152    std::string error;
153    JSONFileValueSerializer serializer(test_file.AppendASCII("manifest.json"));
154    scoped_ptr<base::DictionaryValue> valid_value(
155        static_cast<base::DictionaryValue*>(serializer.Deserialize(&error_code,
156                                                                   &error)));
157    EXPECT_EQ(0, error_code) << error;
158    if (error_code != 0)
159      return NULL;
160
161    EXPECT_TRUE(valid_value.get());
162    if (!valid_value)
163      return NULL;
164
165    return Extension::Create(test_file, location, *valid_value,
166                             Extension::NO_FLAGS, &error);
167  }
168
169  // testing::Test overrides:
170  virtual void SetUp() OVERRIDE {
171    file_thread_.Start();
172    io_thread_.Start();
173  }
174
175  // IconImage::Delegate overrides:
176  virtual void OnExtensionIconImageChanged(IconImage* image) OVERRIDE {
177    image_loaded_count_++;
178    if (quit_in_image_loaded_)
179      base::MessageLoop::current()->Quit();
180  }
181
182  gfx::ImageSkia GetDefaultIcon() {
183    return gfx::ImageSkia(gfx::ImageSkiaRep(gfx::Size(16, 16), 1.0f));
184  }
185
186  // Loads an image to be used in test from the extension.
187  // The image will be loaded from the relative path |path|.
188  SkBitmap GetTestBitmap(const Extension* extension,
189                         const std::string& path,
190                         int size) {
191    TestImageLoader image_loader(extension);
192    return image_loader.LoadBitmap(path, size);
193  }
194
195 private:
196  int image_loaded_count_;
197  bool quit_in_image_loaded_;
198  base::MessageLoop ui_loop_;
199  content::TestBrowserThread ui_thread_;
200  content::TestBrowserThread file_thread_;
201  content::TestBrowserThread io_thread_;
202  scoped_ptr<content::NotificationService> notification_service_;
203
204  DISALLOW_COPY_AND_ASSIGN(ExtensionIconImageTest);
205};
206
207}  // namespace
208
209TEST_F(ExtensionIconImageTest, Basic) {
210  std::vector<ui::ScaleFactor> supported_factors;
211  supported_factors.push_back(ui::SCALE_FACTOR_100P);
212  supported_factors.push_back(ui::SCALE_FACTOR_200P);
213  ui::test::ScopedSetSupportedScaleFactors scoped_supported(supported_factors);
214  scoped_refptr<Extension> extension(CreateExtension(
215      "extension_icon_image", Manifest::INVALID_LOCATION));
216  ASSERT_TRUE(extension.get() != NULL);
217
218  gfx::ImageSkia default_icon = GetDefaultIcon();
219
220  // Load images we expect to find as representations in icon_image, so we
221  // can later use them to validate icon_image.
222  SkBitmap bitmap_16 = GetTestBitmap(extension.get(), "16.png", 16);
223  ASSERT_FALSE(bitmap_16.empty());
224
225  // There is no image of size 32 defined in the extension manifest, so we
226  // should expect manifest image of size 48 resized to size 32.
227  SkBitmap bitmap_48_resized_to_32 =
228      GetTestBitmap(extension.get(), "48.png", 32);
229  ASSERT_FALSE(bitmap_48_resized_to_32.empty());
230
231  IconImage image(browser_context(),
232                  extension.get(),
233                  IconsInfo::GetIcons(extension.get()),
234                  16,
235                  default_icon,
236                  this);
237
238  // No representations in |image_| yet.
239  gfx::ImageSkia::ImageSkiaReps image_reps = image.image_skia().image_reps();
240  ASSERT_EQ(0u, image_reps.size());
241
242  // Gets representation for a scale factor.
243  gfx::ImageSkiaRep representation = image.image_skia().GetRepresentation(1.0f);
244
245  // Before the image representation is loaded, image should contain blank
246  // image representation.
247  EXPECT_TRUE(gfx::BitmapsAreEqual(
248      representation.sk_bitmap(),
249      CreateBlankBitmapForScale(16, ui::SCALE_FACTOR_100P)));
250
251  WaitForImageLoad();
252  EXPECT_EQ(1, ImageLoadedCount());
253  ASSERT_EQ(1u, image.image_skia().image_reps().size());
254
255  representation = image.image_skia().GetRepresentation(1.0f);
256
257  // We should get the right representation now.
258  EXPECT_TRUE(gfx::BitmapsAreEqual(representation.sk_bitmap(), bitmap_16));
259  EXPECT_EQ(16, representation.pixel_width());
260
261  // Gets representation for an additional scale factor.
262  representation = image.image_skia().GetRepresentation(2.0f);
263
264  EXPECT_TRUE(gfx::BitmapsAreEqual(
265      representation.sk_bitmap(),
266      CreateBlankBitmapForScale(16, ui::SCALE_FACTOR_200P)));
267
268  WaitForImageLoad();
269  EXPECT_EQ(1, ImageLoadedCount());
270  ASSERT_EQ(2u, image.image_skia().image_reps().size());
271
272  representation = image.image_skia().GetRepresentation(2.0f);
273
274  // Image should have been resized.
275  EXPECT_EQ(32, representation.pixel_width());
276  EXPECT_TRUE(gfx::BitmapsAreEqual(representation.sk_bitmap(),
277                                   bitmap_48_resized_to_32));
278}
279
280// There is no resource with either exact or bigger size, but there is a smaller
281// resource.
282TEST_F(ExtensionIconImageTest, FallbackToSmallerWhenNoBigger) {
283  std::vector<ui::ScaleFactor> supported_factors;
284  supported_factors.push_back(ui::SCALE_FACTOR_100P);
285  supported_factors.push_back(ui::SCALE_FACTOR_200P);
286  ui::test::ScopedSetSupportedScaleFactors scoped_supported(supported_factors);
287  scoped_refptr<Extension> extension(CreateExtension(
288      "extension_icon_image", Manifest::INVALID_LOCATION));
289  ASSERT_TRUE(extension.get() != NULL);
290
291  gfx::ImageSkia default_icon = GetDefaultIcon();
292
293  // Load images we expect to find as representations in icon_image, so we
294  // can later use them to validate icon_image.
295  SkBitmap bitmap_48 = GetTestBitmap(extension.get(), "48.png", 48);
296  ASSERT_FALSE(bitmap_48.empty());
297
298  IconImage image(browser_context(),
299                  extension.get(),
300                  IconsInfo::GetIcons(extension.get()),
301                  32,
302                  default_icon,
303                  this);
304
305  gfx::ImageSkiaRep representation = image.image_skia().GetRepresentation(2.0f);
306
307  WaitForImageLoad();
308  EXPECT_EQ(1, ImageLoadedCount());
309  ASSERT_EQ(1u, image.image_skia().image_reps().size());
310
311  representation = image.image_skia().GetRepresentation(2.0f);
312
313  // We should have loaded the biggest smaller resource resized to the actual
314  // size.
315  EXPECT_EQ(2.0f, representation.scale());
316  EXPECT_EQ(64, representation.pixel_width());
317  EXPECT_TRUE(gfx::BitmapsAreEqual(representation.sk_bitmap(),
318                                   EnsureBitmapSize(bitmap_48, 64)));
319}
320
321// There is no resource with exact size, but there is a smaller and a bigger
322// one. The bigger resource should be loaded.
323TEST_F(ExtensionIconImageTest, FallbackToBigger) {
324  scoped_refptr<Extension> extension(CreateExtension(
325      "extension_icon_image", Manifest::INVALID_LOCATION));
326  ASSERT_TRUE(extension.get() != NULL);
327
328  gfx::ImageSkia default_icon = GetDefaultIcon();
329
330  // Load images we expect to find as representations in icon_image, so we
331  // can later use them to validate icon_image.
332  SkBitmap bitmap_24 = GetTestBitmap(extension.get(), "24.png", 24);
333  ASSERT_FALSE(bitmap_24.empty());
334
335  IconImage image(browser_context(),
336                  extension.get(),
337                  IconsInfo::GetIcons(extension.get()),
338                  17,
339                  default_icon,
340                  this);
341
342  gfx::ImageSkiaRep representation = image.image_skia().GetRepresentation(1.0f);
343
344  WaitForImageLoad();
345  EXPECT_EQ(1, ImageLoadedCount());
346  ASSERT_EQ(1u, image.image_skia().image_reps().size());
347
348  representation = image.image_skia().GetRepresentation(1.0f);
349
350  // We should have loaded the smallest bigger (resized) resource.
351  EXPECT_EQ(1.0f, representation.scale());
352  EXPECT_EQ(17, representation.pixel_width());
353  EXPECT_TRUE(gfx::BitmapsAreEqual(representation.sk_bitmap(),
354                                   EnsureBitmapSize(bitmap_24, 17)));
355}
356
357// If resource set is empty, |GetRepresentation| should synchronously return
358// default icon, without notifying observer of image change.
359TEST_F(ExtensionIconImageTest, NoResources) {
360  scoped_refptr<Extension> extension(CreateExtension(
361      "extension_icon_image", Manifest::INVALID_LOCATION));
362  ASSERT_TRUE(extension.get() != NULL);
363
364  ExtensionIconSet empty_icon_set;
365  gfx::ImageSkia default_icon = GetDefaultIcon();
366
367  const int kRequestedSize = 24;
368  IconImage image(browser_context(),
369                  extension.get(),
370                  empty_icon_set,
371                  kRequestedSize,
372                  default_icon,
373                  this);
374
375  gfx::ImageSkiaRep representation = image.image_skia().GetRepresentation(1.0f);
376  EXPECT_TRUE(gfx::BitmapsAreEqual(
377      representation.sk_bitmap(),
378      EnsureBitmapSize(
379          default_icon.GetRepresentation(1.0f).sk_bitmap(),
380          kRequestedSize)));
381
382  EXPECT_EQ(0, ImageLoadedCount());
383  // We should have a default icon representation.
384  ASSERT_EQ(1u, image.image_skia().image_reps().size());
385
386  representation = image.image_skia().GetRepresentation(1.0f);
387  EXPECT_TRUE(gfx::BitmapsAreEqual(
388      representation.sk_bitmap(),
389      EnsureBitmapSize(
390          default_icon.GetRepresentation(1.0f).sk_bitmap(),
391          kRequestedSize)));
392}
393
394// If resource set is invalid, image load should be done asynchronously and
395// the observer should be notified when it's done. |GetRepresentation| should
396// return the default icon representation once image load is done.
397TEST_F(ExtensionIconImageTest, InvalidResource) {
398  scoped_refptr<Extension> extension(CreateExtension(
399      "extension_icon_image", Manifest::INVALID_LOCATION));
400  ASSERT_TRUE(extension.get() != NULL);
401
402  const int kInvalidIconSize = 24;
403  ExtensionIconSet invalid_icon_set;
404  invalid_icon_set.Add(kInvalidIconSize, "invalid.png");
405
406  gfx::ImageSkia default_icon = GetDefaultIcon();
407
408  IconImage image(browser_context(),
409                  extension.get(),
410                  invalid_icon_set,
411                  kInvalidIconSize,
412                  default_icon,
413                  this);
414
415  gfx::ImageSkiaRep representation = image.image_skia().GetRepresentation(1.0f);
416  EXPECT_TRUE(gfx::BitmapsAreEqual(
417      representation.sk_bitmap(),
418      CreateBlankBitmapForScale(kInvalidIconSize, ui::SCALE_FACTOR_100P)));
419
420  WaitForImageLoad();
421  EXPECT_EQ(1, ImageLoadedCount());
422  // We should have default icon representation now.
423  ASSERT_EQ(1u, image.image_skia().image_reps().size());
424
425  representation = image.image_skia().GetRepresentation(1.0f);
426  EXPECT_TRUE(gfx::BitmapsAreEqual(
427      representation.sk_bitmap(),
428      EnsureBitmapSize(
429          default_icon.GetRepresentation(1.0f).sk_bitmap(),
430          kInvalidIconSize)));
431}
432
433// Test that IconImage works with lazily (but synchronously) created default
434// icon when IconImage returns synchronously.
435TEST_F(ExtensionIconImageTest, LazyDefaultIcon) {
436  scoped_refptr<Extension> extension(CreateExtension(
437      "extension_icon_image", Manifest::INVALID_LOCATION));
438  ASSERT_TRUE(extension.get() != NULL);
439
440  gfx::ImageSkia default_icon = GetDefaultIcon();
441  gfx::ImageSkia lazy_default_icon(new MockImageSkiaSource(default_icon),
442                                    default_icon.size());
443
444  ExtensionIconSet empty_icon_set;
445
446  const int kRequestedSize = 128;
447  IconImage image(browser_context(),
448                  extension.get(),
449                  empty_icon_set,
450                  kRequestedSize,
451                  lazy_default_icon,
452                  this);
453
454  ASSERT_FALSE(lazy_default_icon.HasRepresentation(1.0f));
455
456  gfx::ImageSkiaRep representation = image.image_skia().GetRepresentation(1.0f);
457
458  // The resouce set is empty, so we should get the result right away.
459  EXPECT_TRUE(lazy_default_icon.HasRepresentation(1.0f));
460  EXPECT_TRUE(gfx::BitmapsAreEqual(
461      representation.sk_bitmap(),
462      EnsureBitmapSize(
463          default_icon.GetRepresentation(1.0f).sk_bitmap(),
464          kRequestedSize)));
465
466  // We should have a default icon representation.
467  ASSERT_EQ(1u, image.image_skia().image_reps().size());
468}
469
470// Test that IconImage works with lazily (but synchronously) created default
471// icon when IconImage returns asynchronously.
472TEST_F(ExtensionIconImageTest, LazyDefaultIcon_AsyncIconImage) {
473  scoped_refptr<Extension> extension(CreateExtension(
474      "extension_icon_image", Manifest::INVALID_LOCATION));
475  ASSERT_TRUE(extension.get() != NULL);
476
477  gfx::ImageSkia default_icon = GetDefaultIcon();
478  gfx::ImageSkia lazy_default_icon(new MockImageSkiaSource(default_icon),
479                                    default_icon.size());
480
481  const int kInvalidIconSize = 24;
482  ExtensionIconSet invalid_icon_set;
483  invalid_icon_set.Add(kInvalidIconSize, "invalid.png");
484
485  IconImage image(browser_context(),
486                  extension.get(),
487                  invalid_icon_set,
488                  kInvalidIconSize,
489                  lazy_default_icon,
490                  this);
491
492  ASSERT_FALSE(lazy_default_icon.HasRepresentation(1.0f));
493
494  gfx::ImageSkiaRep representation = image.image_skia().GetRepresentation(1.0f);
495
496  WaitForImageLoad();
497  EXPECT_EQ(1, ImageLoadedCount());
498  // We should have default icon representation now.
499  ASSERT_EQ(1u, image.image_skia().image_reps().size());
500
501  EXPECT_TRUE(lazy_default_icon.HasRepresentation(1.0f));
502
503  representation = image.image_skia().GetRepresentation(1.0f);
504  EXPECT_TRUE(gfx::BitmapsAreEqual(
505      representation.sk_bitmap(),
506      EnsureBitmapSize(
507          default_icon.GetRepresentation(1.0f).sk_bitmap(),
508          kInvalidIconSize)));
509}
510
511// Tests behavior of image created by IconImage after IconImage host goes
512// away. The image should still return loaded representations. If requested
513// representation was not loaded while IconImage host was around, transparent
514// representations should be returned.
515TEST_F(ExtensionIconImageTest, IconImageDestruction) {
516  scoped_refptr<Extension> extension(CreateExtension(
517      "extension_icon_image", Manifest::INVALID_LOCATION));
518  ASSERT_TRUE(extension.get() != NULL);
519
520  gfx::ImageSkia default_icon = GetDefaultIcon();
521
522  // Load images we expect to find as representations in icon_image, so we
523  // can later use them to validate icon_image.
524  SkBitmap bitmap_16 = GetTestBitmap(extension.get(), "16.png", 16);
525  ASSERT_FALSE(bitmap_16.empty());
526
527  scoped_ptr<IconImage> image(
528      new IconImage(browser_context(),
529                    extension.get(),
530                    IconsInfo::GetIcons(extension.get()),
531                    16,
532                    default_icon,
533                    this));
534
535  // Load an image representation.
536  gfx::ImageSkiaRep representation =
537      image->image_skia().GetRepresentation(1.0f);
538
539  WaitForImageLoad();
540  EXPECT_EQ(1, ImageLoadedCount());
541  ASSERT_EQ(1u, image->image_skia().image_reps().size());
542
543  // Stash loaded image skia, and destroy |image|.
544  gfx::ImageSkia image_skia = image->image_skia();
545  image.reset();
546  extension = NULL;
547
548  // Image skia should still be able to get previously loaded representation.
549  representation = image_skia.GetRepresentation(1.0f);
550
551  EXPECT_EQ(1.0f, representation.scale());
552  EXPECT_EQ(16, representation.pixel_width());
553  EXPECT_TRUE(gfx::BitmapsAreEqual(representation.sk_bitmap(), bitmap_16));
554
555  // When requesting another representation, we should not crash and return some
556  // image of the size. It could be blank or a rescale from the existing 1.0f
557  // icon.
558  representation = image_skia.GetRepresentation(2.0f);
559
560  EXPECT_EQ(16, representation.GetWidth());
561  EXPECT_EQ(16, representation.GetHeight());
562  EXPECT_EQ(2.0f, representation.scale());
563}
564
565}  // namespace extensions
566