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