blob: 08078c5f168ebf562fd19c0ebd621f1f7a5a6b5d [file] [log] [blame]
// Copyright 2015 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/notifications/notification_platform_bridge_mac.h"
#include <memory>
#include <utility>
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/callback.h"
#include "base/callback_helpers.h"
#include "base/i18n/number_formatting.h"
#include "base/mac/bundle_locations.h"
#include "base/mac/foundation_util.h"
#include "base/mac/mac_util.h"
#include "base/mac/scoped_mach_port.h"
#include "base/mac/scoped_nsobject.h"
#include "base/metrics/histogram_macros.h"
#include "base/optional.h"
#include "base/strings/nullable_string16.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/post_task.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/notifications/notification_common.h"
#include "chrome/browser/notifications/notification_display_service_impl.h"
#include "chrome/browser/notifications/platform_notification_service_impl.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/cocoa/notifications/notification_builder_mac.h"
#include "chrome/browser/ui/cocoa/notifications/notification_constants_mac.h"
#import "chrome/browser/ui/cocoa/notifications/notification_delivery.h"
#import "chrome/browser/ui/cocoa/notifications/notification_response_builder_mac.h"
#include "chrome/common/buildflags.h"
#include "chrome/grit/generated_resources.h"
#include "components/crash/content/app/crashpad.h"
#include "components/url_formatter/elide_url.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
#include "third_party/blink/public/platform/modules/notifications/web_notification_constants.h"
#include "third_party/crashpad/crashpad/client/crashpad_client.h"
#include "ui/base/l10n/l10n_util_mac.h"
#include "ui/message_center/public/cpp/notification.h"
#include "ui/message_center/public/cpp/notification_types.h"
#include "url/gurl.h"
#include "url/origin.h"
@class NSUserNotification;
@class NSUserNotificationCenter;
// The mapping from web notifications to NsUserNotification works as follows
// notification#title in NSUserNotification.title
// notification#message in NSUserNotification.informativeText
// notification#context_message in NSUserNotification.subtitle
// notification#id in NSUserNotification.identifier (10.9)
// notification#icon in NSUserNotification.contentImage (10.9)
// Site settings button is implemented as NSUserNotification's action button
// Not easy to implement:
// -notification.requireInteraction
// TODO(miguelg) implement the following features
// - Sound names can be implemented by setting soundName in NSUserNotification
// NSUserNotificationDefaultSoundName gives you the platform default.
namespace {
// Loads the profile and process the Notification response
void DoProcessNotificationResponse(NotificationCommon::Operation operation,
NotificationHandler::Type type,
const std::string& profile_id,
bool incognito,
const GURL& origin,
const std::string& notification_id,
const base::Optional<int>& action_index,
const base::Optional<base::string16>& reply,
const base::Optional<bool>& by_user) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
// Profile ID can be empty for system notifications, which are not bound to a
// profile, but system notifications are transient and thus not handled by
// this NotificationPlatformBridge.
// When transient notifications are supported, this should route the
// notification response to the system NotificationDisplayService.
DCHECK(!profile_id.empty());
ProfileManager* profileManager = g_browser_process->profile_manager();
DCHECK(profileManager);
profileManager->LoadProfile(
profile_id, incognito,
base::Bind(&NotificationDisplayServiceImpl::ProfileLoadedCallback,
operation, type, origin, notification_id, action_index, reply,
by_user));
}
// This enum backs an UMA histogram, so it should be treated as append-only.
enum XPCConnectionEvent {
INTERRUPTED = 0,
INVALIDATED,
XPC_CONNECTION_EVENT_COUNT
};
void RecordXPCEvent(XPCConnectionEvent event) {
UMA_HISTOGRAM_ENUMERATION("Notifications.XPCConnectionEvent", event,
XPC_CONNECTION_EVENT_COUNT);
}
base::string16 CreateNotificationTitle(
const message_center::Notification& notification) {
base::string16 title;
if (notification.type() == message_center::NOTIFICATION_TYPE_PROGRESS) {
title += base::FormatPercent(notification.progress());
title += base::UTF8ToUTF16(" - ");
}
title += notification.title();
return title;
}
bool IsPersistentNotification(
const message_center::Notification& notification) {
return notification.never_timeout() ||
notification.type() == message_center::NOTIFICATION_TYPE_PROGRESS;
}
base::string16 CreateNotificationContext(
const message_center::Notification& notification,
bool requires_attribution) {
if (!requires_attribution)
return notification.context_message();
// Mac OS notifications don't provide a good way to elide the domain (or tell
// you the maximum width of the subtitle field). We have experimentally
// determined the maximum number of characters that fit using the widest
// possible character (m). If the domain fits in those character we show it
// completely. Otherwise we use eTLD + 1.
// These numbers have been obtained through experimentation on various
// Mac OS platforms.
constexpr size_t kMaxDomainLengthAlert = 19;
constexpr size_t kMaxDomainLengthBanner = 28;
size_t max_characters = IsPersistentNotification(notification)
? kMaxDomainLengthAlert
: kMaxDomainLengthBanner;
base::string16 origin = url_formatter::FormatOriginForSecurityDisplay(
url::Origin::Create(notification.origin_url()),
url_formatter::SchemeDisplay::OMIT_HTTP_AND_HTTPS);
if (origin.size() <= max_characters)
return origin;
// Too long, use etld+1
base::string16 etldplusone =
base::UTF8ToUTF16(net::registry_controlled_domains::GetDomainAndRegistry(
notification.origin_url(),
net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES));
// localhost, raw IPs etc. are not handled by GetDomainAndRegistry.
if (etldplusone.empty())
return origin;
return etldplusone;
}
} // namespace
// A Cocoa class that represents the delegate of NSUserNotificationCenter and
// can forward commands to C++.
@interface NotificationCenterDelegate
: NSObject<NSUserNotificationCenterDelegate> {
}
@end
// Interface to communicate with the Alert XPC service.
@interface AlertDispatcherImpl : NSObject<AlertDispatcher>
@end
// /////////////////////////////////////////////////////////////////////////////
NotificationPlatformBridgeMac::NotificationPlatformBridgeMac(
NSUserNotificationCenter* notification_center,
id<AlertDispatcher> alert_dispatcher)
: delegate_([NotificationCenterDelegate alloc]),
notification_center_([notification_center retain]),
alert_dispatcher_([alert_dispatcher retain]) {
[notification_center_ setDelegate:delegate_.get()];
}
NotificationPlatformBridgeMac::~NotificationPlatformBridgeMac() {
[notification_center_ setDelegate:nil];
// TODO(miguelg) do not remove banners if possible.
[notification_center_ removeAllDeliveredNotifications];
[alert_dispatcher_ closeAllNotifications];
}
// static
std::unique_ptr<NotificationPlatformBridge>
NotificationPlatformBridge::Create() {
base::scoped_nsobject<AlertDispatcherImpl> alert_dispatcher(
[[AlertDispatcherImpl alloc] init]);
return std::make_unique<NotificationPlatformBridgeMac>(
[NSUserNotificationCenter defaultUserNotificationCenter],
alert_dispatcher.get());
}
// static
bool NotificationPlatformBridge::CanHandleType(
NotificationHandler::Type notification_type) {
return notification_type != NotificationHandler::Type::TRANSIENT;
}
void NotificationPlatformBridgeMac::Display(
NotificationHandler::Type notification_type,
Profile* profile,
const message_center::Notification& notification,
std::unique_ptr<NotificationCommon::Metadata> metadata) {
base::scoped_nsobject<NotificationBuilder> builder(
[[NotificationBuilder alloc]
initWithCloseLabel:l10n_util::GetNSString(IDS_NOTIFICATION_BUTTON_CLOSE)
optionsLabel:l10n_util::GetNSString(IDS_NOTIFICATION_BUTTON_MORE)
settingsLabel:l10n_util::GetNSString(
IDS_NOTIFICATION_BUTTON_SETTINGS)]);
[builder
setTitle:base::SysUTF16ToNSString(CreateNotificationTitle(notification))];
base::string16 context_message =
notification.items().empty()
? notification.message()
: (notification.items().at(0).title + base::UTF8ToUTF16(" - ") +
notification.items().at(0).message);
[builder setContextMessage:base::SysUTF16ToNSString(context_message)];
bool requires_attribution =
notification.context_message().empty() &&
notification_type != NotificationHandler::Type::EXTENSION;
[builder setSubTitle:base::SysUTF16ToNSString(CreateNotificationContext(
notification, requires_attribution))];
if (!notification.icon().IsEmpty()) {
[builder setIcon:notification.icon().ToNSImage()];
}
[builder
setShowSettingsButton:(notification_type !=
NotificationHandler::Type::EXTENSION &&
notification_type !=
NotificationHandler::Type::SEND_TAB_TO_SELF)];
std::vector<message_center::ButtonInfo> buttons = notification.buttons();
if (!buttons.empty()) {
DCHECK_LE(buttons.size(), blink::kWebNotificationMaxActions);
NSString* buttonOne = base::SysUTF16ToNSString(buttons[0].title);
NSString* buttonTwo = nullptr;
if (buttons.size() > 1)
buttonTwo = base::SysUTF16ToNSString(buttons[1].title);
[builder setButtons:buttonOne secondaryButton:buttonTwo];
}
[builder setTag:base::SysUTF8ToNSString(notification.id())];
// If renotify is needed, delete the notification with the same id
// from the notification center before displaying this one.
// TODO(miguelg): This will need to work for alerts as well via XPC
// once supported.
if (notification.renotify()) {
NSUserNotificationCenter* notification_center =
[NSUserNotificationCenter defaultUserNotificationCenter];
for (NSUserNotification* existing_notification in
[notification_center deliveredNotifications]) {
NSString* identifier = [existing_notification valueForKey:@"identifier"];
if ([identifier
isEqualToString:base::SysUTF8ToNSString(notification.id())]) {
[notification_center removeDeliveredNotification:existing_notification];
break;
}
}
}
[builder setOrigin:base::SysUTF8ToNSString(notification.origin_url().spec())];
[builder setNotificationId:base::SysUTF8ToNSString(notification.id())];
[builder setProfileId:base::SysUTF8ToNSString(GetProfileId(profile))];
[builder setIncognito:profile->IsOffTheRecord()];
[builder
setNotificationType:[NSNumber numberWithInteger:static_cast<NSInteger>(
notification_type)]];
// Send persistent notifications to the XPC service so they
// can be displayed as alerts. Chrome itself can only display
// banners.
if (IsPersistentNotification(notification)) {
NSDictionary* dict = [builder buildDictionary];
[alert_dispatcher_ dispatchNotification:dict];
} else {
NSUserNotification* toast = [builder buildUserNotification];
[notification_center_ deliverNotification:toast];
}
}
void NotificationPlatformBridgeMac::Close(Profile* profile,
const std::string& notification_id) {
NSString* candidate_id = base::SysUTF8ToNSString(notification_id);
NSString* current_profile_id = base::SysUTF8ToNSString(GetProfileId(profile));
bool notification_removed = false;
for (NSUserNotification* toast in
[notification_center_ deliveredNotifications]) {
NSString* toast_id =
[toast.userInfo objectForKey:notification_constants::kNotificationId];
NSString* persistent_profile_id = [toast.userInfo
objectForKey:notification_constants::kNotificationProfileId];
if ([toast_id isEqualToString:candidate_id] &&
[persistent_profile_id isEqualToString:current_profile_id]) {
[notification_center_ removeDeliveredNotification:toast];
notification_removed = true;
break;
}
}
// If no banner existed with that ID try to see if there is an alert
// in the xpc server.
if (!notification_removed) {
[alert_dispatcher_ closeNotificationWithId:candidate_id
withProfileId:current_profile_id];
}
}
void NotificationPlatformBridgeMac::GetDisplayed(
Profile* profile,
GetDisplayedNotificationsCallback callback) const {
[alert_dispatcher_ getDisplayedAlertsForProfileId:base::SysUTF8ToNSString(
GetProfileId(profile))
incognito:profile->IsOffTheRecord()
notificationCenter:notification_center_
callback:std::move(callback)];
}
void NotificationPlatformBridgeMac::SetReadyCallback(
NotificationBridgeReadyCallback callback) {
std::move(callback).Run(true);
}
void NotificationPlatformBridgeMac::DisplayServiceShutDown(Profile* profile) {}
// static
void NotificationPlatformBridgeMac::ProcessNotificationResponse(
NSDictionary* response) {
if (!NotificationPlatformBridgeMac::VerifyNotificationData(response))
return;
NSNumber* button_index =
[response objectForKey:notification_constants::kNotificationButtonIndex];
NSNumber* operation =
[response objectForKey:notification_constants::kNotificationOperation];
std::string notification_origin = base::SysNSStringToUTF8(
[response objectForKey:notification_constants::kNotificationOrigin]);
std::string notification_id = base::SysNSStringToUTF8(
[response objectForKey:notification_constants::kNotificationId]);
std::string profile_id = base::SysNSStringToUTF8(
[response objectForKey:notification_constants::kNotificationProfileId]);
NSNumber* is_incognito =
[response objectForKey:notification_constants::kNotificationIncognito];
NSNumber* notification_type =
[response objectForKey:notification_constants::kNotificationType];
base::Optional<int> action_index;
if (button_index.intValue !=
notification_constants::kNotificationInvalidButtonIndex) {
action_index = button_index.intValue;
}
base::PostTaskWithTraits(
FROM_HERE, {content::BrowserThread::UI},
base::BindOnce(DoProcessNotificationResponse,
static_cast<NotificationCommon::Operation>(
operation.unsignedIntValue),
static_cast<NotificationHandler::Type>(
notification_type.unsignedIntValue),
profile_id, [is_incognito boolValue],
GURL(notification_origin), notification_id, action_index,
base::nullopt /* reply */, true /* by_user */));
}
// static
bool NotificationPlatformBridgeMac::VerifyNotificationData(
NSDictionary* response) {
if (![response
objectForKey:notification_constants::kNotificationButtonIndex] ||
![response objectForKey:notification_constants::kNotificationOperation] ||
![response objectForKey:notification_constants::kNotificationId] ||
![response objectForKey:notification_constants::kNotificationProfileId] ||
![response objectForKey:notification_constants::kNotificationIncognito] ||
![response objectForKey:notification_constants::kNotificationType]) {
LOG(ERROR) << "Missing required key";
return false;
}
NSNumber* button_index =
[response objectForKey:notification_constants::kNotificationButtonIndex];
NSNumber* operation =
[response objectForKey:notification_constants::kNotificationOperation];
NSString* notification_id =
[response objectForKey:notification_constants::kNotificationId];
NSString* profile_id =
[response objectForKey:notification_constants::kNotificationProfileId];
NSNumber* notification_type =
[response objectForKey:notification_constants::kNotificationType];
if (button_index.intValue <
notification_constants::kNotificationInvalidButtonIndex ||
button_index.intValue >=
static_cast<int>(blink::kWebNotificationMaxActions)) {
LOG(ERROR) << "Invalid number of buttons supplied "
<< button_index.intValue;
return false;
}
if (operation.unsignedIntValue > NotificationCommon::OPERATION_MAX) {
LOG(ERROR) << operation.unsignedIntValue
<< " does not correspond to a valid operation.";
return false;
}
if (notification_id.length <= 0) {
LOG(ERROR) << "Notification Id is empty";
return false;
}
if (profile_id.length <= 0) {
LOG(ERROR) << "ProfileId not provided";
return false;
}
if (notification_type.unsignedIntValue >
static_cast<unsigned int>(NotificationHandler::Type::MAX)) {
LOG(ERROR) << notification_type.unsignedIntValue
<< " Does not correspond to a valid operation.";
return false;
}
// Origin is not actually required but if it's there it should be a valid one.
NSString* origin =
[response objectForKey:notification_constants::kNotificationOrigin];
if (origin) {
std::string notificationOrigin = base::SysNSStringToUTF8(origin);
GURL url(notificationOrigin);
if (!url.is_valid())
return false;
}
return true;
}
// /////////////////////////////////////////////////////////////////////////////
@implementation NotificationCenterDelegate
- (void)userNotificationCenter:(NSUserNotificationCenter*)center
didActivateNotification:(NSUserNotification*)notification {
NSDictionary* notificationResponse =
[NotificationResponseBuilder buildActivatedDictionary:notification];
NotificationPlatformBridgeMac::ProcessNotificationResponse(
notificationResponse);
}
// Overriden from _NSUserNotificationCenterDelegatePrivate.
// Emitted when a user clicks the "Close" button in the notification.
// It not is emitted if the notification is closed from the notification
// center or if the app is not running at the time the Close button is
// pressed so it's essentially just a best effort way to detect
// notifications closed by the user.
- (void)userNotificationCenter:(NSUserNotificationCenter*)center
didDismissAlert:(NSUserNotification*)notification {
NSDictionary* notificationResponse =
[NotificationResponseBuilder buildDismissedDictionary:notification];
NotificationPlatformBridgeMac::ProcessNotificationResponse(
notificationResponse);
}
// Overriden from _NSUserNotificationCenterDelegatePrivate.
// Emitted when a user closes a notification from the notification center.
// This is an undocumented method introduced in 10.8 according to
// https://bugzilla.mozilla.org/show_bug.cgi?id=852648#c21
- (void)userNotificationCenter:(NSUserNotificationCenter*)center
didRemoveDeliveredNotifications:(NSArray*)notifications {
for (NSUserNotification* notification in notifications) {
NSDictionary* notificationResponse =
[NotificationResponseBuilder buildDismissedDictionary:notification];
NotificationPlatformBridgeMac::ProcessNotificationResponse(
notificationResponse);
}
}
- (BOOL)userNotificationCenter:(NSUserNotificationCenter*)center
shouldPresentNotification:(NSUserNotification*)nsNotification {
// Always display notifications, regardless of whether the app is foreground.
return YES;
}
@end
@implementation AlertDispatcherImpl {
// The connection to the XPC server in charge of delivering alerts.
base::scoped_nsobject<NSXPCConnection> xpcConnection_;
// YES if the remote object has had |-setMachExceptionPort:| called
// since the service was last started, interrupted, or invalidated.
// If NO, then -serviceProxy will set the exception port.
BOOL setExceptionPort_;
}
- (instancetype)init {
if ((self = [super init])) {
xpcConnection_.reset([[NSXPCConnection alloc]
initWithServiceName:
[NSString
stringWithFormat:notification_constants::kAlertXPCServiceName,
[base::mac::OuterBundle() bundleIdentifier]]]);
xpcConnection_.get().remoteObjectInterface =
[NSXPCInterface interfaceWithProtocol:@protocol(NotificationDelivery)];
xpcConnection_.get().interruptionHandler = ^{
// We will be getting this handler both when the XPC server crashes or
// when it decides to close the connection.
LOG(WARNING) << "AlertNotificationService: XPC connection interrupted.";
RecordXPCEvent(INTERRUPTED);
setExceptionPort_ = NO;
};
xpcConnection_.get().invalidationHandler = ^{
// This means that the connection should be recreated if it needs
// to be used again.
LOG(WARNING) << "AlertNotificationService: XPC connection invalidated.";
RecordXPCEvent(INVALIDATED);
setExceptionPort_ = NO;
};
xpcConnection_.get().exportedInterface =
[NSXPCInterface interfaceWithProtocol:@protocol(NotificationReply)];
xpcConnection_.get().exportedObject = self;
[xpcConnection_ resume];
}
return self;
}
// AlertDispatcher:
- (void)dispatchNotification:(NSDictionary*)data {
[[self serviceProxy] deliverNotification:data];
}
- (void)closeNotificationWithId:(NSString*)notificationId
withProfileId:(NSString*)profileId {
[[self serviceProxy] closeNotificationWithId:notificationId
withProfileId:profileId];
}
- (void)closeAllNotifications {
[[self serviceProxy] closeAllNotifications];
}
- (void)
getDisplayedAlertsForProfileId:(NSString*)profileId
incognito:(BOOL)incognito
notificationCenter:(NSUserNotificationCenter*)notificationCenter
callback:(GetDisplayedNotificationsCallback)callback {
// Create a copyable version of the OnceCallback because ObjectiveC blocks
// copy all referenced variables via copy constructor.
auto copyable_callback = base::AdaptCallbackForRepeating(std::move(callback));
auto reply = ^(NSArray* alerts) {
std::set<std::string> displayedNotifications;
for (NSUserNotification* toast in
[notificationCenter deliveredNotifications]) {
NSString* toastProfileId = [toast.userInfo
objectForKey:notification_constants::kNotificationProfileId];
BOOL incognitoNotification = [[toast.userInfo
objectForKey:notification_constants::kNotificationIncognito]
boolValue];
if ([toastProfileId isEqualToString:profileId] &&
incognito == incognitoNotification) {
displayedNotifications.insert(base::SysNSStringToUTF8([toast.userInfo
objectForKey:notification_constants::kNotificationId]));
}
}
for (NSString* alert in alerts)
displayedNotifications.insert(base::SysNSStringToUTF8(alert));
base::PostTaskWithTraits(
FROM_HERE, {content::BrowserThread::UI},
base::BindOnce(copyable_callback, std::move(displayedNotifications),
true /* supports_synchronization */));
};
[[self serviceProxy] getDisplayedAlertsForProfileId:profileId
andIncognito:incognito
withReply:reply];
}
// NotificationReply:
- (void)notificationClick:(NSDictionary*)notificationResponseData {
NotificationPlatformBridgeMac::ProcessNotificationResponse(
notificationResponseData);
}
// Private methods:
// Retrieves the connection's remoteObjectProxy. Always use this as opposed
// to going directly through the connection, since this will ensure that the
// service has its exception port configured for crash reporting.
- (id<NotificationDelivery>)serviceProxy {
id<NotificationDelivery> proxy = [xpcConnection_ remoteObjectProxy];
if (!setExceptionPort_) {
base::mac::ScopedMachSendRight exceptionPort(
crash_reporter::GetCrashpadClient().GetHandlerMachPort());
base::scoped_nsobject<CrXPCMachPort> xpcPort(
[[CrXPCMachPort alloc] initWithMachSendRight:std::move(exceptionPort)]);
[proxy setMachExceptionPort:xpcPort];
setExceptionPort_ = YES;
}
return proxy;
}
@end