1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 #include "OSXNotificationCenter.h"
7 #import <AppKit/AppKit.h>
8 #include "imgIRequest.h"
9 #include "imgIContainer.h"
10 #include "nsICancelable.h"
11 #include "nsIStringBundle.h"
12 #include "nsNetUtil.h"
13 #import "nsCocoaUtils.h"
14 #include "nsComponentManagerUtils.h"
15 #include "nsContentUtils.h"
16 #include "nsObjCExceptions.h"
19 #include "nsIObserver.h"
21 using namespace mozilla;
23 #define MAX_NOTIFICATION_NAME_LEN 5000
25 @protocol FakeNSUserNotification <NSObject>
26 @property(copy) NSString* title;
27 @property(copy) NSString* subtitle;
28 @property(copy) NSString* informativeText;
29 @property(copy) NSString* actionButtonTitle;
30 @property(copy) NSDictionary* userInfo;
31 @property(copy) NSDate* deliveryDate;
32 @property(copy) NSTimeZone* deliveryTimeZone;
33 @property(copy) NSDateComponents* deliveryRepeatInterval;
34 @property(readonly) NSDate* actualDeliveryDate;
35 @property(readonly, getter=isPresented) BOOL presented;
36 @property(readonly, getter=isRemote) BOOL remote;
37 @property(copy) NSString* soundName;
38 @property BOOL hasActionButton;
39 @property(readonly) NSUserNotificationActivationType activationType;
40 @property(copy) NSString* otherButtonTitle;
41 @property(copy) NSImage* contentImage;
44 @protocol FakeNSUserNotificationCenter <NSObject>
45 + (id<FakeNSUserNotificationCenter>)defaultUserNotificationCenter;
46 @property(assign) id<NSUserNotificationCenterDelegate> delegate;
47 @property(copy) NSArray* scheduledNotifications;
48 - (void)scheduleNotification:(id<FakeNSUserNotification>)notification;
49 - (void)removeScheduledNotification:(id<FakeNSUserNotification>)notification;
50 @property(readonly) NSArray* deliveredNotifications;
51 - (void)deliverNotification:(id<FakeNSUserNotification>)notification;
52 - (void)removeDeliveredNotification:(id<FakeNSUserNotification>)notification;
53 - (void)removeAllDeliveredNotifications;
54 - (void)_removeAllDisplayedNotifications;
55 - (void)_removeDisplayedNotification:(id<FakeNSUserNotification>)notification;
58 @interface mozNotificationCenterDelegate : NSObject <NSUserNotificationCenterDelegate> {
59 OSXNotificationCenter* mOSXNC;
61 - (id)initWithOSXNC:(OSXNotificationCenter*)osxnc;
64 @implementation mozNotificationCenterDelegate
66 - (id)initWithOSXNC:(OSXNotificationCenter*)osxnc {
68 // We should *never* outlive this OSXNotificationCenter.
73 - (void)userNotificationCenter:(id<FakeNSUserNotificationCenter>)center
74 didDeliverNotification:(id<FakeNSUserNotification>)notification {
77 - (void)userNotificationCenter:(id<FakeNSUserNotificationCenter>)center
78 didActivateNotification:(id<FakeNSUserNotification>)notification {
79 unsigned long long additionalActionIndex = ULLONG_MAX;
80 if ([notification respondsToSelector:@selector(_alternateActionIndex)]) {
81 NSNumber* alternateActionIndex = [(NSObject*)notification valueForKey:@"_alternateActionIndex"];
82 additionalActionIndex = [alternateActionIndex unsignedLongLongValue];
84 mOSXNC->OnActivate([[notification userInfo] valueForKey:@"name"], notification.activationType,
85 additionalActionIndex);
88 - (BOOL)userNotificationCenter:(id<FakeNSUserNotificationCenter>)center
89 shouldPresentNotification:(id<FakeNSUserNotification>)notification {
93 // This is an undocumented method that we need for parity with Safari.
94 // Apple bug #15440664.
95 - (void)userNotificationCenter:(id<FakeNSUserNotificationCenter>)center
96 didRemoveDeliveredNotifications:(NSArray*)notifications {
97 for (id<FakeNSUserNotification> notification in notifications) {
98 NSString* name = [[notification userInfo] valueForKey:@"name"];
99 mOSXNC->CloseAlertCocoaString(name);
103 // This is an undocumented method that we need to be notified if a user clicks the close button.
104 - (void)userNotificationCenter:(id<FakeNSUserNotificationCenter>)center
105 didDismissAlert:(id<FakeNSUserNotification>)notification {
106 NSString* name = [[notification userInfo] valueForKey:@"name"];
107 mOSXNC->CloseAlertCocoaString(name);
115 OSXNotificationActionDisable = 0,
116 OSXNotificationActionSettings = 1,
119 class OSXNotificationInfo final : public nsISupports {
121 virtual ~OSXNotificationInfo();
125 OSXNotificationInfo(NSString* name, nsIObserver* observer, const nsAString& alertCookie);
128 nsCOMPtr<nsIObserver> mObserver;
130 RefPtr<nsICancelable> mIconRequest;
131 id<FakeNSUserNotification> mPendingNotification;
134 NS_IMPL_ISUPPORTS0(OSXNotificationInfo)
136 OSXNotificationInfo::OSXNotificationInfo(NSString* name, nsIObserver* observer,
137 const nsAString& alertCookie) {
138 NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
140 NS_ASSERTION(name, "Cannot create OSXNotificationInfo without a name!");
141 mName = [name retain];
142 mObserver = observer;
143 mCookie = alertCookie;
144 mPendingNotification = nil;
146 NS_OBJC_END_TRY_IGNORE_BLOCK;
149 OSXNotificationInfo::~OSXNotificationInfo() {
150 NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
153 [mPendingNotification release];
155 NS_OBJC_END_TRY_IGNORE_BLOCK;
158 static id<FakeNSUserNotificationCenter> GetNotificationCenter() {
159 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
161 Class c = NSClassFromString(@"NSUserNotificationCenter");
162 return [c performSelector:@selector(defaultUserNotificationCenter)];
164 NS_OBJC_END_TRY_BLOCK_RETURN(nil);
167 OSXNotificationCenter::OSXNotificationCenter() {
168 NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
170 mDelegate = [[mozNotificationCenterDelegate alloc] initWithOSXNC:this];
171 GetNotificationCenter().delegate = mDelegate;
172 mSuppressForScreenSharing = false;
174 NS_OBJC_END_TRY_IGNORE_BLOCK;
177 OSXNotificationCenter::~OSXNotificationCenter() {
178 NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
180 [GetNotificationCenter() removeAllDeliveredNotifications];
183 NS_OBJC_END_TRY_IGNORE_BLOCK;
186 NS_IMPL_ISUPPORTS(OSXNotificationCenter, nsIAlertsService, nsIAlertsIconData, nsIAlertsDoNotDisturb,
187 nsIAlertNotificationImageListener)
189 nsresult OSXNotificationCenter::Init() {
190 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
192 return (!!NSClassFromString(@"NSUserNotification")) ? NS_OK : NS_ERROR_FAILURE;
194 NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
198 OSXNotificationCenter::ShowAlertNotification(
199 const nsAString& aImageUrl, const nsAString& aAlertTitle, const nsAString& aAlertText,
200 bool aAlertTextClickable, const nsAString& aAlertCookie, nsIObserver* aAlertListener,
201 const nsAString& aAlertName, const nsAString& aBidi, const nsAString& aLang,
202 const nsAString& aData, nsIPrincipal* aPrincipal, bool aInPrivateBrowsing,
203 bool aRequireInteraction) {
204 nsCOMPtr<nsIAlertNotification> alert = do_CreateInstance(ALERT_NOTIFICATION_CONTRACTID);
205 NS_ENSURE_TRUE(alert, NS_ERROR_FAILURE);
206 // vibrate is unused for now
207 nsTArray<uint32_t> vibrate;
208 nsresult rv = alert->Init(aAlertName, aImageUrl, aAlertTitle, aAlertText, aAlertTextClickable,
209 aAlertCookie, aBidi, aLang, aData, aPrincipal, aInPrivateBrowsing,
210 aRequireInteraction, false, vibrate);
211 NS_ENSURE_SUCCESS(rv, rv);
212 return ShowAlert(alert, aAlertListener);
216 OSXNotificationCenter::ShowPersistentNotification(const nsAString& aPersistentData,
217 nsIAlertNotification* aAlert,
218 nsIObserver* aAlertListener) {
219 return ShowAlert(aAlert, aAlertListener);
223 OSXNotificationCenter::ShowAlert(nsIAlertNotification* aAlert, nsIObserver* aAlertListener) {
224 return ShowAlertWithIconData(aAlert, aAlertListener, 0, nullptr);
228 OSXNotificationCenter::ShowAlertWithIconData(nsIAlertNotification* aAlert,
229 nsIObserver* aAlertListener, uint32_t aIconSize,
230 const uint8_t* aIconData) {
231 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
233 NS_ENSURE_ARG(aAlert);
235 if (mSuppressForScreenSharing) {
239 Class unClass = NSClassFromString(@"NSUserNotification");
240 id<FakeNSUserNotification> notification = [[unClass alloc] init];
243 nsresult rv = aAlert->GetTitle(title);
244 NS_ENSURE_SUCCESS(rv, rv);
245 notification.title = nsCocoaUtils::ToNSString(title);
247 nsAutoString hostPort;
248 rv = aAlert->GetSource(hostPort);
249 NS_ENSURE_SUCCESS(rv, rv);
250 nsCOMPtr<nsIStringBundle> bundle;
251 nsCOMPtr<nsIStringBundleService> sbs = do_GetService(NS_STRINGBUNDLE_CONTRACTID);
252 sbs->CreateBundle("chrome://alerts/locale/alert.properties", getter_AddRefs(bundle));
254 if (!hostPort.IsEmpty() && bundle) {
255 AutoTArray<nsString, 1> formatStrings = {hostPort};
256 nsAutoString notificationSource;
257 bundle->FormatStringFromName("source.label", formatStrings, notificationSource);
258 notification.subtitle = nsCocoaUtils::ToNSString(notificationSource);
262 rv = aAlert->GetText(text);
263 NS_ENSURE_SUCCESS(rv, rv);
264 notification.informativeText = nsCocoaUtils::ToNSString(text);
266 notification.soundName = NSUserNotificationDefaultSoundName;
267 notification.hasActionButton = NO;
269 // If this is not an application/extension alert, show additional actions dealing with
272 if (bundle && NS_SUCCEEDED(aAlert->GetActionable(&isActionable)) && isActionable) {
273 nsAutoString closeButtonTitle, actionButtonTitle, disableButtonTitle, settingsButtonTitle;
274 bundle->GetStringFromName("closeButton.title", closeButtonTitle);
275 bundle->GetStringFromName("actionButton.label", actionButtonTitle);
276 if (!hostPort.IsEmpty()) {
277 AutoTArray<nsString, 1> formatStrings = {hostPort};
278 bundle->FormatStringFromName("webActions.disableForOrigin.label", formatStrings,
281 bundle->GetStringFromName("webActions.settings.label", settingsButtonTitle);
283 notification.otherButtonTitle = nsCocoaUtils::ToNSString(closeButtonTitle);
285 // OS X 10.8 only shows action buttons if the "Alerts" style is set in
286 // Notification Center preferences, and doesn't support the alternate
288 if ([notification respondsToSelector:@selector(set_showsButtons:)] &&
289 [notification respondsToSelector:@selector(set_alwaysShowAlternateActionMenu:)] &&
290 [notification respondsToSelector:@selector(set_alternateActionButtonTitles:)]) {
291 notification.hasActionButton = YES;
292 notification.actionButtonTitle = nsCocoaUtils::ToNSString(actionButtonTitle);
294 [(NSObject*)notification setValue:@(YES) forKey:@"_showsButtons"];
295 [(NSObject*)notification setValue:@(YES) forKey:@"_alwaysShowAlternateActionMenu"];
296 [(NSObject*)notification setValue:@[
297 nsCocoaUtils::ToNSString(disableButtonTitle), nsCocoaUtils::ToNSString(settingsButtonTitle)
299 forKey:@"_alternateActionButtonTitles"];
303 rv = aAlert->GetName(name);
304 // Don't let an alert name be more than MAX_NOTIFICATION_NAME_LEN characters.
305 // More than that shouldn't be necessary and userInfo (assigned to below) has
306 // a length limit of 16k on OS X 10.11. Exception thrown if limit exceeded.
307 if (name.Length() > MAX_NOTIFICATION_NAME_LEN) {
308 return NS_ERROR_FAILURE;
311 NS_ENSURE_SUCCESS(rv, rv);
312 NSString* alertName = nsCocoaUtils::ToNSString(name);
314 return NS_ERROR_FAILURE;
316 notification.userInfo =
317 [NSDictionary dictionaryWithObjects:[NSArray arrayWithObjects:alertName, nil]
318 forKeys:[NSArray arrayWithObjects:@"name", nil]];
321 rv = aAlert->GetCookie(cookie);
322 NS_ENSURE_SUCCESS(rv, rv);
324 OSXNotificationInfo* osxni = new OSXNotificationInfo(alertName, aAlertListener, cookie);
326 // Show the favicon if supported on this version of OS X.
327 if (aIconSize > 0 && [notification respondsToSelector:@selector(set_identityImage:)] &&
328 [notification respondsToSelector:@selector(set_identityImageHasBorder:)]) {
329 NSData* iconData = [NSData dataWithBytes:aIconData length:aIconSize];
330 NSImage* icon = [[[NSImage alloc] initWithData:iconData] autorelease];
332 [(NSObject*)notification setValue:icon forKey:@"_identityImage"];
333 [(NSObject*)notification setValue:@(NO) forKey:@"_identityImageHasBorder"];
336 bool inPrivateBrowsing;
337 rv = aAlert->GetInPrivateBrowsing(&inPrivateBrowsing);
338 NS_ENSURE_SUCCESS(rv, rv);
340 // Show the notification without waiting for an image if there is no icon URL or
341 // notification icons are not supported on this version of OS X.
342 if (![unClass instancesRespondToSelector:@selector(setContentImage:)]) {
343 CloseAlertCocoaString(alertName);
344 mActiveAlerts.AppendElement(osxni);
345 [GetNotificationCenter() deliverNotification:notification];
346 [notification release];
347 if (aAlertListener) {
348 aAlertListener->Observe(nullptr, "alertshow", cookie.get());
351 mPendingAlerts.AppendElement(osxni);
352 osxni->mPendingNotification = notification;
353 // Wait six seconds for the image to load.
354 rv = aAlert->LoadImage(6000, this, osxni, getter_AddRefs(osxni->mIconRequest));
355 if (NS_WARN_IF(NS_FAILED(rv))) {
356 ShowPendingNotification(osxni);
362 NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
366 OSXNotificationCenter::CloseAlert(const nsAString& aAlertName) {
367 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
369 NSString* alertName = nsCocoaUtils::ToNSString(aAlertName);
370 CloseAlertCocoaString(alertName);
373 NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
376 void OSXNotificationCenter::CloseAlertCocoaString(NSString* aAlertName) {
377 NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
380 return; // Can't do anything without a name
383 NSArray* notifications = [GetNotificationCenter() deliveredNotifications];
384 for (id<FakeNSUserNotification> notification in notifications) {
385 NSString* name = [[notification userInfo] valueForKey:@"name"];
386 if ([name isEqualToString:aAlertName]) {
387 [GetNotificationCenter() removeDeliveredNotification:notification];
388 [GetNotificationCenter() _removeDisplayedNotification:notification];
393 for (unsigned int i = 0; i < mActiveAlerts.Length(); i++) {
394 OSXNotificationInfo* osxni = mActiveAlerts[i];
395 if ([aAlertName isEqualToString:osxni->mName]) {
396 if (osxni->mObserver) {
397 osxni->mObserver->Observe(nullptr, "alertfinished", osxni->mCookie.get());
399 if (osxni->mIconRequest) {
400 osxni->mIconRequest->Cancel(NS_BINDING_ABORTED);
401 osxni->mIconRequest = nullptr;
403 mActiveAlerts.RemoveElementAt(i);
408 NS_OBJC_END_TRY_IGNORE_BLOCK;
411 void OSXNotificationCenter::OnActivate(NSString* aAlertName,
412 NSUserNotificationActivationType aActivationType,
413 unsigned long long aAdditionalActionIndex) {
414 NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
417 return; // Can't do anything without a name
420 for (unsigned int i = 0; i < mActiveAlerts.Length(); i++) {
421 OSXNotificationInfo* osxni = mActiveAlerts[i];
422 if ([aAlertName isEqualToString:osxni->mName]) {
423 if (osxni->mObserver) {
424 switch ((int)aActivationType) {
425 case NSUserNotificationActivationTypeAdditionalActionClicked:
426 case NSUserNotificationActivationTypeActionButtonClicked:
427 switch (aAdditionalActionIndex) {
428 case OSXNotificationActionDisable:
429 osxni->mObserver->Observe(nullptr, "alertdisablecallback", osxni->mCookie.get());
431 case OSXNotificationActionSettings:
432 osxni->mObserver->Observe(nullptr, "alertsettingscallback", osxni->mCookie.get());
435 NS_WARNING("Unknown NSUserNotification additional action clicked");
440 osxni->mObserver->Observe(nullptr, "alertclickcallback", osxni->mCookie.get());
448 NS_OBJC_END_TRY_IGNORE_BLOCK;
451 void OSXNotificationCenter::ShowPendingNotification(OSXNotificationInfo* osxni) {
452 NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
454 if (osxni->mIconRequest) {
455 osxni->mIconRequest->Cancel(NS_BINDING_ABORTED);
456 osxni->mIconRequest = nullptr;
459 CloseAlertCocoaString(osxni->mName);
461 for (unsigned int i = 0; i < mPendingAlerts.Length(); i++) {
462 if (mPendingAlerts[i] == osxni) {
463 mActiveAlerts.AppendElement(osxni);
464 mPendingAlerts.RemoveElementAt(i);
469 [GetNotificationCenter() deliverNotification:osxni->mPendingNotification];
471 if (osxni->mObserver) {
472 osxni->mObserver->Observe(nullptr, "alertshow", osxni->mCookie.get());
475 [osxni->mPendingNotification release];
476 osxni->mPendingNotification = nil;
478 NS_OBJC_END_TRY_IGNORE_BLOCK;
482 OSXNotificationCenter::OnImageMissing(nsISupports* aUserData) {
483 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
485 OSXNotificationInfo* osxni = static_cast<OSXNotificationInfo*>(aUserData);
486 if (osxni->mPendingNotification) {
487 // If there was an error getting the image, or the request timed out, show
488 // the notification without a content image.
489 ShowPendingNotification(osxni);
493 NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
497 OSXNotificationCenter::OnImageReady(nsISupports* aUserData, imgIRequest* aRequest) {
498 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
500 nsCOMPtr<imgIContainer> image;
501 nsresult rv = aRequest->GetImage(getter_AddRefs(image));
502 if (NS_WARN_IF(NS_FAILED(rv) || !image)) {
506 OSXNotificationInfo* osxni = static_cast<OSXNotificationInfo*>(aUserData);
507 if (!osxni->mPendingNotification) {
508 return NS_ERROR_FAILURE;
511 NSImage* cocoaImage = nil;
512 // TODO: Pass ComputedStyle here to support context paint properties
513 nsCocoaUtils::CreateDualRepresentationNSImageFromImageContainer(image, imgIContainer::FRAME_FIRST,
514 nullptr, &cocoaImage);
515 (osxni->mPendingNotification).contentImage = cocoaImage;
516 [cocoaImage release];
517 ShowPendingNotification(osxni);
521 NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
524 // nsIAlertsDoNotDisturb
526 OSXNotificationCenter::GetManualDoNotDisturb(bool* aRetVal) { return NS_ERROR_NOT_IMPLEMENTED; }
529 OSXNotificationCenter::SetManualDoNotDisturb(bool aDoNotDisturb) {
530 return NS_ERROR_NOT_IMPLEMENTED;
534 OSXNotificationCenter::GetSuppressForScreenSharing(bool* aRetVal) {
535 NS_OBJC_BEGIN_TRY_BLOCK_RETURN
537 NS_ENSURE_ARG(aRetVal);
538 *aRetVal = mSuppressForScreenSharing;
541 NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE)
545 OSXNotificationCenter::SetSuppressForScreenSharing(bool aSuppress) {
546 NS_OBJC_BEGIN_TRY_BLOCK_RETURN
548 mSuppressForScreenSharing = aSuppress;
551 NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE)
554 } // namespace mozilla