| // 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 |