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 "base/json/json_file_value_serializer.h" 8 #include "base/message_loop/message_loop.h" 9 #include "base/path_service.h" 10 #include "chrome/common/chrome_paths.h" 11 #include "chrome/test/base/testing_profile.h" 12 #include "content/public/test/test_browser_thread.h" 13 #include "extensions/browser/image_loader.h" 14 #include "extensions/common/extension.h" 15 #include "extensions/common/manifest.h" 16 #include "extensions/common/manifest_handlers/icons_handler.h" 17 #include "skia/ext/image_operations.h" 18 #include "testing/gtest/include/gtest/gtest.h" 19 #include "ui/base/resource/resource_bundle.h" 20 #include "ui/gfx/image/image_skia_source.h" 21 #include "ui/gfx/skia_util.h" 22 23 using content::BrowserThread; 24 using extensions::Extension; 25 using extensions::IconImage; 26 using extensions::Manifest; 27 28 namespace { 29 30 SkBitmap CreateBlankBitmapForScale(int size_dip, ui::ScaleFactor scale_factor) { 31 SkBitmap bitmap; 32 const float scale = ui::GetScaleForScaleFactor(scale_factor); 33 bitmap.setConfig(SkBitmap::kARGB_8888_Config, 34 static_cast<int>(size_dip * scale), 35 static_cast<int>(size_dip * scale)); 36 bitmap.allocPixels(); 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 extensions::ImageLoader image_loader_; 113 114 DISALLOW_COPY_AND_ASSIGN(TestImageLoader); 115 }; 116 117 class ExtensionIconImageTest : public testing::Test, 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 } 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(chrome::DIR_TEST_DATA, &test_file)) { 147 EXPECT_FALSE(true); 148 return NULL; 149 } 150 test_file = test_file.AppendASCII("extensions").AppendASCII(name); 151 int error_code = 0; 152 std::string error; 153 JSONFileValueSerializer serializer(test_file.AppendASCII("app.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 203 DISALLOW_COPY_AND_ASSIGN(ExtensionIconImageTest); 204 }; 205 206 } // namespace 207 208 TEST_F(ExtensionIconImageTest, Basic) { 209 std::vector<ui::ScaleFactor> supported_factors; 210 supported_factors.push_back(ui::SCALE_FACTOR_100P); 211 supported_factors.push_back(ui::SCALE_FACTOR_200P); 212 ui::test::ScopedSetSupportedScaleFactors scoped_supported(supported_factors); 213 scoped_ptr<content::BrowserContext> profile(new TestingProfile()); 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(profile.get(), 232 extension.get(), 233 extensions::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_ptr<content::BrowserContext> profile(new TestingProfile()); 288 scoped_refptr<Extension> extension(CreateExtension( 289 "extension_icon_image", Manifest::INVALID_LOCATION)); 290 ASSERT_TRUE(extension.get() != NULL); 291 292 gfx::ImageSkia default_icon = GetDefaultIcon(); 293 294 // Load images we expect to find as representations in icon_image, so we 295 // can later use them to validate icon_image. 296 SkBitmap bitmap_48 = GetTestBitmap(extension.get(), "48.png", 48); 297 ASSERT_FALSE(bitmap_48.empty()); 298 299 IconImage image(profile.get(), 300 extension.get(), 301 extensions::IconsInfo::GetIcons(extension.get()), 302 32, 303 default_icon, 304 this); 305 306 gfx::ImageSkiaRep representation = image.image_skia().GetRepresentation(2.0f); 307 308 WaitForImageLoad(); 309 EXPECT_EQ(1, ImageLoadedCount()); 310 ASSERT_EQ(1u, image.image_skia().image_reps().size()); 311 312 representation = image.image_skia().GetRepresentation(2.0f); 313 314 // We should have loaded the biggest smaller resource resized to the actual 315 // size. 316 EXPECT_EQ(2.0f, representation.scale()); 317 EXPECT_EQ(64, representation.pixel_width()); 318 EXPECT_TRUE(gfx::BitmapsAreEqual(representation.sk_bitmap(), 319 EnsureBitmapSize(bitmap_48, 64))); 320 } 321 322 // There is no resource with exact size, but there is a smaller and a bigger 323 // one. Requested size is smaller than 32 though, so the smaller resource should 324 // be loaded. 325 TEST_F(ExtensionIconImageTest, FallbackToSmaller) { 326 scoped_ptr<content::BrowserContext> profile(new TestingProfile()); 327 scoped_refptr<Extension> extension(CreateExtension( 328 "extension_icon_image", Manifest::INVALID_LOCATION)); 329 ASSERT_TRUE(extension.get() != NULL); 330 331 gfx::ImageSkia default_icon = GetDefaultIcon(); 332 333 // Load images we expect to find as representations in icon_image, so we 334 // can later use them to validate icon_image. 335 SkBitmap bitmap_16 = GetTestBitmap(extension.get(), "16.png", 16); 336 ASSERT_FALSE(bitmap_16.empty()); 337 338 IconImage image(profile.get(), 339 extension.get(), 340 extensions::IconsInfo::GetIcons(extension.get()), 341 17, 342 default_icon, 343 this); 344 345 gfx::ImageSkiaRep representation = image.image_skia().GetRepresentation(1.0f); 346 347 WaitForImageLoad(); 348 EXPECT_EQ(1, ImageLoadedCount()); 349 ASSERT_EQ(1u, image.image_skia().image_reps().size()); 350 351 representation = image.image_skia().GetRepresentation(1.0f); 352 353 // We should have loaded smaller (resized) resource. 354 EXPECT_EQ(1.0f, representation.scale()); 355 EXPECT_EQ(17, representation.pixel_width()); 356 EXPECT_TRUE(gfx::BitmapsAreEqual(representation.sk_bitmap(), 357 EnsureBitmapSize(bitmap_16, 17))); 358 } 359 360 // If resource set is empty, |GetRepresentation| should synchronously return 361 // default icon, without notifying observer of image change. 362 TEST_F(ExtensionIconImageTest, NoResources) { 363 scoped_ptr<content::BrowserContext> profile(new TestingProfile()); 364 scoped_refptr<Extension> extension(CreateExtension( 365 "extension_icon_image", Manifest::INVALID_LOCATION)); 366 ASSERT_TRUE(extension.get() != NULL); 367 368 ExtensionIconSet empty_icon_set; 369 gfx::ImageSkia default_icon = GetDefaultIcon(); 370 371 const int kRequestedSize = 24; 372 IconImage image(profile.get(), 373 extension.get(), 374 empty_icon_set, 375 kRequestedSize, 376 default_icon, 377 this); 378 379 gfx::ImageSkiaRep representation = image.image_skia().GetRepresentation(1.0f); 380 EXPECT_TRUE(gfx::BitmapsAreEqual( 381 representation.sk_bitmap(), 382 EnsureBitmapSize( 383 default_icon.GetRepresentation(1.0f).sk_bitmap(), 384 kRequestedSize))); 385 386 EXPECT_EQ(0, ImageLoadedCount()); 387 // We should have a default icon representation. 388 ASSERT_EQ(1u, image.image_skia().image_reps().size()); 389 390 representation = image.image_skia().GetRepresentation(1.0f); 391 EXPECT_TRUE(gfx::BitmapsAreEqual( 392 representation.sk_bitmap(), 393 EnsureBitmapSize( 394 default_icon.GetRepresentation(1.0f).sk_bitmap(), 395 kRequestedSize))); 396 } 397 398 // If resource set is invalid, image load should be done asynchronously and 399 // the observer should be notified when it's done. |GetRepresentation| should 400 // return the default icon representation once image load is done. 401 TEST_F(ExtensionIconImageTest, InvalidResource) { 402 scoped_ptr<content::BrowserContext> profile(new TestingProfile()); 403 scoped_refptr<Extension> extension(CreateExtension( 404 "extension_icon_image", Manifest::INVALID_LOCATION)); 405 ASSERT_TRUE(extension.get() != NULL); 406 407 const int kInvalidIconSize = 24; 408 ExtensionIconSet invalid_icon_set; 409 invalid_icon_set.Add(kInvalidIconSize, "invalid.png"); 410 411 gfx::ImageSkia default_icon = GetDefaultIcon(); 412 413 IconImage image(profile.get(), 414 extension.get(), 415 invalid_icon_set, 416 kInvalidIconSize, 417 default_icon, 418 this); 419 420 gfx::ImageSkiaRep representation = image.image_skia().GetRepresentation(1.0f); 421 EXPECT_TRUE(gfx::BitmapsAreEqual( 422 representation.sk_bitmap(), 423 CreateBlankBitmapForScale(kInvalidIconSize, ui::SCALE_FACTOR_100P))); 424 425 WaitForImageLoad(); 426 EXPECT_EQ(1, ImageLoadedCount()); 427 // We should have default icon representation now. 428 ASSERT_EQ(1u, image.image_skia().image_reps().size()); 429 430 representation = image.image_skia().GetRepresentation(1.0f); 431 EXPECT_TRUE(gfx::BitmapsAreEqual( 432 representation.sk_bitmap(), 433 EnsureBitmapSize( 434 default_icon.GetRepresentation(1.0f).sk_bitmap(), 435 kInvalidIconSize))); 436 } 437 438 // Test that IconImage works with lazily (but synchronously) created default 439 // icon when IconImage returns synchronously. 440 TEST_F(ExtensionIconImageTest, LazyDefaultIcon) { 441 scoped_ptr<content::BrowserContext> profile(new TestingProfile()); 442 scoped_refptr<Extension> extension(CreateExtension( 443 "extension_icon_image", Manifest::INVALID_LOCATION)); 444 ASSERT_TRUE(extension.get() != NULL); 445 446 gfx::ImageSkia default_icon = GetDefaultIcon(); 447 gfx::ImageSkia lazy_default_icon(new MockImageSkiaSource(default_icon), 448 default_icon.size()); 449 450 ExtensionIconSet empty_icon_set; 451 452 const int kRequestedSize = 128; 453 IconImage image(profile.get(), 454 extension.get(), 455 empty_icon_set, 456 kRequestedSize, 457 lazy_default_icon, 458 this); 459 460 ASSERT_FALSE(lazy_default_icon.HasRepresentation(1.0f)); 461 462 gfx::ImageSkiaRep representation = image.image_skia().GetRepresentation(1.0f); 463 464 // The resouce set is empty, so we should get the result right away. 465 EXPECT_TRUE(lazy_default_icon.HasRepresentation(1.0f)); 466 EXPECT_TRUE(gfx::BitmapsAreEqual( 467 representation.sk_bitmap(), 468 EnsureBitmapSize( 469 default_icon.GetRepresentation(1.0f).sk_bitmap(), 470 kRequestedSize))); 471 472 // We should have a default icon representation. 473 ASSERT_EQ(1u, image.image_skia().image_reps().size()); 474 } 475 476 // Test that IconImage works with lazily (but synchronously) created default 477 // icon when IconImage returns asynchronously. 478 TEST_F(ExtensionIconImageTest, LazyDefaultIcon_AsyncIconImage) { 479 scoped_ptr<content::BrowserContext> profile(new TestingProfile()); 480 scoped_refptr<Extension> extension(CreateExtension( 481 "extension_icon_image", Manifest::INVALID_LOCATION)); 482 ASSERT_TRUE(extension.get() != NULL); 483 484 gfx::ImageSkia default_icon = GetDefaultIcon(); 485 gfx::ImageSkia lazy_default_icon(new MockImageSkiaSource(default_icon), 486 default_icon.size()); 487 488 const int kInvalidIconSize = 24; 489 ExtensionIconSet invalid_icon_set; 490 invalid_icon_set.Add(kInvalidIconSize, "invalid.png"); 491 492 IconImage image(profile.get(), 493 extension.get(), 494 invalid_icon_set, 495 kInvalidIconSize, 496 lazy_default_icon, 497 this); 498 499 ASSERT_FALSE(lazy_default_icon.HasRepresentation(1.0f)); 500 501 gfx::ImageSkiaRep representation = image.image_skia().GetRepresentation(1.0f); 502 503 WaitForImageLoad(); 504 EXPECT_EQ(1, ImageLoadedCount()); 505 // We should have default icon representation now. 506 ASSERT_EQ(1u, image.image_skia().image_reps().size()); 507 508 EXPECT_TRUE(lazy_default_icon.HasRepresentation(1.0f)); 509 510 representation = image.image_skia().GetRepresentation(1.0f); 511 EXPECT_TRUE(gfx::BitmapsAreEqual( 512 representation.sk_bitmap(), 513 EnsureBitmapSize( 514 default_icon.GetRepresentation(1.0f).sk_bitmap(), 515 kInvalidIconSize))); 516 } 517 518 // Tests behavior of image created by IconImage after IconImage host goes 519 // away. The image should still return loaded representations. If requested 520 // representation was not loaded while IconImage host was around, transparent 521 // representations should be returned. 522 TEST_F(ExtensionIconImageTest, IconImageDestruction) { 523 scoped_ptr<content::BrowserContext> profile(new TestingProfile()); 524 scoped_refptr<Extension> extension(CreateExtension( 525 "extension_icon_image", Manifest::INVALID_LOCATION)); 526 ASSERT_TRUE(extension.get() != NULL); 527 528 gfx::ImageSkia default_icon = GetDefaultIcon(); 529 530 // Load images we expect to find as representations in icon_image, so we 531 // can later use them to validate icon_image. 532 SkBitmap bitmap_16 = GetTestBitmap(extension.get(), "16.png", 16); 533 ASSERT_FALSE(bitmap_16.empty()); 534 535 scoped_ptr<IconImage> image( 536 new IconImage(profile.get(), 537 extension.get(), 538 extensions::IconsInfo::GetIcons(extension.get()), 539 16, 540 default_icon, 541 this)); 542 543 // Load an image representation. 544 gfx::ImageSkiaRep representation = 545 image->image_skia().GetRepresentation(1.0f); 546 547 WaitForImageLoad(); 548 EXPECT_EQ(1, ImageLoadedCount()); 549 ASSERT_EQ(1u, image->image_skia().image_reps().size()); 550 551 // Stash loaded image skia, and destroy |image|. 552 gfx::ImageSkia image_skia = image->image_skia(); 553 image.reset(); 554 extension = NULL; 555 556 // Image skia should still be able to get previously loaded representation. 557 representation = image_skia.GetRepresentation(1.0f); 558 559 EXPECT_EQ(1.0f, representation.scale()); 560 EXPECT_EQ(16, representation.pixel_width()); 561 EXPECT_TRUE(gfx::BitmapsAreEqual(representation.sk_bitmap(), bitmap_16)); 562 563 // When requesting another representation, we should not crash and return some 564 // image of the size. It could be blank or a rescale from the existing 1.0f 565 // icon. 566 representation = image_skia.GetRepresentation(2.0f); 567 568 EXPECT_EQ(16, representation.GetWidth()); 569 EXPECT_EQ(16, representation.GetHeight()); 570 EXPECT_EQ(2.0f, representation.scale()); 571 } 572