Having this new interface should allow us to introduce more observer topics without fears on internal backwards compatibility. Existing callers can still use nsIObservers with the same behavior, while ideally we should slowly move them to this new approach. Differential Revision: https://phabricator.services.mozilla.com/D308321
500 lines
16 KiB
Plaintext
500 lines
16 KiB
Plaintext
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
#include "MacOSNotificationCenter.h"
|
|
#import <AppKit/AppKit.h>
|
|
#include "imgIContainer.h"
|
|
#include "imgIRequest.h"
|
|
#include "nsCOMPtr.h"
|
|
#import "nsCocoaUtils.h"
|
|
#include "nsComponentManagerUtils.h"
|
|
#include "nsContentUtils.h"
|
|
#include "nsICancelable.h"
|
|
#include "nsIObserver.h"
|
|
#include "nsIStringBundle.h"
|
|
#include "nsNetUtil.h"
|
|
#include "nsObjCExceptions.h"
|
|
#include "nsString.h"
|
|
|
|
using namespace mozilla;
|
|
|
|
#define MAX_NOTIFICATION_NAME_LEN 5000
|
|
|
|
static constexpr nsLiteralString kActionSuffix = u"-moz"_ns;
|
|
|
|
@interface mozNotificationCenterDelegate
|
|
: NSObject <NSUserNotificationCenterDelegate> {
|
|
MacOSNotificationCenter* mMacOSNC;
|
|
}
|
|
- (id)initWithMacOSNC:(MacOSNotificationCenter*)macosnc;
|
|
@end
|
|
|
|
@implementation mozNotificationCenterDelegate
|
|
|
|
- (id)initWithMacOSNC:(MacOSNotificationCenter*)macosnc {
|
|
[super init];
|
|
// We should *never* outlive this MacOSNotificationCenter.
|
|
mMacOSNC = macosnc;
|
|
return self;
|
|
}
|
|
|
|
- (void)userNotificationCenter:(NSUserNotificationCenter*)center
|
|
didDeliverNotification:(NSUserNotification*)notification {
|
|
}
|
|
|
|
- (void)userNotificationCenter:(NSUserNotificationCenter*)center
|
|
didActivateNotification:(NSUserNotification*)notification {
|
|
mMacOSNC->OnActivate([[notification userInfo] valueForKey:@"name"],
|
|
notification.activationType,
|
|
notification.additionalActivationAction);
|
|
}
|
|
|
|
- (BOOL)userNotificationCenter:(NSUserNotificationCenter*)center
|
|
shouldPresentNotification:(NSUserNotification*)notification {
|
|
return YES;
|
|
}
|
|
|
|
// This is an undocumented method that we need for parity with Safari.
|
|
// Apple bug #15440664.
|
|
- (void)userNotificationCenter:(NSUserNotificationCenter*)center
|
|
didRemoveDeliveredNotifications:(NSArray*)notifications {
|
|
for (NSUserNotification* notification in notifications) {
|
|
NSString* name = [[notification userInfo] valueForKey:@"name"];
|
|
mMacOSNC->CloseAlertCocoaString(name);
|
|
}
|
|
}
|
|
|
|
// This is an undocumented method that we need to be notified if a user clicks
|
|
// the close button.
|
|
- (void)userNotificationCenter:(NSUserNotificationCenter*)center
|
|
didDismissAlert:(NSUserNotification*)notification {
|
|
NSString* name = [[notification userInfo] valueForKey:@"name"];
|
|
mMacOSNC->CloseAlertCocoaString(name);
|
|
}
|
|
|
|
@end
|
|
|
|
namespace mozilla {
|
|
|
|
class MacOSNotificationInfo final : public nsISupports {
|
|
private:
|
|
virtual ~MacOSNotificationInfo();
|
|
|
|
public:
|
|
NS_DECL_ISUPPORTS
|
|
MacOSNotificationInfo(NSString* name,
|
|
nsIAlertNotification* aAlertNotification,
|
|
nsIAlertCallbacks* aAlertCallbacks,
|
|
const nsAString& alertCookie, bool privateBrowsing);
|
|
|
|
NSString* mName;
|
|
nsCOMPtr<nsIAlertNotification> mAlertNotification;
|
|
nsCOMPtr<nsIAlertCallbacks> mAlertCallbacks;
|
|
nsString mCookie;
|
|
bool mPrivateBrowsing;
|
|
};
|
|
|
|
NS_IMPL_ISUPPORTS0(MacOSNotificationInfo)
|
|
|
|
MacOSNotificationInfo::MacOSNotificationInfo(
|
|
NSString* name, nsIAlertNotification* aAlertNotification,
|
|
nsIAlertCallbacks* aAlertCallbacks, const nsAString& alertCookie,
|
|
bool privateBrowsing) {
|
|
NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
|
|
|
|
NS_ASSERTION(name, "Cannot create MacOSNotificationInfo without a name!");
|
|
mName = [name retain];
|
|
mAlertNotification = aAlertNotification;
|
|
mAlertCallbacks = aAlertCallbacks;
|
|
mCookie = alertCookie;
|
|
mPrivateBrowsing = privateBrowsing;
|
|
|
|
NS_OBJC_END_TRY_IGNORE_BLOCK;
|
|
}
|
|
|
|
MacOSNotificationInfo::~MacOSNotificationInfo() {
|
|
NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
|
|
|
|
[mName release];
|
|
|
|
NS_OBJC_END_TRY_IGNORE_BLOCK;
|
|
}
|
|
|
|
static NSUserNotificationCenter* GetNotificationCenter() {
|
|
NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
|
|
|
|
Class c = NSClassFromString(@"NSUserNotificationCenter");
|
|
return [c performSelector:@selector(defaultUserNotificationCenter)];
|
|
|
|
NS_OBJC_END_TRY_BLOCK_RETURN(nil);
|
|
}
|
|
|
|
MacOSNotificationCenter::MacOSNotificationCenter() {
|
|
NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
|
|
|
|
mDelegate = [[mozNotificationCenterDelegate alloc] initWithMacOSNC:this];
|
|
GetNotificationCenter().delegate = mDelegate;
|
|
mSuppressForScreenSharing = false;
|
|
|
|
NS_OBJC_END_TRY_IGNORE_BLOCK;
|
|
}
|
|
|
|
MacOSNotificationCenter::~MacOSNotificationCenter() {
|
|
NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
|
|
|
|
[GetNotificationCenter() removeAllDeliveredNotifications];
|
|
[mDelegate release];
|
|
|
|
NS_OBJC_END_TRY_IGNORE_BLOCK;
|
|
}
|
|
|
|
NS_IMPL_ISUPPORTS(MacOSNotificationCenter, nsIAlertsService,
|
|
nsIAlertsDoNotDisturb)
|
|
|
|
nsresult MacOSNotificationCenter::Init() {
|
|
NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
|
|
|
|
return (!!NSClassFromString(@"NSUserNotification")) ? NS_OK
|
|
: NS_ERROR_FAILURE;
|
|
|
|
NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
MacOSNotificationCenter::ShowAlert(nsIAlertNotification* aAlert,
|
|
nsIObserver* aAlertListener) {
|
|
return NS_ERROR_NOT_IMPLEMENTED; // Implemented in nsAlertsService
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
MacOSNotificationCenter::ShowAlertWithCallbacks(
|
|
nsIAlertNotification* aAlert, nsIAlertCallbacks* aAlertCallbacks) {
|
|
NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
|
|
|
|
NS_ENSURE_ARG(aAlert);
|
|
|
|
if (mSuppressForScreenSharing) {
|
|
return NS_OK;
|
|
}
|
|
|
|
Class unClass = NSClassFromString(@"NSUserNotification");
|
|
NSUserNotification* notification = [[[unClass alloc] init] autorelease];
|
|
|
|
nsAutoString title;
|
|
nsresult rv = aAlert->GetTitle(title);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
notification.title = nsCocoaUtils::ToNSString(title);
|
|
|
|
nsAutoString hostPort;
|
|
rv = aAlert->GetSource(hostPort);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
nsCOMPtr<nsIStringBundle> bundle;
|
|
nsCOMPtr<nsIStringBundleService> sbs =
|
|
do_GetService(NS_STRINGBUNDLE_CONTRACTID);
|
|
sbs->CreateBundle("chrome://alerts/locale/alert.properties",
|
|
getter_AddRefs(bundle));
|
|
|
|
if (!hostPort.IsEmpty() && bundle) {
|
|
AutoTArray<nsString, 1> formatStrings = {hostPort};
|
|
nsAutoString notificationSource;
|
|
bundle->FormatStringFromName("source.label", formatStrings,
|
|
notificationSource);
|
|
notification.subtitle = nsCocoaUtils::ToNSString(notificationSource);
|
|
}
|
|
|
|
nsAutoString text;
|
|
rv = aAlert->GetText(text);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
notification.informativeText = nsCocoaUtils::ToNSString(text);
|
|
|
|
bool isSilent;
|
|
aAlert->GetSilent(&isSilent);
|
|
notification.soundName = isSilent ? nil : NSUserNotificationDefaultSoundName;
|
|
|
|
NSMutableArray* additionalActions = [[NSMutableArray alloc] init];
|
|
|
|
nsTArray<RefPtr<nsIAlertAction>> actions;
|
|
MOZ_TRY(aAlert->GetActions(actions));
|
|
|
|
for (const RefPtr<nsIAlertAction>& action : actions) {
|
|
nsAutoString actionName;
|
|
MOZ_TRY(action->GetAction(actionName));
|
|
|
|
nsAutoString actionTitle;
|
|
MOZ_TRY(action->GetTitle(actionTitle));
|
|
|
|
// Add suffix to prevent potential collision with keywords like "settings"
|
|
NSString* actionNameNS =
|
|
nsCocoaUtils::ToNSString(actionName + kActionSuffix);
|
|
NSString* actionTitleNS = nsCocoaUtils::ToNSString(actionTitle);
|
|
NSUserNotificationAction* notificationAction =
|
|
[NSUserNotificationAction actionWithIdentifier:actionNameNS
|
|
title:actionTitleNS];
|
|
[additionalActions addObject:notificationAction];
|
|
}
|
|
|
|
// If this is not an application/extension alert, show additional actions
|
|
// dealing with permissions.
|
|
bool isActionable;
|
|
if (bundle && NS_SUCCEEDED(aAlert->GetActionable(&isActionable)) &&
|
|
isActionable) {
|
|
nsAutoString disableButtonTitle;
|
|
if (!hostPort.IsEmpty()) {
|
|
AutoTArray<nsString, 1> formatStrings = {hostPort};
|
|
bundle->FormatStringFromName("webActions.disableForOrigin.label",
|
|
formatStrings, disableButtonTitle);
|
|
}
|
|
|
|
nsAutoString settingsButtonTitle;
|
|
bundle->GetStringFromName("webActions.settings.label", settingsButtonTitle);
|
|
|
|
NSString* actionNameNS = nsCocoaUtils::ToNSString(kAlertActionDisable);
|
|
NSString* actionTitleNS = nsCocoaUtils::ToNSString(disableButtonTitle);
|
|
NSUserNotificationAction* notificationAction =
|
|
[NSUserNotificationAction actionWithIdentifier:actionNameNS
|
|
title:actionTitleNS];
|
|
[additionalActions addObject:notificationAction];
|
|
|
|
actionNameNS = nsCocoaUtils::ToNSString(kAlertActionSettings);
|
|
actionTitleNS = nsCocoaUtils::ToNSString(settingsButtonTitle);
|
|
notificationAction =
|
|
[NSUserNotificationAction actionWithIdentifier:actionNameNS
|
|
title:actionTitleNS];
|
|
[additionalActions addObject:notificationAction];
|
|
}
|
|
|
|
notification.additionalActions = additionalActions;
|
|
notification.hasActionButton = additionalActions.count == 0;
|
|
[additionalActions release];
|
|
|
|
nsAutoString name;
|
|
rv = aAlert->GetName(name);
|
|
// Don't let an alert name be more than MAX_NOTIFICATION_NAME_LEN characters.
|
|
// More than that shouldn't be necessary and userInfo (assigned to below) has
|
|
// a length limit of 16k on MacOS 10.11. Exception thrown if limit exceeded.
|
|
if (name.Length() > MAX_NOTIFICATION_NAME_LEN) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
NSString* alertName = nsCocoaUtils::ToNSString(name);
|
|
if (!alertName) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
notification.userInfo = [NSDictionary
|
|
dictionaryWithObjects:[NSArray arrayWithObjects:alertName, nil]
|
|
forKeys:[NSArray arrayWithObjects:@"name", nil]];
|
|
|
|
nsAutoString cookie;
|
|
rv = aAlert->GetCookie(cookie);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
nsCOMPtr<imgIContainer> image;
|
|
MOZ_TRY(aAlert->GetImage(getter_AddRefs(image)));
|
|
if (image) {
|
|
NSImage* cocoaImage = nil;
|
|
// TODO: Pass pres context / ComputedStyle here to support context paint
|
|
// properties.
|
|
// TODO: Do we have a reasonable size to pass around here?
|
|
nsCocoaUtils::CreateDualRepresentationNSImageFromImageContainer(
|
|
image, imgIContainer::FRAME_FIRST, nullptr, NSMakeSize(0, 0),
|
|
&cocoaImage);
|
|
notification.contentImage = cocoaImage;
|
|
[cocoaImage release];
|
|
}
|
|
|
|
bool inPrivateBrowsing;
|
|
rv = aAlert->GetInPrivateBrowsing(&inPrivateBrowsing);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
MacOSNotificationInfo* macosni = new MacOSNotificationInfo(
|
|
alertName, aAlert, aAlertCallbacks, cookie, inPrivateBrowsing);
|
|
|
|
CloseAlertCocoaString(alertName);
|
|
mActiveAlerts.AppendElement(macosni);
|
|
[GetNotificationCenter() deliverNotification:notification];
|
|
if (aAlertCallbacks) {
|
|
aAlertCallbacks->OnAlertShow();
|
|
}
|
|
|
|
return NS_OK;
|
|
|
|
NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
MacOSNotificationCenter::CloseAlert(const nsAString& aAlertName,
|
|
bool aContextClosed) {
|
|
NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
|
|
|
|
NSString* alertName = nsCocoaUtils::ToNSString(aAlertName);
|
|
CloseAlertCocoaString(alertName);
|
|
return NS_OK;
|
|
|
|
NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
|
|
}
|
|
|
|
NS_IMETHODIMP MacOSNotificationCenter::Teardown() {
|
|
mActiveAlerts.Clear();
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP MacOSNotificationCenter::PbmTeardown() {
|
|
NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
|
|
|
|
NSArray* notifications = [GetNotificationCenter() deliveredNotifications];
|
|
for (int32_t i = mActiveAlerts.Length() - 1; i >= 0; i--) {
|
|
MacOSNotificationInfo* macosni = mActiveAlerts[i];
|
|
if (!macosni->mPrivateBrowsing) {
|
|
continue;
|
|
}
|
|
NSString* name = macosni->mName;
|
|
for (NSUserNotification* notification in notifications) {
|
|
NSString* notifName = [[notification userInfo] valueForKey:@"name"];
|
|
if ([notifName isEqualToString:name]) {
|
|
[GetNotificationCenter() removeDeliveredNotification:notification];
|
|
break;
|
|
}
|
|
}
|
|
if (macosni->mAlertCallbacks) {
|
|
macosni->mAlertCallbacks->OnAlertFinished();
|
|
}
|
|
mActiveAlerts.RemoveElementAt(i);
|
|
}
|
|
return NS_OK;
|
|
|
|
NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
|
|
}
|
|
|
|
void MacOSNotificationCenter::CloseAlertCocoaString(NSString* aAlertName) {
|
|
NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
|
|
|
|
if (!aAlertName) {
|
|
return; // Can't do anything without a name
|
|
}
|
|
|
|
NSArray* notifications = [GetNotificationCenter() deliveredNotifications];
|
|
for (NSUserNotification* notification in notifications) {
|
|
NSString* name = [[notification userInfo] valueForKey:@"name"];
|
|
if ([name isEqualToString:aAlertName]) {
|
|
[GetNotificationCenter() removeDeliveredNotification:notification];
|
|
break;
|
|
}
|
|
}
|
|
|
|
for (unsigned int i = 0; i < mActiveAlerts.Length(); i++) {
|
|
MacOSNotificationInfo* macosni = mActiveAlerts[i];
|
|
if ([aAlertName isEqualToString:macosni->mName]) {
|
|
if (macosni->mAlertCallbacks) {
|
|
macosni->mAlertCallbacks->OnAlertFinished();
|
|
}
|
|
mActiveAlerts.RemoveElementAt(i);
|
|
break;
|
|
}
|
|
}
|
|
|
|
NS_OBJC_END_TRY_IGNORE_BLOCK;
|
|
}
|
|
|
|
void MacOSNotificationCenter::OnActivate(
|
|
NSString* aAlertName, NSUserNotificationActivationType aActivationType,
|
|
NSUserNotificationAction* aAdditionalActivationAction) {
|
|
NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
|
|
|
|
if (!aAlertName) {
|
|
return; // Can't do anything without a name
|
|
}
|
|
|
|
for (unsigned int i = 0; i < mActiveAlerts.Length(); i++) {
|
|
MacOSNotificationInfo* macosni = mActiveAlerts[i];
|
|
if ([aAlertName isEqualToString:macosni->mName]) {
|
|
if (macosni->mAlertCallbacks) {
|
|
switch ((int)aActivationType) {
|
|
case NSUserNotificationActivationTypeAdditionalActionClicked: {
|
|
MOZ_ASSERT(aAdditionalActivationAction);
|
|
nsAutoString actionName;
|
|
nsCocoaUtils::GetStringForNSString(
|
|
aAdditionalActivationAction.identifier, actionName);
|
|
|
|
if (actionName == kAlertActionDisable) {
|
|
macosni->mAlertCallbacks->OnAlertDisable();
|
|
break;
|
|
}
|
|
if (actionName == kAlertActionSettings) {
|
|
macosni->mAlertCallbacks->OnAlertSettings();
|
|
break;
|
|
}
|
|
|
|
// Trim the suffix
|
|
actionName.Truncate(actionName.Length() - kActionSuffix.Length());
|
|
|
|
nsCOMPtr<nsIAlertAction> action;
|
|
macosni->mAlertNotification->GetAction(actionName,
|
|
getter_AddRefs(action));
|
|
macosni->mAlertCallbacks->OnAlertClick(action);
|
|
break;
|
|
}
|
|
case NSUserNotificationActivationTypeActionButtonClicked:
|
|
default:
|
|
macosni->mAlertCallbacks->OnAlertClick(nullptr);
|
|
break;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
NS_OBJC_END_TRY_IGNORE_BLOCK;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
MacOSNotificationCenter::GetHistory(nsTArray<nsString>& aResult) {
|
|
// NSUserNotificationCenter doesn't support this, blocked by the migration to
|
|
// UNUserNotificationCenter which has
|
|
// getDeliveredNotificationsWithCompletionHandler
|
|
// https://developer.apple.com/documentation/usernotifications/unusernotificationcenter/getdeliverednotifications(completionhandler:)?language=objc
|
|
// See bug 1971395.
|
|
return NS_ERROR_NOT_IMPLEMENTED;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
MacOSNotificationCenter::IsFullscreen(bool* aRetVal) {
|
|
*aRetVal = false;
|
|
return NS_ERROR_NOT_IMPLEMENTED;
|
|
}
|
|
|
|
// nsIAlertsDoNotDisturb
|
|
NS_IMETHODIMP
|
|
MacOSNotificationCenter::GetManualDoNotDisturb(bool* aRetVal) {
|
|
return NS_ERROR_NOT_IMPLEMENTED;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
MacOSNotificationCenter::SetManualDoNotDisturb(bool aDoNotDisturb) {
|
|
return NS_ERROR_NOT_IMPLEMENTED;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
MacOSNotificationCenter::GetSuppressForScreenSharing(bool* aRetVal) {
|
|
NS_OBJC_BEGIN_TRY_BLOCK_RETURN
|
|
|
|
NS_ENSURE_ARG(aRetVal);
|
|
*aRetVal = mSuppressForScreenSharing;
|
|
return NS_OK;
|
|
|
|
NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE)
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
MacOSNotificationCenter::SetSuppressForScreenSharing(bool aSuppress) {
|
|
NS_OBJC_BEGIN_TRY_BLOCK_RETURN
|
|
|
|
mSuppressForScreenSharing = aSuppress;
|
|
return NS_OK;
|
|
|
|
NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE)
|
|
}
|
|
|
|
} // namespace mozilla
|