Home | History | Annotate | Download | only in browser
      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 
     27 using content::BrowserThread;
     28 
     29 namespace extensions {
     30 namespace {
     31 
     32 SkBitmap 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 
     41 SkBitmap 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.
     53 class 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.
     69 class 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 
    117 class 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 
    209 TEST_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.
    282 TEST_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.
    323 TEST_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.
    359 TEST_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.
    397 TEST_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.
    435 TEST_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.
    472 TEST_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.
    515 TEST_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