diff options
author | dewittj@chromium.org <dewittj@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-06-04 08:40:41 +0000 |
---|---|---|
committer | dewittj@chromium.org <dewittj@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-06-04 08:40:41 +0000 |
commit | a4af7d5f79be09c03b36d86964500195aec4fc70 (patch) | |
tree | f93d0da2200bc0f40becf52e862aa1dfde32c314 /chrome | |
parent | c00b8385572f791022c063546ac30e106b4981a1 (diff) | |
download | chromium_src-a4af7d5f79be09c03b36d86964500195aec4fc70.zip chromium_src-a4af7d5f79be09c03b36d86964500195aec4fc70.tar.gz chromium_src-a4af7d5f79be09c03b36d86964500195aec4fc70.tar.bz2 |
Reland: Allow high-res bitmaps to be passed in from notifications API.
Previously the custom binding in JS would prevent an image that's larger
than the size of the template from being sent to the browser process.
This relaxes the maximum to be the largest supported scale factor of the machine.
TBR=kalman@chromium.org,dimich@chromium.org
BUG=239676
Review URL: https://codereview.chromium.org/291193009
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@274730 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome')
21 files changed, 926 insertions, 672 deletions
diff --git a/chrome/browser/extensions/api/notifications/notifications_api.cc b/chrome/browser/extensions/api/notifications/notifications_api.cc index 53c94c7..2e6d46f 100644 --- a/chrome/browser/extensions/api/notifications/notifications_api.cc +++ b/chrome/browser/extensions/api/notifications/notifications_api.cc @@ -17,6 +17,7 @@ #include "chrome/browser/notifications/notification_ui_manager.h" #include "chrome/browser/profiles/profile.h" #include "chrome/common/chrome_version_info.h" +#include "chrome/common/extensions/api/notifications/notification_style.h" #include "content/public/browser/render_process_host.h" #include "content/public/browser/render_view_host.h" #include "content/public/browser/web_contents.h" @@ -24,6 +25,7 @@ #include "extensions/common/extension.h" #include "extensions/common/features/feature.h" #include "third_party/skia/include/core/SkBitmap.h" +#include "ui/base/layout.h" #include "ui/gfx/image/image.h" #include "ui/gfx/image/image_skia.h" #include "ui/gfx/image/image_skia_rep.h" @@ -40,28 +42,37 @@ namespace { const char kMissingRequiredPropertiesForCreateNotification[] = "Some of the required properties are missing: type, iconUrl, title and " "message."; +const char kUnableToDecodeIconError[] = + "Unable to successfully use the provided image."; const char kUnexpectedProgressValueForNonProgressType[] = "The progress value should not be specified for non-progress notification"; const char kInvalidProgressValue[] = "The progress value should range from 0 to 100"; +const char kExtraListItemsProvided[] = + "List items provided for notification type != list"; +const char kExtraImageProvided[] = + "Image resource provided for notification type != image"; // Converts an object with width, height, and data in RGBA format into an // gfx::Image (in ARGB format). bool NotificationBitmapToGfxImage( + float max_scale, + const gfx::Size& target_size_dips, api::notifications::NotificationBitmap* notification_bitmap, gfx::Image* return_image) { if (!notification_bitmap) return false; - // Ensure a sane set of dimensions. - const int max_width = message_center::kNotificationPreferredImageWidth; - const int max_height = message_center::kNotificationPreferredImageHeight; + const int max_device_pixel_width = target_size_dips.width() * max_scale; + const int max_device_pixel_height = target_size_dips.height() * max_scale; + const int BYTES_PER_PIXEL = 4; const int width = notification_bitmap->width; const int height = notification_bitmap->height; - if (width < 0 || height < 0 || width > max_width || height > max_height) + if (width < 0 || height < 0 || width > max_device_pixel_width || + height > max_device_pixel_height) return false; // Ensure we have rgba data. @@ -100,7 +111,8 @@ bool NotificationBitmapToGfxImage( ((c_rgba_data[rgba_index + 2] & 0xFF) << 0)); } - // TODO(dewittj): Handle HiDPI images. + // TODO(dewittj): Handle HiDPI images with more than one scale factor + // representation. gfx::ImageSkia skia(gfx::ImageSkiaRep(bitmap, 1.0f)); *return_image = gfx::Image(skia); return true; @@ -254,6 +266,11 @@ bool NotificationsApiFunction::CreateNotification( return false; } + NotificationBitmapSizes bitmap_sizes = GetNotificationBitmapSizes(); + + float image_scale = + ui::GetScaleForScaleFactor(ui::GetSupportedScaleFactors().back()); + // Extract required fields: type, title, message, and icon. message_center::NotificationType type = MapApiTemplateTypeToType(options->type); @@ -261,8 +278,13 @@ bool NotificationsApiFunction::CreateNotification( const base::string16 message(base::UTF8ToUTF16(*options->message)); gfx::Image icon; - // TODO(dewittj): Return error if this fails. - NotificationBitmapToGfxImage(options->icon_bitmap.get(), &icon); + if (!NotificationBitmapToGfxImage(image_scale, + bitmap_sizes.icon_size, + options->icon_bitmap.get(), + &icon)) { + SetError(kUnableToDecodeIconError); + return false; + } // Then, handle any optional data that's been provided. message_center::RichNotificationData optional_fields; @@ -280,8 +302,10 @@ bool NotificationsApiFunction::CreateNotification( for (size_t i = 0; i < number_of_buttons; i++) { message_center::ButtonInfo info( base::UTF8ToUTF16((*options->buttons)[i]->title)); - NotificationBitmapToGfxImage((*options->buttons)[i]->icon_bitmap.get(), - &info.icon); + NotificationBitmapToGfxImage(image_scale, + bitmap_sizes.button_icon_size, + (*options->buttons)[i]->icon_bitmap.get(), + &info.icon); optional_fields.buttons.push_back(info); } } @@ -291,16 +315,22 @@ bool NotificationsApiFunction::CreateNotification( base::UTF8ToUTF16(*options->context_message); } - bool has_image = NotificationBitmapToGfxImage(options->image_bitmap.get(), + bool has_image = NotificationBitmapToGfxImage(image_scale, + bitmap_sizes.image_size, + options->image_bitmap.get(), &optional_fields.image); // We should have an image if and only if the type is an image type. - if (has_image != (type == message_center::NOTIFICATION_TYPE_IMAGE)) + if (has_image != (type == message_center::NOTIFICATION_TYPE_IMAGE)) { + SetError(kExtraImageProvided); return false; + } // We should have list items if and only if the type is a multiple type. bool has_list_items = options->items.get() && options->items->size() > 0; - if (has_list_items != (type == message_center::NOTIFICATION_TYPE_MULTIPLE)) + if (has_list_items != (type == message_center::NOTIFICATION_TYPE_MULTIPLE)) { + SetError(kExtraListItemsProvided); return false; + } if (options->progress.get() != NULL) { // We should have progress if and only if the type is a progress type. @@ -355,6 +385,10 @@ bool NotificationsApiFunction::UpdateNotification( const std::string& id, api::notifications::NotificationOptions* options, Notification* notification) { + NotificationBitmapSizes bitmap_sizes = GetNotificationBitmapSizes(); + float image_scale = + ui::GetScaleForScaleFactor(ui::GetSupportedScaleFactors().back()); + // Update optional fields if provided. if (options->type != api::notifications::TEMPLATE_TYPE_NONE) notification->set_type(MapApiTemplateTypeToType(options->type)); @@ -366,7 +400,8 @@ bool NotificationsApiFunction::UpdateNotification( // TODO(dewittj): Return error if this fails. if (options->icon_bitmap) { gfx::Image icon; - NotificationBitmapToGfxImage(options->icon_bitmap.get(), &icon); + NotificationBitmapToGfxImage( + image_scale, bitmap_sizes.icon_size, options->icon_bitmap.get(), &icon); notification->set_icon(icon); } @@ -385,8 +420,10 @@ bool NotificationsApiFunction::UpdateNotification( for (size_t i = 0; i < number_of_buttons; i++) { message_center::ButtonInfo button( base::UTF8ToUTF16((*options->buttons)[i]->title)); - NotificationBitmapToGfxImage((*options->buttons)[i]->icon_bitmap.get(), - &button.icon); + NotificationBitmapToGfxImage(image_scale, + bitmap_sizes.button_icon_size, + (*options->buttons)[i]->icon_bitmap.get(), + &button.icon); buttons.push_back(button); } notification->set_buttons(buttons); @@ -398,10 +435,16 @@ bool NotificationsApiFunction::UpdateNotification( } gfx::Image image; - if (NotificationBitmapToGfxImage(options->image_bitmap.get(), &image)) { + bool has_image = NotificationBitmapToGfxImage(image_scale, + bitmap_sizes.image_size, + options->image_bitmap.get(), + &image); + if (has_image) { // We should have an image if and only if the type is an image type. - if (notification->type() != message_center::NOTIFICATION_TYPE_IMAGE) + if (notification->type() != message_center::NOTIFICATION_TYPE_IMAGE) { + SetError(kExtraImageProvided); return false; + } notification->set_image(image); } @@ -422,8 +465,10 @@ bool NotificationsApiFunction::UpdateNotification( if (options->items.get() && options->items->size() > 0) { // We should have list items if and only if the type is a multiple type. - if (notification->type() != message_center::NOTIFICATION_TYPE_MULTIPLE) + if (notification->type() != message_center::NOTIFICATION_TYPE_MULTIPLE) { + SetError(kExtraListItemsProvided); return false; + } std::vector<message_center::NotificationItem> items; using api::notifications::NotificationItem; diff --git a/chrome/browser/extensions/api/notifications/notifications_apitest.cc b/chrome/browser/extensions/api/notifications/notifications_apitest.cc index 5c06792..7c6a97b 100644 --- a/chrome/browser/extensions/api/notifications/notifications_apitest.cc +++ b/chrome/browser/extensions/api/notifications/notifications_apitest.cc @@ -10,6 +10,8 @@ #include "chrome/browser/extensions/api/notifications/notifications_api.h" #include "chrome/browser/extensions/extension_apitest.h" #include "chrome/browser/extensions/extension_function_test_utils.h" +#include "chrome/browser/notifications/notification.h" +#include "chrome/browser/notifications/notification_ui_manager.h" #include "content/public/browser/notification_service.h" #include "content/public/test/test_utils.h" #include "extensions/browser/api/test/test_api.h" @@ -93,409 +95,8 @@ class NotificationsApiTest : public ExtensionApiTest { } // namespace -IN_PROC_BROWSER_TEST_F(NotificationsApiTest, TestIdUsage) { - // Create a new notification. A lingering output of this block is the - // notifications ID, which we'll use in later parts of this test. - std::string notification_id; - scoped_refptr<Extension> empty_extension(utils::CreateEmptyExtension()); - { - scoped_refptr<extensions::NotificationsCreateFunction> - notification_function( - new extensions::NotificationsCreateFunction()); - - notification_function->set_extension(empty_extension.get()); - notification_function->set_has_callback(true); - - scoped_ptr<base::Value> result(utils::RunFunctionAndReturnSingleResult( - notification_function.get(), - "[\"\", " // Empty string: ask API to generate ID - "{" - "\"type\": \"basic\"," - "\"iconUrl\": \"an/image/that/does/not/exist.png\"," - "\"title\": \"Attention!\"," - "\"message\": \"Check out Cirque du Soleil\"" - "}]", - browser(), - utils::NONE)); - - ASSERT_EQ(base::Value::TYPE_STRING, result->GetType()); - ASSERT_TRUE(result->GetAsString(¬ification_id)); - ASSERT_TRUE(notification_id.length() > 0); - } - - // Update the existing notification. - { - scoped_refptr<extensions::NotificationsUpdateFunction> - notification_function( - new extensions::NotificationsUpdateFunction()); - - notification_function->set_extension(empty_extension.get()); - notification_function->set_has_callback(true); - - scoped_ptr<base::Value> result(utils::RunFunctionAndReturnSingleResult( - notification_function.get(), - "[\"" + notification_id + - "\", " - "{" - "\"type\": \"basic\"," - "\"iconUrl\": \"an/image/that/does/not/exist.png\"," - "\"title\": \"Attention!\"," - "\"message\": \"Too late! The show ended yesterday\"" - "}]", - browser(), - utils::NONE)); - - ASSERT_EQ(base::Value::TYPE_BOOLEAN, result->GetType()); - bool copy_bool_value = false; - ASSERT_TRUE(result->GetAsBoolean(©_bool_value)); - ASSERT_TRUE(copy_bool_value); - - // TODO(miket): add a testing method to query the message from the - // displayed notification, and assert it matches the updated message. - // - // TODO(miket): add a method to count the number of outstanding - // notifications, and confirm it remains at one at this point. - } - - // Update a nonexistent notification. - { - scoped_refptr<extensions::NotificationsUpdateFunction> - notification_function( - new extensions::NotificationsUpdateFunction()); - - notification_function->set_extension(empty_extension.get()); - notification_function->set_has_callback(true); - - scoped_ptr<base::Value> result(utils::RunFunctionAndReturnSingleResult( - notification_function.get(), - "[\"xxxxxxxxxxxx\", " - "{" - "\"type\": \"basic\"," - "\"iconUrl\": \"an/image/that/does/not/exist.png\"," - "\"title\": \"!\"," - "\"message\": \"!\"" - "}]", - browser(), - utils::NONE)); - - ASSERT_EQ(base::Value::TYPE_BOOLEAN, result->GetType()); - bool copy_bool_value = false; - ASSERT_TRUE(result->GetAsBoolean(©_bool_value)); - ASSERT_FALSE(copy_bool_value); - } - - // Clear a nonexistent notification. - { - scoped_refptr<extensions::NotificationsClearFunction> - notification_function( - new extensions::NotificationsClearFunction()); - - notification_function->set_extension(empty_extension.get()); - notification_function->set_has_callback(true); - - scoped_ptr<base::Value> result( - utils::RunFunctionAndReturnSingleResult(notification_function.get(), - "[\"xxxxxxxxxxx\"]", - browser(), - utils::NONE)); - - ASSERT_EQ(base::Value::TYPE_BOOLEAN, result->GetType()); - bool copy_bool_value = false; - ASSERT_TRUE(result->GetAsBoolean(©_bool_value)); - ASSERT_FALSE(copy_bool_value); - } - - // Clear the notification we created. - { - scoped_refptr<extensions::NotificationsClearFunction> - notification_function( - new extensions::NotificationsClearFunction()); - - notification_function->set_extension(empty_extension.get()); - notification_function->set_has_callback(true); - - scoped_ptr<base::Value> result( - utils::RunFunctionAndReturnSingleResult(notification_function.get(), - "[\"" + notification_id + "\"]", - browser(), - utils::NONE)); - - ASSERT_EQ(base::Value::TYPE_BOOLEAN, result->GetType()); - bool copy_bool_value = false; - ASSERT_TRUE(result->GetAsBoolean(©_bool_value)); - ASSERT_TRUE(copy_bool_value); - } -} - -IN_PROC_BROWSER_TEST_F(NotificationsApiTest, TestBaseFormatNotification) { - scoped_refptr<Extension> empty_extension(utils::CreateEmptyExtension()); - - // Create a new notification with the minimum required properties. - { - scoped_refptr<extensions::NotificationsCreateFunction> - notification_create_function( - new extensions::NotificationsCreateFunction()); - notification_create_function->set_extension(empty_extension.get()); - notification_create_function->set_has_callback(true); - - scoped_ptr<base::Value> result(utils::RunFunctionAndReturnSingleResult( - notification_create_function.get(), - "[\"\", " - "{" - "\"type\": \"basic\"," - "\"iconUrl\": \"an/image/that/does/not/exist.png\"," - "\"title\": \"Attention!\"," - "\"message\": \"Check out Cirque du Soleil\"" - "}]", - browser(), - utils::NONE)); - - std::string notification_id; - ASSERT_EQ(base::Value::TYPE_STRING, result->GetType()); - ASSERT_TRUE(result->GetAsString(¬ification_id)); - ASSERT_TRUE(notification_id.length() > 0); - } - - // Create another new notification with more than the required properties. - { - scoped_refptr<extensions::NotificationsCreateFunction> - notification_create_function( - new extensions::NotificationsCreateFunction()); - notification_create_function->set_extension(empty_extension.get()); - notification_create_function->set_has_callback(true); - - scoped_ptr<base::Value> result(utils::RunFunctionAndReturnSingleResult( - notification_create_function.get(), - "[\"\", " - "{" - "\"type\": \"basic\"," - "\"iconUrl\": \"an/image/that/does/not/exist.png\"," - "\"title\": \"Attention!\"," - "\"message\": \"Check out Cirque du Soleil\"," - "\"priority\": 1," - "\"eventTime\": 1234567890.12345678," - "\"buttons\": [" - " {" - " \"title\": \"Up\"," - " \"iconUrl\":\"http://www.google.com/logos/2012/\"" - " }," - " {" - " \"title\": \"Down\"" // note: no iconUrl - " }" - "]," - "\"expandedMessage\": \"This is a longer expanded message.\"," - "\"imageUrl\": \"http://www.google.com/logos/2012/election12-hp.jpg\"" - "}]", - browser(), - utils::NONE)); - - std::string notification_id; - ASSERT_EQ(base::Value::TYPE_STRING, result->GetType()); - ASSERT_TRUE(result->GetAsString(¬ification_id)); - ASSERT_TRUE(notification_id.length() > 0); - } - - // Error case: missing type property. - { - scoped_refptr<extensions::NotificationsCreateFunction> - notification_create_function( - new extensions::NotificationsCreateFunction()); - notification_create_function->set_extension(empty_extension.get()); - notification_create_function->set_has_callback(true); - - utils::RunFunction( - notification_create_function.get(), - "[\"\", " - "{" - "\"iconUrl\": \"an/image/that/does/not/exist.png\"," - "\"title\": \"Attention!\"," - "\"message\": \"Check out Cirque du Soleil\"" - "}]", - browser(), - utils::NONE); - - EXPECT_FALSE(notification_create_function->GetError().empty()); - } - - // Error case: missing iconUrl property. - { - scoped_refptr<extensions::NotificationsCreateFunction> - notification_create_function( - new extensions::NotificationsCreateFunction()); - notification_create_function->set_extension(empty_extension.get()); - notification_create_function->set_has_callback(true); - - utils::RunFunction( - notification_create_function.get(), - "[\"\", " - "{" - "\"type\": \"basic\"," - "\"title\": \"Attention!\"," - "\"message\": \"Check out Cirque du Soleil\"" - "}]", - browser(), - utils::NONE); - - EXPECT_FALSE(notification_create_function->GetError().empty()); - } - - // Error case: missing title property. - { - scoped_refptr<extensions::NotificationsCreateFunction> - notification_create_function( - new extensions::NotificationsCreateFunction()); - notification_create_function->set_extension(empty_extension.get()); - notification_create_function->set_has_callback(true); - - utils::RunFunction( - notification_create_function.get(), - "[\"\", " - "{" - "\"type\": \"basic\"," - "\"iconUrl\": \"an/image/that/does/not/exist.png\"," - "\"message\": \"Check out Cirque du Soleil\"" - "}]", - browser(), - utils::NONE); - - EXPECT_FALSE(notification_create_function->GetError().empty()); - } - - // Error case: missing message property. - { - scoped_refptr<extensions::NotificationsCreateFunction> - notification_create_function( - new extensions::NotificationsCreateFunction()); - notification_create_function->set_extension(empty_extension.get()); - notification_create_function->set_has_callback(true); - - utils::RunFunction( - notification_create_function.get(), - "[\"\", " - "{" - "\"type\": \"basic\"," - "\"iconUrl\": \"an/image/that/does/not/exist.png\"," - "\"title\": \"Attention!\"" - "}]", - browser(), - utils::NONE); - - EXPECT_FALSE(notification_create_function->GetError().empty()); - } -} - -IN_PROC_BROWSER_TEST_F(NotificationsApiTest, TestMultipleItemNotification) { - scoped_refptr<extensions::NotificationsCreateFunction> - notification_create_function( - new extensions::NotificationsCreateFunction()); - scoped_refptr<Extension> empty_extension(utils::CreateEmptyExtension()); - - notification_create_function->set_extension(empty_extension.get()); - notification_create_function->set_has_callback(true); - - scoped_ptr<base::Value> result(utils::RunFunctionAndReturnSingleResult( - notification_create_function.get(), - "[\"\", " - "{" - "\"type\": \"list\"," - "\"iconUrl\": \"an/image/that/does/not/exist.png\"," - "\"title\": \"Multiple Item Notification Title\"," - "\"message\": \"Multiple item notification message.\"," - "\"items\": [" - " {\"title\": \"Brett Boe\"," - " \"message\": \"This is an important message!\"}," - " {\"title\": \"Carla Coe\"," - " \"message\": \"Just took a look at the proposal\"}," - " {\"title\": \"Donna Doe\"," - " \"message\": \"I see that you went to the conference\"}," - " {\"title\": \"Frank Foe\"," - " \"message\": \"I ate Harry's sandwich!\"}," - " {\"title\": \"Grace Goe\"," - " \"message\": \"I saw Frank steal a sandwich :-)\"}" - "]," - "\"priority\": 1," - "\"eventTime\": 1361488019.9999999" - "}]", - browser(), - utils::NONE)); - // TODO(dharcourt): [...], items = [{title: foo, message: bar}, ...], [...] - - std::string notification_id; - ASSERT_EQ(base::Value::TYPE_STRING, result->GetType()); - ASSERT_TRUE(result->GetAsString(¬ification_id)); - ASSERT_TRUE(notification_id.length() > 0); -} - -#if defined(OS_LINUX) -#define MAYBE_TestGetAll DISABLED_TestGetAll -#else -#define MAYBE_TestGetAll TestGetAll -#endif - -IN_PROC_BROWSER_TEST_F(NotificationsApiTest, MAYBE_TestGetAll) { - scoped_refptr<Extension> empty_extension(utils::CreateEmptyExtension()); - - { - scoped_refptr<extensions::NotificationsGetAllFunction> - notification_get_all_function( - new extensions::NotificationsGetAllFunction()); - notification_get_all_function->set_extension(empty_extension.get()); - notification_get_all_function->set_has_callback(true); - scoped_ptr<base::Value> result(utils::RunFunctionAndReturnSingleResult( - notification_get_all_function.get(), "[]", browser(), utils::NONE)); - - base::DictionaryValue* return_value; - ASSERT_EQ(base::Value::TYPE_DICTIONARY, result->GetType()); - ASSERT_TRUE(result->GetAsDictionary(&return_value)); - ASSERT_TRUE(return_value->size() == 0); - } - - const unsigned int kNotificationsToCreate = 4; - - for (unsigned int i = 0; i < kNotificationsToCreate; i++) { - scoped_refptr<extensions::NotificationsCreateFunction> - notification_create_function( - new extensions::NotificationsCreateFunction()); - - notification_create_function->set_extension(empty_extension.get()); - notification_create_function->set_has_callback(true); - - scoped_ptr<base::Value> result(utils::RunFunctionAndReturnSingleResult( - notification_create_function.get(), - base::StringPrintf("[\"identifier-%u\", " - "{" - "\"type\": \"basic\"," - "\"iconUrl\": \"an/image/that/does/not/exist.png\"," - "\"title\": \"Title\"," - "\"message\": \"Message.\"," - "\"priority\": 1," - "\"eventTime\": 1361488019.9999999" - "}]", - i), - browser(), - utils::NONE)); - } - - { - scoped_refptr<extensions::NotificationsGetAllFunction> - notification_get_all_function( - new extensions::NotificationsGetAllFunction()); - notification_get_all_function->set_extension(empty_extension.get()); - notification_get_all_function->set_has_callback(true); - scoped_ptr<base::Value> result(utils::RunFunctionAndReturnSingleResult( - notification_get_all_function.get(), "[]", browser(), utils::NONE)); - - base::DictionaryValue* return_value; - ASSERT_EQ(base::Value::TYPE_DICTIONARY, result->GetType()); - ASSERT_TRUE(result->GetAsDictionary(&return_value)); - ASSERT_EQ(return_value->size(), kNotificationsToCreate); - bool dictionary_bool = false; - for (unsigned int i = 0; i < kNotificationsToCreate; i++) { - std::string id = base::StringPrintf("identifier-%u", i); - ASSERT_TRUE(return_value->GetBoolean(id, &dictionary_bool)); - ASSERT_TRUE(dictionary_bool); - } - } +IN_PROC_BROWSER_TEST_F(NotificationsApiTest, TestBasicUsage) { + ASSERT_TRUE(RunExtensionTest("notifications/api/basic_usage")) << message_; } IN_PROC_BROWSER_TEST_F(NotificationsApiTest, TestEvents) { @@ -526,235 +127,34 @@ IN_PROC_BROWSER_TEST_F(NotificationsApiTest, TestByUser) { true); EXPECT_TRUE(catcher.GetNextResult()) << catcher.message(); } -} - - -#if defined(OS_LINUX) -#define MAYBE_TestProgressNotification DISABLED_TestProgressNotification -#else -#define MAYBE_TestProgressNotification TestProgressNotification -#endif - -IN_PROC_BROWSER_TEST_F(NotificationsApiTest, MAYBE_TestProgressNotification) { - scoped_refptr<Extension> empty_extension(utils::CreateEmptyExtension()); - - // Create a new progress notification. - std::string notification_id; - { - scoped_refptr<extensions::NotificationsCreateFunction> - notification_create_function( - new extensions::NotificationsCreateFunction()); - notification_create_function->set_extension(empty_extension.get()); - notification_create_function->set_has_callback(true); - - scoped_ptr<base::Value> result(utils::RunFunctionAndReturnSingleResult( - notification_create_function.get(), - "[\"\", " - "{" - "\"type\": \"progress\"," - "\"iconUrl\": \"an/image/that/does/not/exist.png\"," - "\"title\": \"Test!\"," - "\"message\": \"This is a progress notification.\"," - "\"priority\": 1," - "\"eventTime\": 1234567890.12345678," - "\"progress\": 30" - "}]", - browser(), - utils::NONE)); - - EXPECT_EQ(base::Value::TYPE_STRING, result->GetType()); - EXPECT_TRUE(result->GetAsString(¬ification_id)); - EXPECT_TRUE(notification_id.length() > 0); - } - - // Update the progress property only. - { - scoped_refptr<extensions::NotificationsUpdateFunction> - notification_function( - new extensions::NotificationsUpdateFunction()); - notification_function->set_extension(empty_extension.get()); - notification_function->set_has_callback(true); - - scoped_ptr<base::Value> result(utils::RunFunctionAndReturnSingleResult( - notification_function.get(), - "[\"" + notification_id + - "\", " - "{" - "\"progress\": 60" - "}]", - browser(), - utils::NONE)); - - EXPECT_EQ(base::Value::TYPE_BOOLEAN, result->GetType()); - bool copy_bool_value = false; - EXPECT_TRUE(result->GetAsBoolean(©_bool_value)); - EXPECT_TRUE(copy_bool_value); - } - // Error case: progress value provided for non-progress type. { - scoped_refptr<extensions::NotificationsCreateFunction> - notification_create_function( - new extensions::NotificationsCreateFunction()); - notification_create_function->set_extension(empty_extension.get()); - notification_create_function->set_has_callback(true); - - utils::RunFunction( - notification_create_function.get(), - "[\"\", " - "{" - "\"type\": \"basic\"," - "\"iconUrl\": \"an/image/that/does/not/exist.png\"," - "\"title\": \"Test!\"," - "\"message\": \"This is a progress notification.\"," - "\"priority\": 1," - "\"eventTime\": 1234567890.12345678," - "\"progress\": 10" - "}]", - browser(), - utils::NONE); - EXPECT_FALSE(notification_create_function->GetError().empty()); + ResultCatcher catcher; + g_browser_process->message_center()->RemoveAllNotifications(false); + EXPECT_TRUE(catcher.GetNextResult()) << catcher.message(); } - - // Error case: progress value less than lower bound. { - scoped_refptr<extensions::NotificationsCreateFunction> - notification_create_function( - new extensions::NotificationsCreateFunction()); - notification_create_function->set_extension(empty_extension.get()); - notification_create_function->set_has_callback(true); - - utils::RunFunction( - notification_create_function.get(), - "[\"\", " - "{" - "\"type\": \"progress\"," - "\"iconUrl\": \"an/image/that/does/not/exist.png\"," - "\"title\": \"Test!\"," - "\"message\": \"This is a progress notification.\"," - "\"priority\": 1," - "\"eventTime\": 1234567890.12345678," - "\"progress\": -10" - "}]", - browser(), - utils::NONE); - EXPECT_FALSE(notification_create_function->GetError().empty()); - } - - // Error case: progress value greater than upper bound. - { - scoped_refptr<extensions::NotificationsCreateFunction> - notification_create_function( - new extensions::NotificationsCreateFunction()); - notification_create_function->set_extension(empty_extension.get()); - notification_create_function->set_has_callback(true); - - utils::RunFunction( - notification_create_function.get(), - "[\"\", " - "{" - "\"type\": \"progress\"," - "\"iconUrl\": \"an/image/that/does/not/exist.png\"," - "\"title\": \"Test!\"," - "\"message\": \"This is a progress notification.\"," - "\"priority\": 1," - "\"eventTime\": 1234567890.12345678," - "\"progress\": 101" - "}]", - browser(), - utils::NONE); - EXPECT_FALSE(notification_create_function->GetError().empty()); + ResultCatcher catcher; + g_browser_process->message_center()->RemoveAllNotifications(true); + EXPECT_TRUE(catcher.GetNextResult()) << catcher.message(); } } IN_PROC_BROWSER_TEST_F(NotificationsApiTest, TestPartialUpdate) { - scoped_refptr<Extension> empty_extension(utils::CreateEmptyExtension()); - - // Create a new notification. - std::string notification_id; - { - scoped_refptr<extensions::NotificationsCreateFunction> - notification_function( - new extensions::NotificationsCreateFunction()); - notification_function->set_extension(empty_extension.get()); - notification_function->set_has_callback(true); - - scoped_ptr<base::Value> result(utils::RunFunctionAndReturnSingleResult( - notification_function.get(), - "[\"\", " // Empty string: ask API to generate ID - "{" - "\"type\": \"basic\"," - "\"iconUrl\": \"an/image/that/does/not/exist.png\"," - "\"title\": \"Attention!\"," - "\"message\": \"Check out Cirque du Soleil\"," - "\"buttons\": [{\"title\": \"Button\"}]" - "}]", - browser(), - utils::NONE)); - - ASSERT_EQ(base::Value::TYPE_STRING, result->GetType()); - ASSERT_TRUE(result->GetAsString(¬ification_id)); - ASSERT_TRUE(notification_id.length() > 0); - } + ASSERT_TRUE(RunExtensionTest("notifications/api/partial_update")) << message_; + const extensions::Extension* extension = GetSingleLoadedExtension(); + ASSERT_TRUE(extension) << message_; - // Update a few properties in the existing notification. const char kNewTitle[] = "Changed!"; const char kNewMessage[] = "Too late! The show ended yesterday"; - { - scoped_refptr<extensions::NotificationsUpdateFunction> - notification_function( - new extensions::NotificationsUpdateFunction()); - notification_function->set_extension(empty_extension.get()); - notification_function->set_has_callback(true); - - scoped_ptr<base::Value> result(utils::RunFunctionAndReturnSingleResult( - notification_function.get(), - "[\"" + notification_id + - "\", " - "{" - "\"title\": \"" + kNewTitle + "\"," - "\"message\": \"" + kNewMessage + "\"" - "}]", - browser(), - utils::NONE)); - - ASSERT_EQ(base::Value::TYPE_BOOLEAN, result->GetType()); - bool copy_bool_value = false; - ASSERT_TRUE(result->GetAsBoolean(©_bool_value)); - ASSERT_TRUE(copy_bool_value); - } - - // Update some other properties in the existing notification. int kNewPriority = 2; - { - scoped_refptr<extensions::NotificationsUpdateFunction> - notification_function( - new extensions::NotificationsUpdateFunction()); - notification_function->set_extension(empty_extension.get()); - notification_function->set_has_callback(true); - - scoped_ptr<base::Value> result(utils::RunFunctionAndReturnSingleResult( - notification_function.get(), - "[\"" + notification_id + - "\", " - "{" - "\"priority\": " + base::IntToString(kNewPriority) + "," - "\"buttons\": []" - "}]", - browser(), - utils::NONE)); - ASSERT_EQ(base::Value::TYPE_BOOLEAN, result->GetType()); - bool copy_bool_value = false; - ASSERT_TRUE(result->GetAsBoolean(©_bool_value)); - ASSERT_TRUE(copy_bool_value); - } - - // Get the updated notification and verify its data. const message_center::NotificationList::Notifications& notifications = g_browser_process->message_center()->GetVisibleNotifications(); ASSERT_EQ(1u, notifications.size()); message_center::Notification* notification = *(notifications.begin()); + LOG(INFO) << "Notification ID: " << notification->id(); + EXPECT_EQ(base::ASCIIToUTF16(kNewTitle), notification->title()); EXPECT_EQ(base::ASCIIToUTF16(kNewMessage), notification->message()); EXPECT_EQ(kNewPriority, notification->priority()); diff --git a/chrome/chrome_common.gypi b/chrome/chrome_common.gypi index afd5f98..22a9ef6 100644 --- a/chrome/chrome_common.gypi +++ b/chrome/chrome_common.gypi @@ -137,6 +137,8 @@ 'common/extensions/api/managed_mode_private/managed_mode_handler.h', 'common/extensions/api/media_galleries_private/media_galleries_handler.h', 'common/extensions/api/media_galleries_private/media_galleries_handler.cc', + 'common/extensions/api/notifications/notification_style.cc', + 'common/extensions/api/notifications/notification_style.h', 'common/extensions/api/omnibox/omnibox_handler.cc', 'common/extensions/api/omnibox/omnibox_handler.h', 'common/extensions/api/plugins/plugins_handler.cc', diff --git a/chrome/chrome_renderer.gypi b/chrome/chrome_renderer.gypi index 955bbe49..bbcd4b5 100644 --- a/chrome/chrome_renderer.gypi +++ b/chrome/chrome_renderer.gypi @@ -76,6 +76,8 @@ 'renderer/extensions/file_browser_private_custom_bindings.h', 'renderer/extensions/media_galleries_custom_bindings.cc', 'renderer/extensions/media_galleries_custom_bindings.h', + 'renderer/extensions/notifications_native_handler.cc', + 'renderer/extensions/notifications_native_handler.h', 'renderer/extensions/page_actions_custom_bindings.cc', 'renderer/extensions/page_actions_custom_bindings.h', 'renderer/extensions/page_capture_custom_bindings.cc', diff --git a/chrome/chrome_tests_unit.gypi b/chrome/chrome_tests_unit.gypi index 3af3583..97587ac 100644 --- a/chrome/chrome_tests_unit.gypi +++ b/chrome/chrome_tests_unit.gypi @@ -1911,6 +1911,9 @@ 'renderer/net/renderer_predictor_unittest.cc', 'renderer/plugins/plugin_uma_unittest.cc', 'renderer/prerender/prerender_dispatcher_unittest.cc', + 'renderer/resources/extensions/notifications_custom_bindings.js', + 'renderer/resources/extensions/notifications_custom_bindings.gtestjs', + 'renderer/resources/extensions/notifications_test_util.js', 'renderer/safe_browsing/features_unittest.cc', 'renderer/safe_browsing/murmurhash3_util_unittest.cc', 'renderer/safe_browsing/phishing_term_feature_extractor_unittest.cc', diff --git a/chrome/common/extensions/api/notifications/notification_style.cc b/chrome/common/extensions/api/notifications/notification_style.cc new file mode 100644 index 0000000..5d2d883 --- /dev/null +++ b/chrome/common/extensions/api/notifications/notification_style.cc @@ -0,0 +1,20 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/common/extensions/api/notifications/notification_style.h" + +#include "ui/message_center/message_center_style.h" + +NotificationBitmapSizes GetNotificationBitmapSizes() { + NotificationBitmapSizes sizes; + sizes.image_size = + gfx::Size(message_center::kNotificationPreferredImageWidth, + message_center::kNotificationPreferredImageHeight); + sizes.icon_size = gfx::Size(message_center::kNotificationIconSize, + message_center::kNotificationIconSize); + sizes.button_icon_size = + gfx::Size(message_center::kNotificationButtonIconSize, + message_center::kNotificationButtonIconSize); + return sizes; +} diff --git a/chrome/common/extensions/api/notifications/notification_style.h b/chrome/common/extensions/api/notifications/notification_style.h new file mode 100644 index 0000000..ef4eb19 --- /dev/null +++ b/chrome/common/extensions/api/notifications/notification_style.h @@ -0,0 +1,20 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_COMMON_EXTENSIONS_API_NOTIFICATIONS_NOTIFICATION_STYLE_H_ +#define CHROME_COMMON_EXTENSIONS_API_NOTIFICATIONS_NOTIFICATION_STYLE_H_ + +#include "ui/gfx/size.h" + +// This structure describes the size in DIPs of each type of image rendered +// by the notification center within a notification. +struct NotificationBitmapSizes { + gfx::Size image_size; + gfx::Size icon_size; + gfx::Size button_icon_size; +}; + +NotificationBitmapSizes GetNotificationBitmapSizes(); + +#endif // CHROME_COMMON_EXTENSIONS_API_NOTIFICATIONS_NOTIFICATION_STYLE_H_ diff --git a/chrome/renderer/extensions/chrome_extensions_dispatcher_delegate.cc b/chrome/renderer/extensions/chrome_extensions_dispatcher_delegate.cc index 4105d74..20a5ef8 100644 --- a/chrome/renderer/extensions/chrome_extensions_dispatcher_delegate.cc +++ b/chrome/renderer/extensions/chrome_extensions_dispatcher_delegate.cc @@ -19,6 +19,7 @@ #include "chrome/renderer/extensions/file_browser_handler_custom_bindings.h" #include "chrome/renderer/extensions/file_browser_private_custom_bindings.h" #include "chrome/renderer/extensions/media_galleries_custom_bindings.h" +#include "chrome/renderer/extensions/notifications_native_handler.h" #include "chrome/renderer/extensions/page_actions_custom_bindings.h" #include "chrome/renderer/extensions/page_capture_custom_bindings.h" #include "chrome/renderer/extensions/pepper_request_natives.h" @@ -111,6 +112,10 @@ void ChromeExtensionsDispatcherDelegate::RegisterNativeHandlers( scoped_ptr<NativeHandler>( new extensions::FileBrowserPrivateCustomBindings(context))); module_system->RegisterNativeHandler( + "notifications_private", + scoped_ptr<NativeHandler>( + new extensions::NotificationsNativeHandler(context))); + module_system->RegisterNativeHandler( "mediaGalleries", scoped_ptr<NativeHandler>( new extensions::MediaGalleriesCustomBindings(context))); diff --git a/chrome/renderer/extensions/notifications_native_handler.cc b/chrome/renderer/extensions/notifications_native_handler.cc new file mode 100644 index 0000000..a08284b --- /dev/null +++ b/chrome/renderer/extensions/notifications_native_handler.cc @@ -0,0 +1,48 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/renderer/extensions/notifications_native_handler.h" + +#include <string> + +#include "base/logging.h" +#include "base/values.h" +#include "chrome/common/extensions/api/notifications/notification_style.h" +#include "chrome/renderer/extensions/chrome_v8_context.h" +#include "content/public/renderer/v8_value_converter.h" +#include "ui/base/layout.h" + +namespace extensions { + +NotificationsNativeHandler::NotificationsNativeHandler(ScriptContext* context) + : ObjectBackedNativeHandler(context) { + RouteFunction( + "GetNotificationImageSizes", + base::Bind(&NotificationsNativeHandler::GetNotificationImageSizes, + base::Unretained(this))); +} + +void NotificationsNativeHandler::GetNotificationImageSizes( + const v8::FunctionCallbackInfo<v8::Value>& args) { + NotificationBitmapSizes bitmap_sizes = NotificationBitmapSizes(); + + float scale_factor = + ui::GetScaleForScaleFactor(ui::GetSupportedScaleFactors().back()); + + scoped_ptr<base::DictionaryValue> dict(new base::DictionaryValue); + dict->SetDouble("scaleFactor", scale_factor); + dict->SetInteger("icon.width", bitmap_sizes.icon_size.width()); + dict->SetInteger("icon.height", bitmap_sizes.icon_size.height()); + dict->SetInteger("image.width", bitmap_sizes.image_size.width()); + dict->SetInteger("image.height", bitmap_sizes.image_size.height()); + dict->SetInteger("buttonIcon.width", bitmap_sizes.button_icon_size.width()); + dict->SetInteger("buttonIcon.height", bitmap_sizes.button_icon_size.height()); + + scoped_ptr<content::V8ValueConverter> converter( + content::V8ValueConverter::create()); + args.GetReturnValue().Set( + converter->ToV8Value(dict.get(), context()->v8_context())); +} + +} // namespace extensions diff --git a/chrome/renderer/extensions/notifications_native_handler.h b/chrome/renderer/extensions/notifications_native_handler.h new file mode 100644 index 0000000..f47a808 --- /dev/null +++ b/chrome/renderer/extensions/notifications_native_handler.h @@ -0,0 +1,42 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_RENDERER_EXTENSIONS_NOTIFICATIONS_NATIVE_HANDLER_H_ +#define CHROME_RENDERER_EXTENSIONS_NOTIFICATIONS_NATIVE_HANDLER_H_ + +#include "base/compiler_specific.h" +#include "extensions/renderer/object_backed_native_handler.h" + +namespace base { +class Value; +} + +namespace extensions { + +class NotificationsNativeHandler : public ObjectBackedNativeHandler { + public: + explicit NotificationsNativeHandler(ScriptContext* context); + + private: + // This implements notifications_private.GetImageSizes() which + // informs the renderer of the actual rendered size of each + // component of a notification. It additionally includes + // information about the system's maximum scale factor so that + // larger images specified in DP can be interpreted as scaled + // versions of the DIP size. + // * |args| is used only to get the return value. + // * The return value contains the following keys: + // scaleFactor - a float a la devicePixelRatio + // icon - a dictionary with integer keys "height" and "width" (DIPs) + // image - a dictionary of the same format as |icon| + // buttonIcon - a dictionary of the same format as |icon| + void GetNotificationImageSizes( + const v8::FunctionCallbackInfo<v8::Value>& args); + + DISALLOW_COPY_AND_ASSIGN(NotificationsNativeHandler); +}; + +} // namespace extensions + +#endif // CHROME_RENDERER_EXTENSIONS_NOTIFICATIONS_NATIVE_HANDLER_H_ diff --git a/chrome/renderer/resources/extensions/OWNERS b/chrome/renderer/resources/extensions/OWNERS index b3b7eec..6e5c1ae 100644 --- a/chrome/renderer/resources/extensions/OWNERS +++ b/chrome/renderer/resources/extensions/OWNERS @@ -21,3 +21,6 @@ per-file web_view*.js=lazyboy@chromium.org per-file ad_view*.js=fsamuel@chromium.org per-file ad_view*.js=lazyboy@chromium.org per-file enterprise_platform_keys*=pneubeck@chromium.org +per-file notifications_*.js=dewittj@chromium.org +per-file image_util.js=dewittj@chromium.org +per-file *.gtestjs=* diff --git a/chrome/renderer/resources/extensions/notifications_custom_bindings.gtestjs b/chrome/renderer/resources/extensions/notifications_custom_bindings.gtestjs new file mode 100644 index 0000000..af1e89f --- /dev/null +++ b/chrome/renderer/resources/extensions/notifications_custom_bindings.gtestjs @@ -0,0 +1,104 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * Test fixture for the notifications custom bindings adapter. + * @constructor + * @extends {testing.Test} + */ +function NotificationsCustomBindingsTest() { + testing.Test.call(this); +} + +NotificationsCustomBindingsTest.prototype = { + __proto__: testing.Test.prototype, + + /** @Override */ + extraLibraries: [ + 'notifications_test_util.js', + 'notifications_custom_bindings.js' + ], +}; + +TEST_F('NotificationsCustomBindingsTest', 'TestImageDataSetter', function () { + var c = {}; + var k = "key"; + var callback = imageDataSetter(c, k); + callback('val'); + expectTrue(c[k] === 'val'); +}); + +TEST_F('NotificationsCustomBindingsTest', 'TestGetUrlSpecs', function () { + var imageSizes = { + scaleFactor: 1.0, + icon: { width: 10, height: 10 }, + image: { width: 24, height: 32 }, + buttonIcon: { width: 2, height: 2} + }; + + var notificationDetails = {}; + + var emptySpecs = getUrlSpecs(imageSizes, notificationDetails); + expectTrue(emptySpecs.length === 0); + + notificationDetails.iconUrl = "iconUrl"; + notificationDetails.imageUrl = "imageUrl"; + notificationDetails.buttons = [ + {iconUrl: "buttonOneIconUrl"}, + {iconUrl: "buttonTwoIconUrl"}]; + + var allSpecs = getUrlSpecs(imageSizes, notificationDetails); + expectTrue(allSpecs.length === 4); + + expectFalse(notificationDetails.iconBitmap === "test"); + expectFalse(notificationDetails.imageBitmap === "test"); + expectFalse(notificationDetails.buttons[0].iconBitmap === "test"); + expectFalse(notificationDetails.buttons[1].iconBitmap === "test"); + + for (var i = 0; i < allSpecs.length; i++) { + var expectedKeys = ['path', 'width', 'height', 'callback']; + var spec = allSpecs[i]; + for (var j in expectedKeys) { + expectTrue(spec.hasOwnProperty(expectedKeys[j])); + } + spec.callback(spec.path + "|" + spec.width + "|" + spec.height); + } + + expectTrue(notificationDetails.iconBitmap === "iconUrl|10|10"); + expectTrue(notificationDetails.imageBitmap === "imageUrl|24|32"); + expectTrue( + notificationDetails.buttons[0].iconBitmap === "buttonOneIconUrl|2|2"); + expectTrue( + notificationDetails.buttons[1].iconBitmap === "buttonTwoIconUrl|2|2"); +}); + +TEST_F('NotificationsCustomBindingsTest', 'TestGetUrlSpecsScaled', function () { + var imageSizes = { + scaleFactor: 2.0, + icon: { width: 10, height: 10 }, + image: { width: 24, height: 32 }, + buttonIcon: { width: 2, height: 2} + }; + var notificationDetails = { + iconUrl: "iconUrl", + imageUrl: "imageUrl", + buttons: [ + {iconUrl: "buttonOneIconUrl"}, + {iconUrl: "buttonTwoIconUrl"} + ] + }; + + var allSpecs = getUrlSpecs(imageSizes, notificationDetails); + for (var i = 0; i < allSpecs.length; i++) { + var spec = allSpecs[i]; + spec.callback(spec.path + "|" + spec.width + "|" + spec.height); + } + + expectEquals(notificationDetails.iconBitmap, "iconUrl|20|20"); + expectEquals(notificationDetails.imageBitmap, "imageUrl|48|64"); + expectEquals(notificationDetails.buttons[0].iconBitmap, + "buttonOneIconUrl|4|4"); + expectEquals(notificationDetails.buttons[1].iconBitmap, + "buttonTwoIconUrl|4|4"); +}); diff --git a/chrome/renderer/resources/extensions/notifications_custom_bindings.js b/chrome/renderer/resources/extensions/notifications_custom_bindings.js index fb8264d..013628a 100644 --- a/chrome/renderer/resources/extensions/notifications_custom_bindings.js +++ b/chrome/renderer/resources/extensions/notifications_custom_bindings.js @@ -3,66 +3,75 @@ // found in the LICENSE file. // Custom bindings for the notifications API. +// var binding = require('binding').Binding.create('notifications'); var sendRequest = require('sendRequest').sendRequest; var imageUtil = require('imageUtil'); var lastError = require('lastError'); +var notificationsPrivate = requireNative('notifications_private'); -function image_data_setter(context, key) { +function imageDataSetter(context, key) { var f = function(val) { this[key] = val; }; return $Function.bind(f, context); } -function replaceNotificationOptionURLs(notification_details, callback) { - // A URL Spec is an object with the following keys: - // path: The resource to be downloaded. - // width: (optional) The maximum width of the image to be downloaded. - // height: (optional) The maximum height of the image to be downloaded. - // callback: A function to be called when the URL is complete. It - // should accept an ImageData object and set the appropriate - // field in the output of create. - - // TODO(dewittj): Try to remove hard-coding of image sizes. +// A URL Spec is an object with the following keys: +// path: The resource to be downloaded. +// width: (optional) The maximum width of the image to be downloaded in device +// pixels. +// height: (optional) The maximum height of the image to be downloaded in +// device pixels. +// callback: A function to be called when the URL is complete. It +// should accept an ImageData object and set the appropriate +// field in |notificationDetails|. +function getUrlSpecs(imageSizes, notificationDetails) { + var urlSpecs = []; + // |iconUrl| might be optional for notification updates. - var url_specs = []; - if (notification_details.iconUrl) { - $Array.push(url_specs, { - path: notification_details.iconUrl, - width: 80, - height: 80, - callback: image_data_setter(notification_details, 'iconBitmap') + if (notificationDetails.iconUrl) { + $Array.push(urlSpecs, { + path: notificationDetails.iconUrl, + width: imageSizes.icon.width * imageSizes.scaleFactor, + height: imageSizes.icon.height * imageSizes.scaleFactor, + callback: imageDataSetter(notificationDetails, 'iconBitmap') }); } // |imageUrl| is optional. - if (notification_details.imageUrl) { - $Array.push(url_specs, { - path: notification_details.imageUrl, - width: 360, - height: 240, - callback: image_data_setter(notification_details, 'imageBitmap') + if (notificationDetails.imageUrl) { + $Array.push(urlSpecs, { + path: notificationDetails.imageUrl, + width: imageSizes.image.width * imageSizes.scaleFactor, + height: imageSizes.image.height * imageSizes.scaleFactor, + callback: imageDataSetter(notificationDetails, 'imageBitmap') }); } // Each button has an optional icon. - var button_list = notification_details.buttons; - if (button_list && typeof button_list.length === 'number') { - var num_buttons = button_list.length; - for (var i = 0; i < num_buttons; i++) { - if (button_list[i].iconUrl) { - $Array.push(url_specs, { - path: button_list[i].iconUrl, - width: 16, - height: 16, - callback: image_data_setter(button_list[i], 'iconBitmap') + var buttonList = notificationDetails.buttons; + if (buttonList && typeof buttonList.length === 'number') { + var numButtons = buttonList.length; + for (var i = 0; i < numButtons; i++) { + if (buttonList[i].iconUrl) { + $Array.push(urlSpecs, { + path: buttonList[i].iconUrl, + width: imageSizes.buttonIcon.width * imageSizes.scaleFactor, + height: imageSizes.buttonIcon.height * imageSizes.scaleFactor, + callback: imageDataSetter(buttonList[i], 'iconBitmap') }); } } } + return urlSpecs; +} + +function replaceNotificationOptionURLs(notification_details, callback) { + var imageSizes = notificationsPrivate.GetNotificationImageSizes(); + var url_specs = getUrlSpecs(imageSizes, notification_details); if (!url_specs.length) { callback(true); return; diff --git a/chrome/renderer/resources/extensions/notifications_test_util.js b/chrome/renderer/resources/extensions/notifications_test_util.js new file mode 100644 index 0000000..85a30ee --- /dev/null +++ b/chrome/renderer/resources/extensions/notifications_test_util.js @@ -0,0 +1,53 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// NotificationsTestUtil contains stubs for the global classes and +// variables used by notifications_custom_bindings.js that are not +// available with gtestjs tests. +var require = function(library) { + return { + binding: { + 'Binding': { + 'create': function () { + return { + registerCustomHook: function () {}, + generate: function () {} + }; + } + } + }, + sendRequest: { + sendRequest: function () {} + }, + }[library]; +}; + +var requireNative = function(library) { + return { + notifications_private: { + GetNotificationImageSizes: function () { + return { + scaleFactor: 0, + icon: { width: 0, height: 0 }, + image: { width: 0, height: 0 }, + buttonIcon: { width: 0, height: 0} + }; + } + } + }[library]; +} + +var exports = {}; + +var $Array = { + push: function (ary, val) { + ary.push(val); + } +}; + +var $Function = { + bind: function (fn, context) { + return fn.bind(context); + } +}; diff --git a/chrome/test/data/extensions/api_test/notifications/api/basic_usage/background.js b/chrome/test/data/extensions/api_test/notifications/api/basic_usage/background.js new file mode 100644 index 0000000..50fb49d --- /dev/null +++ b/chrome/test/data/extensions/api_test/notifications/api/basic_usage/background.js @@ -0,0 +1,305 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +const notifications = chrome.notifications; + +const red_dot = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA" + + "AAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO" + + "9TXL0Y4OHwAAAABJRU5ErkJggg=="; + +var basicNotificationOptions = { + type: "basic", + title: "Basic title", + message: "Basic message", + iconUrl: red_dot +}; + +function create(id, options) { + return new Promise(function (resolve, reject) { + notifications.create(id, options, function (id) { + if (chrome.runtime.lastError) { + reject(new Error("Unable to create notification")); + return; + } + resolve(id); + return; + }); + }); +}; + +function update(id, options) { + return new Promise(function (resolve, reject) { + notifications.update(id, options, function (ok) { + if (chrome.runtime.lastError || !ok) { + reject(new Error("Unable to update notification")); + return; + } + resolve(ok); + return; + }); + }); +} + +function clear(id) { + return new Promise(function (resolve, reject) { + notifications.clear(id, function (ok) { + if (chrome.runtime.lastError || !ok) { + reject(new Error("Unable to clear notification")); + return; + } + resolve(ok); + return; + }); + }); +} + +function getAll() { + return new Promise(function (resolve, reject) { + notifications.getAll(function (ids) { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError.message)); + return; + } + + if (ids === undefined) { + resolve([]); + return + } + + var id_list = Object.keys(ids); + resolve(id_list); + }); + }); +} + +function clearAll() { + return getAll().then(function (ids) { + var idPromises = ids.map(function (id) { return clear(id); }); + return Promise.all(idPromises); + }); +} + +function succeedTest(testName) { + return function () { + return clearAll().then( + function () { chrome.test.succeed(testName); }, + function (error) { + console.log("Unknown error in clearAll: " + + JSON.stringify(arguments)); + }); + }; +} + +function failTest(testName) { + return function () { + return clearAll().then( + function () { chrome.test.fail(testName); }, + function (error) { + console.log("Unknown error in clearAll: " + + JSON.stringify(error.message)); + }); + }; +} + +function testIdUsage() { + var testName = "testIdUsage"; + console.log("Starting testIdUsage."); + var succeed = succeedTest(testName); + var fail = failTest(testName); + + var createNotification = function (idString) { + var options = { + type: "basic", + iconUrl: red_dot, + title: "Attention!", + message: "Check out Cirque du Soleil" + }; + + return create(idString, options); + }; + + var updateNotification = function (idString) { + var options = { title: "!", message: "!" }; + return update(idString, options); + }; + + // Should successfully create the notification + createNotification("foo") + // And update it. + .then(updateNotification) + .catch(fail) + // Next try to update a non-existent notification. + .then(function () { return updateNotification("foo2"); }) + // And fail if it returns true. + .then(fail) + // Next try to clear a non-existent notification. + .catch(function () { return clear("foo2"); }) + .then(fail) + // And finally clear the original notification. + .catch(function () { return clear("foo"); }) + .catch(fail) + .then(succeed); +}; + +function testBaseFormat() { + var testName = "testBaseFormat"; + console.log("Starting " + testName); + var succeed = succeedTest(testName); + var fail = failTest(testName); + + var createNotificationWithout = function(toDelete) { + var options = { + type: "basic", + iconUrl: red_dot, + title: "Attention!", + message: "Check out Cirque du Soleil", + contextMessage: "Foobar.", + priority: 1, + eventTime: 123457896.12389, + expandedMessage: "This is a longer expanded message.", + isClickable: true + }; + + for (var i = 0; i < toDelete.length; i++) { + delete options[toDelete[i]]; + } + + return create("", options); + }; + + // Construct some exclusion lists. The |createNotificationWithout| function + // starts with a complex notification and then deletes items in this list. + var basicNotification= [ + "buttons", + "items", + "progress", + "imageUrl" + ]; + var bareNotification = basicNotification.concat([ + "priority", + "eventTime", + "expandedMessage", + ]); + var basicNoType = basicNotification.concat(["type"]); + var basicNoIcon = basicNotification.concat(["iconUrl"]); + var basicNoTitle = basicNotification.concat(["title"]); + var basicNoMessage = basicNotification.concat(["message"]); + + // Try creating a basic notification with just some of the fields. + createNotificationWithout(basicNotification) + // Try creating a basic notification with all possible fields. + .then(function () { return createNotificationWithout([]); }) + // Try creating a basic notification with the minimum in fields. + .then(function () { return createNotificationWithout(bareNotification); }) + // After this line we are checking to make sure that there is an error + // when notifications are created without the proper fields. + .catch(fail) + // Error if no type. + .then(function () { return createNotificationWithout(basicNoType) }) + // Error if no icon. + .catch(function () { return createNotificationWithout(basicNoIcon) }) + // Error if no title. + .catch(function () { return createNotificationWithout(basicNoTitle) }) + // Error if no message. + .catch(function () { return createNotificationWithout(basicNoMessage) }) + .then(fail, succeed); +}; + +function testListItem() { + var testName = "testListItem"; + console.log("Starting " + testName); + var succeed = succeedTest(testName); + var fail = failTest(testName); + + var item = { title: "Item title.", message: "Item message." }; + var options = { + type: "list", + iconUrl: red_dot, + title: "Attention!", + message: "Check out Cirque du Soleil", + contextMessage: "Foobar.", + priority: 1, + eventTime: 123457896.12389, + items: [item, item, item, item, item], + isClickable: true + }; + create("id", options).then(succeed, fail); +}; + +function arrayEquals(a, b) { + if (a === b) return true; + if (a == null || b == null) return false; + if (a.length !== b.length) return false; + + for (var i = 0; i < a.length; i++) { + if (a[i] !== b[i]) return false; + } + return true; +}; + +function testGetAll() { + var testName = "testGetAll"; + console.log("Starting " + testName); + var succeed = succeedTest(testName); + var fail = failTest(testName); + var in_ids = ["a", "b", "c", "d"]; + + // First do a get all, make sure the list is empty. + getAll() + .then(function (ids) { + chrome.test.assertEq(0, ids.length); + }) + // Then create a bunch of notifications. + .then(function () { + var newNotifications = in_ids.map(function (id) { + return create(id, basicNotificationOptions); + }); + return Promise.all(newNotifications); + }) + // Try getAll again. + .then(function () { return getAll(); }) + // Check that the right set of notifications is in the center. + .then(function (ids) { + chrome.test.assertEq(4, ids.length); + chrome.test.assertTrue(arrayEquals(ids, in_ids)); + succeed(); + }, fail); +} + +function testProgress() { + var testName = "testProgress"; + console.log("Starting " + testName); + var succeed = succeedTest(testName); + var fail = failTest(testName); + var progressOptions = { + type: "progress", + title: "Basic title", + message: "Basic message", + iconUrl: red_dot, + progress: 30 + }; + + // First, create a basic progress notification. + create("progress", progressOptions) + // and update it to have a different progress level. + .then(function () { return update("progress", { progress: 60 }); }) + // If either of the above failed, the test fails. + .catch(fail) + // Now the following parts should all cause an error: + // First update the progress to a low value, out-of-range + .then(function () { return update("progress", { progress: -10 }); }) + // First update the progress to a high value, out-of-range + .then(fail, function () { return update("progress", { progress: 101 }); }) + .then(function () { return clear("progress"); }) + // Finally try to create a notification that has a progress value but not + // progress type. + .then(fail, function () { + progressOptions.type = "basic"; + return create("progress", progressOptions); + }).then(fail, succeed); +} + +chrome.test.runTests([ + testIdUsage, testBaseFormat, testListItem, testGetAll, testProgress +]); diff --git a/chrome/test/data/extensions/api_test/notifications/api/basic_usage/manifest.json b/chrome/test/data/extensions/api_test/notifications/api/basic_usage/manifest.json new file mode 100644 index 0000000..ffed6e8 --- /dev/null +++ b/chrome/test/data/extensions/api_test/notifications/api/basic_usage/manifest.json @@ -0,0 +1,13 @@ +{ + "name": "chrome.notifications", + "version": "0.1", + "description": "chrome.notifications API events", + "app": { + "background": { + "scripts": ["background.js"] + } + }, + "permissions": [ + "notifications" + ] +} diff --git a/chrome/test/data/extensions/api_test/notifications/api/by_user/background.js b/chrome/test/data/extensions/api_test/notifications/api/by_user/background.js index bb59d3d..3d765de 100644 --- a/chrome/test/data/extensions/api_test/notifications/api/by_user/background.js +++ b/chrome/test/data/extensions/api_test/notifications/api/by_user/background.js @@ -25,10 +25,25 @@ var results = { function createCallback(id) { } +function notifyPass() { chrome.test.notifyPass(); } + var onClosedHooks = { + FOO: notifyPass, + BAR: notifyPass, BIFF: function() { - notifications.create("BLAT", notificationData, createCallback); - notifications.create("BLOT", notificationData, createCallback); + notifications.create("BLAT", notificationData, function () { + if (chrome.runtime.lastError) { + chrome.test.notifyFail(lastError.message); + return; + } + notifications.create("BLOT", notificationData, function () { + if (chrome.runtime.lastError) { + chrome.test.notifyFail(lastError.message); + return; + } + chrome.test.notifyPass("Created the new notifications."); + }); + }); }, }; @@ -38,19 +53,25 @@ function onClosedListener(id, by_user) { " closed with bad by_user param ( "+ by_user +" )"); return; } - chrome.test.notifyPass(); delete results[id]; if (typeof onClosedHooks[id] === "function") onClosedHooks[id](); - if (Object.keys(results).length === 0) + if (Object.keys(results).length === 0) { + chrome.test.notifyPass("Done!"); theOnlyTestDone(); + } } notifications.onClosed.addListener(onClosedListener); function theOnlyTest() { + // This test coordinates with the browser test. First, 4 notifications are + // created. Then 2 are manually cancelled in C++. Then clearAll is called + // with false |by_user|. Then once the BIFF notification is cleared, we + // create two more notifications in JS, and C++ calls the clearAll with true + // |by_user|. theOnlyTestDone = chrome.test.callbackAdded(); notifications.create("FOO", notificationData, createCallback); diff --git a/chrome/test/data/extensions/api_test/notifications/api/partial_update/background.js b/chrome/test/data/extensions/api_test/notifications/api/partial_update/background.js new file mode 100644 index 0000000..756fc26 --- /dev/null +++ b/chrome/test/data/extensions/api_test/notifications/api/partial_update/background.js @@ -0,0 +1,141 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +const notifications = chrome.notifications; + +function arrayEquals(a, b) { + if (a === b) return true; + if (a == null || b == null) return false; + if (a.length !== b.length) return false; + + for (var i = 0; i < a.length; i++) { + if (a[i] !== b[i]) return false; + } + return true; +}; + +function create(id, options) { + return new Promise(function (resolve, reject) { + notifications.create(id, options, function (id) { + if (chrome.runtime.lastError) { + reject(new Error("Unable to create notification")); + return; + } + console.log("Created with id: " + id); + resolve(id); + return; + }); + }); +}; + +function update(id, options) { + return new Promise(function (resolve, reject) { + notifications.update(id, options, function (ok) { + if (chrome.runtime.lastError || !ok) { + reject(new Error("Unable to update notification")); + return; + } + console.log("Updated id: ", id); + resolve(ok); + return; + }); + }); +} + +function clear(id) { + return new Promise(function (resolve, reject) { + notifications.clear(id, function (ok) { + if (chrome.runtime.lastError || !ok) { + reject(new Error("Unable to clear notification")); + return; + } + resolve(ok); + return; + }); + }); +} + +function getAll() { + return new Promise(function (resolve, reject) { + notifications.getAll(function (ids) { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError.message)); + return; + } + + if (ids === undefined) { + resolve([]); + return + } + + var id_list = Object.keys(ids); + resolve(id_list); + }); + }); +} + +function clearAll() { + return getAll().then(function (ids) { + var idPromises = ids.map(function (id) { return clear(id); }); + return Promise.all(idPromises); + }); +} + +function succeedTest(testName) { + return function () { + return clearAll().then( + function () { chrome.test.succeed(testName); }, + function (error) { + console.log("Unknown error in clearAll: " + + JSON.stringify(arguments)); + }); + }; +} + +function failTest(testName) { + return function () { + return clearAll().then( + function () { chrome.test.fail(testName); }, + function (error) { + console.log("Unknown error in clearAll: " + + JSON.stringify(error.message)); + }); + }; +} + +function testPartialUpdate() { + var testName = "testPartialUpdate"; + console.log("Starting " + testName); + var succeed = succeedTest(testName); + var fail = failTest(testName); + + const red_dot = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA" + + "AAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO" + + "9TXL0Y4OHwAAAABJRU5ErkJggg=="; + + var basicNotificationOptions = { + type: "basic", + title: "Basic title", + message: "Basic message", + iconUrl: red_dot, + buttons: [{title: "Button"}] + }; + + // Create a notification. + create("testId", basicNotificationOptions) + // Then update a few items + .then(function () { + return update("testId", { + title: "Changed!", + message: "Too late! The show ended yesterday" + }); + }) + // Then update a few more items + .then(function () { return update("testId", {priority:2, buttons: []}); }) + // The test will continue in C++, checking that all the updates "took" + .then(chrome.test.succeed, chrome.test.fail); +}; + + +chrome.test.runTests([testPartialUpdate]); diff --git a/chrome/test/data/extensions/api_test/notifications/api/partial_update/manifest.json b/chrome/test/data/extensions/api_test/notifications/api/partial_update/manifest.json new file mode 100644 index 0000000..ffed6e8 --- /dev/null +++ b/chrome/test/data/extensions/api_test/notifications/api/partial_update/manifest.json @@ -0,0 +1,13 @@ +{ + "name": "chrome.notifications", + "version": "0.1", + "description": "chrome.notifications API events", + "app": { + "background": { + "scripts": ["background.js"] + } + }, + "permissions": [ + "notifications" + ] +} diff --git a/chrome/test/data/extensions/api_test/notifications/api/user_gesture/background.js b/chrome/test/data/extensions/api_test/notifications/api/user_gesture/background.js index f9194e2..fd1bd61 100644 --- a/chrome/test/data/extensions/api_test/notifications/api/user_gesture/background.js +++ b/chrome/test/data/extensions/api_test/notifications/api/user_gesture/background.js @@ -2,6 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +const red_dot = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA" + + "AAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO" + + "9TXL0Y4OHwAAAABJRU5ErkJggg=="; + function userActionTest() { chrome.test.sendMessage(""); } @@ -11,7 +15,7 @@ chrome.notifications.onButtonClicked.addListener(userActionTest); chrome.notifications.onClosed.addListener(userActionTest); chrome.notifications.create("test", { - iconUrl: "", + iconUrl: red_dot, title: "hi", message: "there", type: "basic", diff --git a/chrome/unit_tests.isolate b/chrome/unit_tests.isolate index 15a24b7..36e5af0b 100644 --- a/chrome/unit_tests.isolate +++ b/chrome/unit_tests.isolate @@ -70,6 +70,7 @@ '../third_party/pywebsocket/', '../third_party/tlslite/', '<(PRODUCT_DIR)/pyproto/', + '<(PRODUCT_DIR)/test_data/chrome/renderer/resources/extensions/', ], }, }], |