// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "chrome/services/mac_notifications/mac_notification_service_ns.h"
#import <Foundation/Foundation.h>
#import <Foundation/NSUserNotification.h>
#include <utility>
#include <vector>
#include "base/mac/mac_util.h"
#include "base/strings/sys_string_conversions.h"
#include "chrome/common/notifications/notification_constants.h"
#include "chrome/common/notifications/notification_operation.h"
#include "chrome/grit/generated_resources.h"
#import "chrome/services/mac_notifications/mac_notification_service_utils.h"
#include "chrome/services/mac_notifications/public/cpp/mac_notification_metrics.h"
#include "mojo/public/cpp/bindings/shared_remote.h"
#include "ui/base/l10n/l10n_util_mac.h"
#include "ui/gfx/image/image.h"
#include "url/origin.h"
// This class implements the Chromium interface to a deprecated API. It is in
// the process of being replaced, and warnings about its deprecation are not
// helpful. https://crbug.com/1127306
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
@interface AlertNSNotificationCenterDelegate
: NSObject <NSUserNotificationCenterDelegate>
- (instancetype)initWithActionHandler:
(mojo::PendingRemote<
mac_notifications::mojom::MacNotificationActionHandler>)handler;
@end
namespace {
NotificationOperation GetNotificationOperationFromNotification(
NSUserNotification* notification) {
if (notification.activationType == NSUserNotificationActivationTypeNone) {
return NotificationOperation::kClose;
}
if (notification.activationType !=
NSUserNotificationActivationTypeActionButtonClicked) {
return NotificationOperation::kClick;
}
int button_count = 1;
if ([notification
respondsToSelector:@selector(_alternateActionButtonTitles)]) {
int alternate_button_count =
[[notification valueForKey:@"_alternateActionButtonTitles"] count];
// We might not need alternateActionButtonTitles if there's only 1 button.
if (alternate_button_count)
button_count = alternate_button_count;
}
int button_index = 0;
if (button_count > 1) {
// There are multiple buttons in the overflow menu. Get the clicked index.
button_index =
[[notification valueForKey:@"_alternateActionIndex"] intValue];
}
bool has_settings_button = [[notification.userInfo
objectForKey:mac_notifications::kNotificationHasSettingsButton]
boolValue];
bool clicked_last_button = button_index == button_count - 1;
// The settings button is always the last button if present.
if (clicked_last_button && has_settings_button)
return NotificationOperation::kSettings;
// Otherwise the user clicked on an action button.
return NotificationOperation::kClick;
}
int GetActionButtonIndexFromNotification(NSUserNotification* notification) {
if (notification.activationType !=
NSUserNotificationActivationTypeActionButtonClicked ||
GetNotificationOperationFromNotification(notification) !=
NotificationOperation::kClick) {
return kNotificationInvalidButtonIndex;
}
// If we couldn't show an overflow menu there's only one button.
if (![notification
respondsToSelector:@selector(_alternateActionButtonTitles)]) {
return 0;
}
int alternate_button_count =
[[notification valueForKey:@"_alternateActionButtonTitles"] count];
if (alternate_button_count <= 1)
return 0;
// There are multiple buttons in the overflow menu. Get the clicked index.
return [[notification valueForKey:@"_alternateActionIndex"] intValue];
}
void AddActionButtons(
NSUserNotification* notification,
const std::vector<mac_notifications::mojom::NotificationActionButtonPtr>&
buttons,
bool show_settings_button) {
DCHECK_LE(buttons.size(), 2u);
if (![notification respondsToSelector:@selector(_showsButtons)])
return;
// Force the notification to always show its action buttons.
[notification setValue:@YES forKey:@"_showsButtons"];
NSMutableArray* action_buttons = [NSMutableArray arrayWithCapacity:3];
for (const auto& button : buttons)
[action_buttons addObject:base::SysUTF16ToNSString(button->title)];
if (show_settings_button) {
// If we can't show an action menu but need a settings button, only show the
// settings button and don't show developer provided actions.
if (![notification
respondsToSelector:@selector(_alwaysShowAlternateActionMenu)]) {
[action_buttons removeAllObjects];
}
[action_buttons
addObject:l10n_util::GetNSString(IDS_NOTIFICATION_BUTTON_SETTINGS)];
}
if (action_buttons.count == 0) {
// Don't show action button if no actions needed.
[notification setHasActionButton:NO];
return;
}
if (action_buttons.count == 1) {
// Only one action so we don't need a menu. Just set the button title.
[notification setActionButtonTitle:[action_buttons firstObject]];
return;
}
DCHECK([notification
respondsToSelector:@selector(_alwaysShowAlternateActionMenu)]);
DCHECK([notification
respondsToSelector:@selector(_alternateActionButtonTitles)]);
// macOS does not support overriding the text of the overflow button and
// will always show "Options" via this API. Setting actionButtonTitle just
// appends another button into the overflow menu. Only the new UNNotification
// API allows overriding this title.
[notification setValue:@NO forKey:@"_hasActionButton"];
// Show the alternate menu with developer actions and settings if needed.
[notification setValue:@YES forKey:@"_alwaysShowAlternateActionMenu"];
[notification setValue:action_buttons forKey:@"_alternateActionButtonTitles"];
}
} // namespace
namespace mac_notifications {
MacNotificationServiceNS::MacNotificationServiceNS(
mojo::PendingReceiver<mojom::MacNotificationService> service,
mojo::PendingRemote<mojom::MacNotificationActionHandler> handler,
NSUserNotificationCenter* notification_center)
: binding_(this, std::move(service)),
delegate_([[AlertNSNotificationCenterDelegate alloc]
initWithActionHandler:std::move(handler)]),
notification_center_(notification_center) {
notification_center_.delegate = delegate_;
}
MacNotificationServiceNS::~MacNotificationServiceNS() {
notification_center_.delegate = nil;
}
void MacNotificationServiceNS::DisplayNotification(
mojom::NotificationPtr notification) {
NSUserNotification* toast = [[NSUserNotification alloc] init];
toast.title = base::SysUTF16ToNSString(notification->title);
toast.subtitle = base::SysUTF16ToNSString(notification->subtitle);
toast.informativeText = base::SysUTF16ToNSString(notification->body);
toast.userInfo = GetMacNotificationUserInfo(notification);
AddActionButtons(toast, notification->buttons,
notification->show_settings_button);
if (!notification->icon.isNull())
toast.contentImage = gfx::Image(notification->icon).ToNSImage();
NSString* notification_id =
base::SysUTF8ToNSString(DeriveMacNotificationId(notification->meta->id));
toast.identifier = notification_id;
[notification_center_ deliverNotification:toast];
}
void MacNotificationServiceNS::GetDisplayedNotifications(
mojom::ProfileIdentifierPtr profile,
const std::optional<GURL>& origin,
GetDisplayedNotificationsCallback callback) {
std::vector<mojom::NotificationIdentifierPtr> notifications;
// Note: |profile| might be null if we want all notifications.
NSString* profile_id = profile ? base::SysUTF8ToNSString(profile->id) : nil;
bool incognito = profile && profile->incognito;
for (NSUserNotification* toast in notification_center_
.deliveredNotifications) {
NSString* toast_id = [toast.userInfo objectForKey:kNotificationId];
NSString* toast_profile_id =
[toast.userInfo objectForKey:kNotificationProfileId];
BOOL toast_incognito =
[[toast.userInfo objectForKey:kNotificationIncognito] boolValue];
if (!profile_id || ([profile_id isEqualToString:toast_profile_id] &&
incognito == toast_incognito)) {
NSString* toast_origin_url =
[toast.userInfo objectForKey:kNotificationOrigin];
GURL toast_origin = GURL(base::SysNSStringToUTF8(toast_origin_url));
if (!origin.has_value() || url::IsSameOriginWith(toast_origin, *origin)) {
auto profile_identifier = mojom::ProfileIdentifier::New(
base::SysNSStringToUTF8(toast_profile_id), toast_incognito);
notifications.push_back(mojom::NotificationIdentifier::New(
base::SysNSStringToUTF8(toast_id), std::move(profile_identifier)));
}
}
}
std::move(callback).Run(std::move(notifications));
}
void MacNotificationServiceNS::CloseNotification(
mojom::NotificationIdentifierPtr identifier) {
NSString* notification_id = base::SysUTF8ToNSString(identifier->id);
NSString* profile_id = base::SysUTF8ToNSString(identifier->profile->id);
bool incognito = identifier->profile->incognito;
for (NSUserNotification* toast in notification_center_
.deliveredNotifications) {
NSString* toast_id = [toast.userInfo objectForKey:kNotificationId];
NSString* toast_profile_id =
[toast.userInfo objectForKey:kNotificationProfileId];
BOOL toast_incognito =
[[toast.userInfo objectForKey:kNotificationIncognito] boolValue];
if ([notification_id isEqualToString:toast_id] &&
[profile_id isEqualToString:toast_profile_id] &&
incognito == toast_incognito) {
[notification_center_ removeDeliveredNotification:toast];
break;
}
}
}
void MacNotificationServiceNS::CloseNotificationsForProfile(
mojom::ProfileIdentifierPtr profile) {
NSString* profile_id = base::SysUTF8ToNSString(profile->id);
bool incognito = profile->incognito;
for (NSUserNotification* toast in notification_center_
.deliveredNotifications) {
NSString* toast_profile_id =
[toast.userInfo objectForKey:kNotificationProfileId];
BOOL toast_incognito =
[[toast.userInfo objectForKey:kNotificationIncognito] boolValue];
if ([profile_id isEqualToString:toast_profile_id] &&
incognito == toast_incognito) {
[notification_center_ removeDeliveredNotification:toast];
}
}
}
void MacNotificationServiceNS::CloseAllNotifications() {
[notification_center_ removeAllDeliveredNotifications];
}
void MacNotificationServiceNS::OkayToTerminateService(
OkayToTerminateServiceCallback callback) {
GetDisplayedNotifications(
/*profile=*/nullptr, /*origin=*/std::nullopt,
base::BindOnce([](std::vector<mojom::NotificationIdentifierPtr>
notifications) {
return notifications.empty();
}).Then(std::move(callback)));
}
} // namespace mac_notifications
@implementation AlertNSNotificationCenterDelegate {
// We're using a SharedRemote here as we need to reply on the same sequence
// that created the mojo connection and the methods below get called by macOS.
mojo::SharedRemote<mac_notifications::mojom::MacNotificationActionHandler>
_handler;
}
- (instancetype)initWithActionHandler:
(mojo::PendingRemote<
mac_notifications::mojom::MacNotificationActionHandler>)handler {
if ((self = [super init])) {
_handler.Bind(std::move(handler), /*bind_task_runner=*/nullptr);
}
return self;
}
- (void)userNotificationCenter:(NSUserNotificationCenter*)center
didActivateNotification:(NSUserNotification*)notification {
mac_notifications::mojom::NotificationMetadataPtr meta =
mac_notifications::GetMacNotificationMetadata(notification.userInfo);
NotificationOperation operation =
GetNotificationOperationFromNotification(notification);
int buttonIndex = GetActionButtonIndexFromNotification(notification);
auto actionInfo = mac_notifications::mojom::NotificationActionInfo::New(
std::move(meta), operation, buttonIndex, /*reply=*/std::nullopt);
_handler->OnNotificationAction(std::move(actionInfo));
}
// Overridden 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 {
mac_notifications::mojom::NotificationMetadataPtr meta =
mac_notifications::GetMacNotificationMetadata(notification.userInfo);
auto operation = NotificationOperation::kClose;
int buttonIndex = kNotificationInvalidButtonIndex;
auto actionInfo = mac_notifications::mojom::NotificationActionInfo::New(
std::move(meta), operation, buttonIndex, /*reply=*/std::nullopt);
_handler->OnNotificationAction(std::move(actionInfo));
}
// Overridden 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) {
DCHECK(notification);
mac_notifications::mojom::NotificationMetadataPtr meta =
mac_notifications::GetMacNotificationMetadata(notification.userInfo);
auto operation = NotificationOperation::kClose;
int buttonIndex = kNotificationInvalidButtonIndex;
auto actionInfo = mac_notifications::mojom::NotificationActionInfo::New(
std::move(meta), operation, buttonIndex, /*reply=*/std::nullopt);
_handler->OnNotificationAction(std::move(actionInfo));
}
}
- (BOOL)userNotificationCenter:(NSUserNotificationCenter*)center
shouldPresentNotification:(NSUserNotification*)nsNotification {
// Always display notifications, regardless of whether the app is foreground.
return YES;
}
@end
#pragma clang diagnostic pop