blob: 1c64ed289fdb824cbf2c569d9eaf827344915eda [file]
// Copyright 2021 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.
#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"
@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"];
// A default close button label is provided by the platform but we explicitly
// override it in case the user decides to not use the OS language in Chrome.
// macOS 11 already shows a close button in the top-left corner.
if (!base::mac::IsAtLeastOS11()) {
[notification setOtherButtonTitle:l10n_util::GetNSString(
IDS_NOTIFICATION_BUTTON_CLOSE)];
}
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 11 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 on macOS 11.
if (base::mac::IsAtLeastOS11()) {
[notification setValue:@NO forKey:@"_hasActionButton"];
} else {
[notification setActionButtonTitle:l10n_util::GetNSString(
IDS_NOTIFICATION_BUTTON_MORE)];
}
// 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 retain]) {
[notification_center_ setDelegate:delegate_.get()];
}
MacNotificationServiceNS::~MacNotificationServiceNS() {
[notification_center_ setDelegate:nil];
}
void MacNotificationServiceNS::DisplayNotification(
mojom::NotificationPtr notification) {
base::scoped_nsobject<NSUserNotification> toast(
[[NSUserNotification alloc] init]);
[toast setTitle:base::SysUTF16ToNSString(notification->title)];
[toast setSubtitle:base::SysUTF16ToNSString(notification->subtitle)];
[toast setInformativeText:base::SysUTF16ToNSString(notification->body)];
[toast setUserInfo:GetMacNotificationUserInfo(notification)];
AddActionButtons(toast.get(), notification->buttons,
notification->show_settings_button);
if (!notification->icon.isNull())
[toast setContentImage:gfx::Image(notification->icon).ToNSImage()];
NSString* notification_id =
base::SysUTF8ToNSString(DeriveMacNotificationId(notification->meta->id));
[toast setIdentifier:notification_id];
LogMacNotificationDelivered(IsAppBundleAlertStyle(), /*success=*/true);
[notification_center_ deliverNotification:toast.get()];
}
void MacNotificationServiceNS::GetDisplayedNotifications(
mojom::ProfileIdentifierPtr profile,
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)) {
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];
}
} // 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=*/absl::nullopt);
_handler->OnNotificationAction(std::move(actionInfo));
}
// 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 {
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=*/absl::nullopt);
_handler->OnNotificationAction(std::move(actionInfo));
}
// 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) {
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=*/absl::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