blob: d26317e05e32900c39bfd2a4140efce24472d743 [file] [log] [blame]
// Copyright (c) 2012 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/browser/extensions/api/notifications/notifications_api.h"
#include <stddef.h>
#include <utility>
#include "base/callback.h"
#include "base/guid.h"
#include "base/macros.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_macros.h"
#include "base/rand_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/notifications/notification.h"
#include "chrome/browser/notifications/notification_conversion_helper.h"
#include "chrome/browser/notifications/notification_ui_manager.h"
#include "chrome/browser/notifications/notifier_state_tracker.h"
#include "chrome/browser/notifications/notifier_state_tracker_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/extensions/api/notifications/notification_style.h"
#include "components/keyed_service/content/browser_context_keyed_service_shutdown_notifier_factory.h"
#include "components/keyed_service/core/keyed_service_shutdown_notifier.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/web_contents.h"
#include "extensions/browser/event_router.h"
#include "extensions/browser/extension_system_provider.h"
#include "extensions/browser/extensions_browser_client.h"
#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_operations.h"
#include "ui/gfx/image/image_skia_rep.h"
#include "ui/message_center/message_center_style.h"
#include "ui/message_center/notifier_settings.h"
#include "url/gurl.h"
namespace extensions {
namespace notifications = api::notifications;
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";
#if !defined(OS_CHROMEOS)
const char kLowPriorityDeprecatedOnPlatform[] =
"Low-priority notifications are deprecated on this platform.";
#endif
// Given an extension id and another id, returns an id that is unique
// relative to other extensions.
std::string CreateScopedIdentifier(const std::string& extension_id,
const std::string& id) {
return extension_id + "-" + id;
}
// Removes the unique internal identifier to send the ID as the
// extension expects it.
std::string StripScopeFromIdentifier(const std::string& extension_id,
const std::string& id) {
size_t index_of_separator = extension_id.length() + 1;
DCHECK_LT(index_of_separator, id.length());
return id.substr(index_of_separator);
}
const gfx::ImageSkia CreateSolidColorImage(int width,
int height,
SkColor color) {
SkBitmap bitmap;
bitmap.allocN32Pixels(width, height);
bitmap.eraseColor(color);
return gfx::ImageSkia::CreateFrom1xBitmap(bitmap);
}
// Take the alpha channel of small_image, mask it with the foreground,
// then add the masked foreground on top of the background
const gfx::Image GetMaskedSmallImage(const gfx::ImageSkia& small_image) {
int width = small_image.width();
int height = small_image.height();
// Background color grey
const gfx::ImageSkia background = CreateSolidColorImage(
width, height, message_center::kSmallImageMaskBackgroundColor);
// Foreground color white
const gfx::ImageSkia foreground = CreateSolidColorImage(
width, height, message_center::kSmallImageMaskForegroundColor);
const gfx::ImageSkia masked_small_image =
gfx::ImageSkiaOperations::CreateMaskedImage(foreground, small_image);
return gfx::Image(gfx::ImageSkiaOperations::CreateSuperimposedImage(
background, masked_small_image));
}
class ShutdownNotifierFactory
: public BrowserContextKeyedServiceShutdownNotifierFactory {
public:
static ShutdownNotifierFactory* GetInstance() {
return base::Singleton<ShutdownNotifierFactory>::get();
}
private:
friend struct base::DefaultSingletonTraits<ShutdownNotifierFactory>;
ShutdownNotifierFactory()
: BrowserContextKeyedServiceShutdownNotifierFactory(
"NotificationsApiDelegate") {
DependsOn(ExtensionsBrowserClient::Get()->GetExtensionSystemFactory());
}
~ShutdownNotifierFactory() override {}
DISALLOW_COPY_AND_ASSIGN(ShutdownNotifierFactory);
};
class NotificationsApiDelegate : public NotificationDelegate {
public:
NotificationsApiDelegate(ChromeAsyncExtensionFunction* api_function,
Profile* profile,
const std::string& extension_id,
const std::string& id)
: api_function_(api_function),
event_router_(EventRouter::Get(profile)),
extension_id_(extension_id),
id_(id),
scoped_id_(CreateScopedIdentifier(extension_id, id)) {
DCHECK(api_function_);
shutdown_notifier_subscription_ =
ShutdownNotifierFactory::GetInstance()->Get(profile)->Subscribe(
base::Bind(&NotificationsApiDelegate::Shutdown,
base::Unretained(this)));
}
void Close(bool by_user) override {
EventRouter::UserGestureState gesture =
by_user ? EventRouter::USER_GESTURE_ENABLED
: EventRouter::USER_GESTURE_NOT_ENABLED;
std::unique_ptr<base::ListValue> args(CreateBaseEventArgs());
args->Append(new base::FundamentalValue(by_user));
SendEvent(events::NOTIFICATIONS_ON_CLOSED,
notifications::OnClosed::kEventName, gesture, std::move(args));
}
void Click() override {
std::unique_ptr<base::ListValue> args(CreateBaseEventArgs());
SendEvent(events::NOTIFICATIONS_ON_CLICKED,
notifications::OnClicked::kEventName,
EventRouter::USER_GESTURE_ENABLED, std::move(args));
}
bool HasClickedListener() override {
if (!event_router_)
return false;
return event_router_->HasEventListener(
notifications::OnClicked::kEventName);
}
void ButtonClick(int index) override {
std::unique_ptr<base::ListValue> args(CreateBaseEventArgs());
args->Append(new base::FundamentalValue(index));
SendEvent(events::NOTIFICATIONS_ON_BUTTON_CLICKED,
notifications::OnButtonClicked::kEventName,
EventRouter::USER_GESTURE_ENABLED, std::move(args));
}
std::string id() const override { return scoped_id_; }
private:
~NotificationsApiDelegate() override {}
void SendEvent(events::HistogramValue histogram_value,
const std::string& name,
EventRouter::UserGestureState user_gesture,
std::unique_ptr<base::ListValue> args) {
if (!event_router_)
return;
std::unique_ptr<Event> event(
new Event(histogram_value, name, std::move(args)));
event->user_gesture = user_gesture;
event_router_->DispatchEventToExtension(extension_id_, std::move(event));
}
void Shutdown() {
event_router_ = nullptr;
shutdown_notifier_subscription_.reset();
}
std::unique_ptr<base::ListValue> CreateBaseEventArgs() {
std::unique_ptr<base::ListValue> args(new base::ListValue());
args->Append(new base::StringValue(id_));
return args;
}
scoped_refptr<ChromeAsyncExtensionFunction> api_function_;
// Since this class is refcounted it may outlive the profile. We listen for
// profile-keyed service shutdown events and reset to nullptr at that time,
// so make sure to check for a valid pointer before use.
EventRouter* event_router_;
const std::string extension_id_;
const std::string id_;
const std::string scoped_id_;
std::unique_ptr<KeyedServiceShutdownNotifier::Subscription>
shutdown_notifier_subscription_;
DISALLOW_COPY_AND_ASSIGN(NotificationsApiDelegate);
};
} // namespace
bool NotificationsApiFunction::IsNotificationsApiAvailable() {
// We need to check this explicitly rather than letting
// _permission_features.json enforce it, because we're sharing the
// chrome.notifications permissions namespace with WebKit notifications.
return extension()->is_platform_app() || extension()->is_extension();
}
NotificationsApiFunction::NotificationsApiFunction() {
}
NotificationsApiFunction::~NotificationsApiFunction() {
}
bool NotificationsApiFunction::CreateNotification(
const std::string& id,
api::notifications::NotificationOptions* options) {
// First, make sure the required fields exist: type, title, message, icon.
// These fields are defined as optional in IDL such that they can be used as
// optional for notification updates. But for notification creations, they
// should be present.
if (options->type == api::notifications::TEMPLATE_TYPE_NONE ||
!options->icon_url || !options->title || !options->message) {
SetError(kMissingRequiredPropertiesForCreateNotification);
return false;
}
#if !defined(OS_CHROMEOS)
if (options->priority &&
*options->priority < message_center::DEFAULT_PRIORITY) {
SetError(kLowPriorityDeprecatedOnPlatform);
return false;
}
#endif
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);
UMA_HISTOGRAM_ENUMERATION("Notifications.ExtensionNotificationType", type,
message_center::NOTIFICATION_TYPE_LAST);
const base::string16 title(base::UTF8ToUTF16(*options->title));
const base::string16 message(base::UTF8ToUTF16(*options->message));
gfx::Image icon;
if (!options->icon_bitmap.get() ||
!NotificationConversionHelper::NotificationBitmapToGfxImage(
image_scale, bitmap_sizes.icon_size, *options->icon_bitmap, &icon)) {
SetError(kUnableToDecodeIconError);
return false;
}
// Then, handle any optional data that's been provided.
message_center::RichNotificationData optional_fields;
if (options->app_icon_mask_url.get()) {
gfx::Image small_icon_mask;
if (!NotificationConversionHelper::NotificationBitmapToGfxImage(
image_scale, bitmap_sizes.app_icon_mask_size,
*options->app_icon_mask_bitmap, &small_icon_mask)) {
SetError(kUnableToDecodeIconError);
return false;
}
optional_fields.small_image =
GetMaskedSmallImage(small_icon_mask.AsImageSkia());
}
if (options->priority.get())
optional_fields.priority = *options->priority;
if (options->event_time.get())
optional_fields.timestamp = base::Time::FromJsTime(*options->event_time);
if (options->buttons.get()) {
// Currently we allow up to 2 buttons.
size_t number_of_buttons = options->buttons->size();
// Use distinct buckets for 1-16 notification action buttons, and an
// overflow bucket for 17 or more action buttons. Does not impact how many
// action buttons are shown.
UMA_HISTOGRAM_ENUMERATION("Notifications.ExtensionNotificationActionCount",
number_of_buttons, 17);
number_of_buttons = number_of_buttons > 2 ? 2 : number_of_buttons;
for (size_t i = 0; i < number_of_buttons; i++) {
message_center::ButtonInfo info(
base::UTF8ToUTF16((*options->buttons)[i].title));
extensions::api::notifications::NotificationBitmap* icon_bitmap_ptr =
(*options->buttons)[i].icon_bitmap.get();
if (icon_bitmap_ptr) {
NotificationConversionHelper::NotificationBitmapToGfxImage(
image_scale, bitmap_sizes.button_icon_size, *icon_bitmap_ptr,
&info.icon);
}
optional_fields.buttons.push_back(info);
}
}
if (options->context_message) {
optional_fields.context_message =
base::UTF8ToUTF16(*options->context_message);
}
bool has_image = options->image_bitmap.get() &&
NotificationConversionHelper::NotificationBitmapToGfxImage(
image_scale, bitmap_sizes.image_size,
*options->image_bitmap, &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)) {
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)) {
SetError(kExtraListItemsProvided);
return false;
}
if (options->progress.get() != NULL) {
// We should have progress if and only if the type is a progress type.
if (type != message_center::NOTIFICATION_TYPE_PROGRESS) {
SetError(kUnexpectedProgressValueForNonProgressType);
return false;
}
optional_fields.progress = *options->progress;
// Progress value should range from 0 to 100.
if (optional_fields.progress < 0 || optional_fields.progress > 100) {
SetError(kInvalidProgressValue);
return false;
}
}
if (has_list_items) {
using api::notifications::NotificationItem;
for (const NotificationItem& api_item : *options->items) {
optional_fields.items.push_back(message_center::NotificationItem(
base::UTF8ToUTF16(api_item.title),
base::UTF8ToUTF16(api_item.message)));
}
}
if (options->is_clickable.get())
optional_fields.clickable = *options->is_clickable;
NotificationsApiDelegate* api_delegate(new NotificationsApiDelegate(
this, GetProfile(), extension_->id(), id)); // ownership is passed to
// Notification
Notification notification(
type, title, message, icon,
message_center::NotifierId(message_center::NotifierId::APPLICATION,
extension_->id()),
base::UTF8ToUTF16(extension_->name()), extension_->url(),
api_delegate->id(), optional_fields, api_delegate);
// Apply the "requireInteraction" flag. The value defaults to false.
notification.set_never_timeout(options->require_interaction &&
*options->require_interaction);
g_browser_process->notification_ui_manager()->Add(notification, GetProfile());
return true;
}
bool NotificationsApiFunction::UpdateNotification(
const std::string& id,
api::notifications::NotificationOptions* options,
Notification* notification) {
#if !defined(OS_CHROMEOS)
if (options->priority &&
*options->priority < message_center::DEFAULT_PRIORITY) {
SetError(kLowPriorityDeprecatedOnPlatform);
return false;
}
#endif
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));
if (options->title)
notification->set_title(base::UTF8ToUTF16(*options->title));
if (options->message)
notification->set_message(base::UTF8ToUTF16(*options->message));
if (options->icon_bitmap.get()) {
gfx::Image icon;
if (!NotificationConversionHelper::NotificationBitmapToGfxImage(
image_scale, bitmap_sizes.icon_size, *options->icon_bitmap,
&icon)) {
SetError(kUnableToDecodeIconError);
return false;
}
notification->set_icon(icon);
}
if (options->app_icon_mask_bitmap.get()) {
gfx::Image app_icon_mask;
if (!NotificationConversionHelper::NotificationBitmapToGfxImage(
image_scale, bitmap_sizes.app_icon_mask_size,
*options->app_icon_mask_bitmap, &app_icon_mask)) {
SetError(kUnableToDecodeIconError);
return false;
}
notification->set_small_image(
GetMaskedSmallImage(app_icon_mask.AsImageSkia()));
}
if (options->priority)
notification->set_priority(*options->priority);
if (options->event_time)
notification->set_timestamp(base::Time::FromJsTime(*options->event_time));
if (options->buttons) {
// Currently we allow up to 2 buttons.
size_t number_of_buttons = options->buttons->size();
number_of_buttons = number_of_buttons > 2 ? 2 : number_of_buttons;
std::vector<message_center::ButtonInfo> buttons;
for (size_t i = 0; i < number_of_buttons; i++) {
message_center::ButtonInfo button(
base::UTF8ToUTF16((*options->buttons)[i].title));
extensions::api::notifications::NotificationBitmap* icon_bitmap_ptr =
(*options->buttons)[i].icon_bitmap.get();
if (icon_bitmap_ptr) {
NotificationConversionHelper::NotificationBitmapToGfxImage(
image_scale, bitmap_sizes.button_icon_size, *icon_bitmap_ptr,
&button.icon);
}
buttons.push_back(button);
}
notification->set_buttons(buttons);
}
if (options->context_message) {
notification->set_context_message(
base::UTF8ToUTF16(*options->context_message));
}
gfx::Image image;
bool has_image =
options->image_bitmap.get() &&
NotificationConversionHelper::NotificationBitmapToGfxImage(
image_scale, bitmap_sizes.image_size, *options->image_bitmap, &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) {
SetError(kExtraImageProvided);
return false;
}
notification->set_image(image);
}
if (options->progress) {
// We should have progress if and only if the type is a progress type.
if (notification->type() != message_center::NOTIFICATION_TYPE_PROGRESS) {
SetError(kUnexpectedProgressValueForNonProgressType);
return false;
}
int progress = *options->progress;
// Progress value should range from 0 to 100.
if (progress < 0 || progress > 100) {
SetError(kInvalidProgressValue);
return false;
}
notification->set_progress(progress);
}
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) {
SetError(kExtraListItemsProvided);
return false;
}
std::vector<message_center::NotificationItem> items;
using api::notifications::NotificationItem;
for (const NotificationItem& api_item : *options->items) {
items.push_back(message_center::NotificationItem(
base::UTF8ToUTF16(api_item.title),
base::UTF8ToUTF16(api_item.message)));
}
notification->set_items(items);
}
// Then override if it's already set.
if (options->is_clickable.get())
notification->set_clickable(*options->is_clickable);
g_browser_process->notification_ui_manager()->Update(*notification,
GetProfile());
return true;
}
bool NotificationsApiFunction::AreExtensionNotificationsAllowed() const {
NotifierStateTracker* notifier_state_tracker =
NotifierStateTrackerFactory::GetForProfile(GetProfile());
return notifier_state_tracker->IsNotifierEnabled(
message_center::NotifierId(message_center::NotifierId::APPLICATION,
extension_->id()));
}
bool NotificationsApiFunction::IsNotificationsApiEnabled() const {
return CanRunWhileDisabled() || AreExtensionNotificationsAllowed();
}
bool NotificationsApiFunction::CanRunWhileDisabled() const {
return false;
}
bool NotificationsApiFunction::RunAsync() {
if (IsNotificationsApiAvailable() && IsNotificationsApiEnabled()) {
return RunNotificationsApi();
} else {
SendResponse(false);
return true;
}
}
message_center::NotificationType
NotificationsApiFunction::MapApiTemplateTypeToType(
api::notifications::TemplateType type) {
switch (type) {
case api::notifications::TEMPLATE_TYPE_NONE:
case api::notifications::TEMPLATE_TYPE_BASIC:
return message_center::NOTIFICATION_TYPE_BASE_FORMAT;
case api::notifications::TEMPLATE_TYPE_IMAGE:
return message_center::NOTIFICATION_TYPE_IMAGE;
case api::notifications::TEMPLATE_TYPE_LIST:
return message_center::NOTIFICATION_TYPE_MULTIPLE;
case api::notifications::TEMPLATE_TYPE_PROGRESS:
return message_center::NOTIFICATION_TYPE_PROGRESS;
default:
// Gracefully handle newer application code that is running on an older
// runtime that doesn't recognize the requested template.
return message_center::NOTIFICATION_TYPE_BASE_FORMAT;
}
}
NotificationsCreateFunction::NotificationsCreateFunction() {
}
NotificationsCreateFunction::~NotificationsCreateFunction() {
}
bool NotificationsCreateFunction::RunNotificationsApi() {
params_ = api::notifications::Create::Params::Create(*args_);
EXTENSION_FUNCTION_VALIDATE(params_.get());
const std::string extension_id(extension_->id());
std::string notification_id;
if (params_->notification_id.get() && !params_->notification_id->empty()) {
// If the caller provided a notificationId, use that.
notification_id = *params_->notification_id;
} else {
// Otherwise, use a randomly created GUID. In case that GenerateGUID returns
// the empty string, simply generate a random string.
notification_id = base::GenerateGUID();
if (notification_id.empty())
notification_id = base::RandBytesAsString(16);
}
SetResult(base::MakeUnique<base::StringValue>(notification_id));
// TODO(dewittj): Add more human-readable error strings if this fails.
if (!CreateNotification(notification_id, &params_->options))
return false;
SendResponse(true);
return true;
}
NotificationsUpdateFunction::NotificationsUpdateFunction() {
}
NotificationsUpdateFunction::~NotificationsUpdateFunction() {
}
bool NotificationsUpdateFunction::RunNotificationsApi() {
params_ = api::notifications::Update::Params::Create(*args_);
EXTENSION_FUNCTION_VALIDATE(params_.get());
// We are in update. If the ID doesn't exist, succeed but call the callback
// with "false".
const Notification* matched_notification =
g_browser_process->notification_ui_manager()->FindById(
CreateScopedIdentifier(extension_->id(), params_->notification_id),
NotificationUIManager::GetProfileID(GetProfile()));
if (!matched_notification) {
SetResult(base::MakeUnique<base::FundamentalValue>(false));
SendResponse(true);
return true;
}
// Copy the existing notification to get a writable version of it.
Notification notification = *matched_notification;
// If we have trouble updating the notification (could be improper use of API
// or some other reason), mark the function as failed, calling the callback
// with false.
// TODO(dewittj): Add more human-readable error strings if this fails.
bool could_update_notification = UpdateNotification(
params_->notification_id, &params_->options, &notification);
SetResult(
base::MakeUnique<base::FundamentalValue>(could_update_notification));
if (!could_update_notification)
return false;
// No trouble, created the notification, send true to the callback and
// succeed.
SendResponse(true);
return true;
}
NotificationsClearFunction::NotificationsClearFunction() {
}
NotificationsClearFunction::~NotificationsClearFunction() {
}
bool NotificationsClearFunction::RunNotificationsApi() {
params_ = api::notifications::Clear::Params::Create(*args_);
EXTENSION_FUNCTION_VALIDATE(params_.get());
bool cancel_result = g_browser_process->notification_ui_manager()->CancelById(
CreateScopedIdentifier(extension_->id(), params_->notification_id),
NotificationUIManager::GetProfileID(GetProfile()));
SetResult(base::MakeUnique<base::FundamentalValue>(cancel_result));
SendResponse(true);
return true;
}
NotificationsGetAllFunction::NotificationsGetAllFunction() {}
NotificationsGetAllFunction::~NotificationsGetAllFunction() {}
bool NotificationsGetAllFunction::RunNotificationsApi() {
NotificationUIManager* notification_ui_manager =
g_browser_process->notification_ui_manager();
std::set<std::string> notification_ids =
notification_ui_manager->GetAllIdsByProfileAndSourceOrigin(
NotificationUIManager::GetProfileID(GetProfile()), extension_->url());
std::unique_ptr<base::DictionaryValue> result(new base::DictionaryValue());
for (std::set<std::string>::iterator iter = notification_ids.begin();
iter != notification_ids.end(); iter++) {
result->SetBooleanWithoutPathExpansion(
StripScopeFromIdentifier(extension_->id(), *iter), true);
}
SetResult(std::move(result));
SendResponse(true);
return true;
}
NotificationsGetPermissionLevelFunction::
NotificationsGetPermissionLevelFunction() {}
NotificationsGetPermissionLevelFunction::
~NotificationsGetPermissionLevelFunction() {}
bool NotificationsGetPermissionLevelFunction::CanRunWhileDisabled() const {
return true;
}
bool NotificationsGetPermissionLevelFunction::RunNotificationsApi() {
api::notifications::PermissionLevel result =
AreExtensionNotificationsAllowed()
? api::notifications::PERMISSION_LEVEL_GRANTED
: api::notifications::PERMISSION_LEVEL_DENIED;
SetResult(base::MakeUnique<base::StringValue>(
api::notifications::ToString(result)));
SendResponse(true);
return true;
}
} // namespace extensions