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 @interface mozNotificationCenterDelegate
26 : NSObject <NSUserNotificationCenterDelegate> {
27 OSXNotificationCenter* mOSXNC;
29 - (id)initWithOSXNC:(OSXNotificationCenter*)osxnc;
32 @implementation mozNotificationCenterDelegate
34 - (id)initWithOSXNC:(OSXNotificationCenter*)osxnc {
36 // We should *never* outlive this OSXNotificationCenter.
41 - (void)userNotificationCenter:(NSUserNotificationCenter*)center
42 didDeliverNotification:(NSUserNotification*)notification {
45 - (void)userNotificationCenter:(NSUserNotificationCenter*)center
46 didActivateNotification:(NSUserNotification*)notification {
47 unsigned long long additionalActionIndex = ULLONG_MAX;
48 if ([notification respondsToSelector:@selector(_alternateActionIndex)]) {
49 NSNumber* alternateActionIndex =
50 [(NSObject*)notification valueForKey:@"_alternateActionIndex"];
51 additionalActionIndex = [alternateActionIndex unsignedLongLongValue];
53 mOSXNC->OnActivate([[notification userInfo] valueForKey:@"name"],
54 notification.activationType, additionalActionIndex);
57 - (BOOL)userNotificationCenter:(NSUserNotificationCenter*)center
58 shouldPresentNotification:(NSUserNotification*)notification {
62 // This is an undocumented method that we need for parity with Safari.
63 // Apple bug #15440664.
64 - (void)userNotificationCenter:(NSUserNotificationCenter*)center
65 didRemoveDeliveredNotifications:(NSArray*)notifications {
66 for (NSUserNotification* notification in notifications) {
67 NSString* name = [[notification userInfo] valueForKey:@"name"];
68 mOSXNC->CloseAlertCocoaString(name);
72 // This is an undocumented method that we need to be notified if a user clicks
74 - (void)userNotificationCenter:(NSUserNotificationCenter*)center
75 didDismissAlert:(NSUserNotification*)notification {
76 NSString* name = [[notification userInfo] valueForKey:@"name"];
77 mOSXNC->CloseAlertCocoaString(name);
85 OSXNotificationActionDisable = 0,
86 OSXNotificationActionSettings = 1,
89 class OSXNotificationInfo final : public nsISupports {
91 virtual ~OSXNotificationInfo();
95 OSXNotificationInfo(NSString* name, nsIObserver* observer,
96 const nsAString& alertCookie);
99 nsCOMPtr<nsIObserver> mObserver;
101 RefPtr<nsICancelable> mIconRequest;
102 NSUserNotification* mPendingNotification;
105 NS_IMPL_ISUPPORTS0(OSXNotificationInfo)
107 OSXNotificationInfo::OSXNotificationInfo(NSString* name, nsIObserver* observer,
108 const nsAString& alertCookie) {
109 NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
111 NS_ASSERTION(name, "Cannot create OSXNotificationInfo without a name!");
112 mName = [name retain];
113 mObserver = observer;
114 mCookie = alertCookie;
115 mPendingNotification = nil;
117 NS_OBJC_END_TRY_IGNORE_BLOCK;
120 OSXNotificationInfo::~OSXNotificationInfo() {
121 NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
124 [mPendingNotification release];
126 NS_OBJC_END_TRY_IGNORE_BLOCK;
129 static NSUserNotificationCenter* GetNotificationCenter() {
130 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
132 Class c = NSClassFromString(@"NSUserNotificationCenter");
133 return [c performSelector:@selector(defaultUserNotificationCenter)];
135 NS_OBJC_END_TRY_BLOCK_RETURN(nil);
138 OSXNotificationCenter::OSXNotificationCenter() {
139 NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
141 mDelegate = [[mozNotificationCenterDelegate alloc] initWithOSXNC:this];
142 GetNotificationCenter().delegate = mDelegate;
143 mSuppressForScreenSharing = false;
145 NS_OBJC_END_TRY_IGNORE_BLOCK;
148 OSXNotificationCenter::~OSXNotificationCenter() {
149 NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
151 [GetNotificationCenter() removeAllDeliveredNotifications];
154 NS_OBJC_END_TRY_IGNORE_BLOCK;
157 NS_IMPL_ISUPPORTS(OSXNotificationCenter, nsIAlertsService, nsIAlertsIconData,
158 nsIAlertsDoNotDisturb, nsIAlertNotificationImageListener)
160 nsresult OSXNotificationCenter::Init() {
161 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
163 return (!!NSClassFromString(@"NSUserNotification")) ? NS_OK
166 NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
170 OSXNotificationCenter::ShowAlertNotification(
171 const nsAString& aImageUrl, const nsAString& aAlertTitle,
172 const nsAString& aAlertText, bool aAlertTextClickable,
173 const nsAString& aAlertCookie, nsIObserver* aAlertListener,
174 const nsAString& aAlertName, const nsAString& aBidi, const nsAString& aLang,
175 const nsAString& aData, nsIPrincipal* aPrincipal, bool aInPrivateBrowsing,
176 bool aRequireInteraction) {
177 nsCOMPtr<nsIAlertNotification> alert =
178 do_CreateInstance(ALERT_NOTIFICATION_CONTRACTID);
179 NS_ENSURE_TRUE(alert, NS_ERROR_FAILURE);
180 // vibrate is unused for now
181 nsTArray<uint32_t> vibrate;
182 nsresult rv = alert->Init(aAlertName, aImageUrl, aAlertTitle, aAlertText,
183 aAlertTextClickable, aAlertCookie, aBidi, aLang,
184 aData, aPrincipal, aInPrivateBrowsing,
185 aRequireInteraction, false, vibrate);
186 NS_ENSURE_SUCCESS(rv, rv);
187 return ShowAlert(alert, aAlertListener);
191 OSXNotificationCenter::ShowAlert(nsIAlertNotification* aAlert,
192 nsIObserver* aAlertListener) {
193 return ShowAlertWithIconData(aAlert, aAlertListener, 0, nullptr);
197 OSXNotificationCenter::ShowAlertWithIconData(nsIAlertNotification* aAlert,
198 nsIObserver* aAlertListener,
200 const uint8_t* aIconData) {
201 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
203 NS_ENSURE_ARG(aAlert);
205 if (mSuppressForScreenSharing) {
209 Class unClass = NSClassFromString(@"NSUserNotification");
210 NSUserNotification* notification = [[unClass alloc] init];
213 nsresult rv = aAlert->GetTitle(title);
214 NS_ENSURE_SUCCESS(rv, rv);
215 notification.title = nsCocoaUtils::ToNSString(title);
217 nsAutoString hostPort;
218 rv = aAlert->GetSource(hostPort);
219 NS_ENSURE_SUCCESS(rv, rv);
220 nsCOMPtr<nsIStringBundle> bundle;
221 nsCOMPtr<nsIStringBundleService> sbs =
222 do_GetService(NS_STRINGBUNDLE_CONTRACTID);
223 sbs->CreateBundle("chrome://alerts/locale/alert.properties",
224 getter_AddRefs(bundle));
226 if (!hostPort.IsEmpty() && bundle) {
227 AutoTArray<nsString, 1> formatStrings = {hostPort};
228 nsAutoString notificationSource;
229 bundle->FormatStringFromName("source.label", formatStrings,
231 notification.subtitle = nsCocoaUtils::ToNSString(notificationSource);
235 rv = aAlert->GetText(text);
236 NS_ENSURE_SUCCESS(rv, rv);
237 notification.informativeText = nsCocoaUtils::ToNSString(text);
240 aAlert->GetSilent(&isSilent);
241 notification.soundName = isSilent ? nil : NSUserNotificationDefaultSoundName;
242 notification.hasActionButton = NO;
244 // If this is not an application/extension alert, show additional actions
245 // dealing with permissions.
247 if (bundle && NS_SUCCEEDED(aAlert->GetActionable(&isActionable)) &&
249 nsAutoString closeButtonTitle, actionButtonTitle, disableButtonTitle,
251 bundle->GetStringFromName("closeButton.title", closeButtonTitle);
252 bundle->GetStringFromName("actionButton.label", actionButtonTitle);
253 if (!hostPort.IsEmpty()) {
254 AutoTArray<nsString, 1> formatStrings = {hostPort};
255 bundle->FormatStringFromName("webActions.disableForOrigin.label",
256 formatStrings, disableButtonTitle);
258 bundle->GetStringFromName("webActions.settings.label", settingsButtonTitle);
260 notification.otherButtonTitle = nsCocoaUtils::ToNSString(closeButtonTitle);
262 // OS X 10.8 only shows action buttons if the "Alerts" style is set in
263 // Notification Center preferences, and doesn't support the alternate
265 if ([notification respondsToSelector:@selector(set_showsButtons:)] &&
267 respondsToSelector:@selector(set_alwaysShowAlternateActionMenu:)] &&
269 respondsToSelector:@selector(set_alternateActionButtonTitles:)]) {
270 notification.hasActionButton = YES;
271 notification.actionButtonTitle =
272 nsCocoaUtils::ToNSString(actionButtonTitle);
274 [(NSObject*)notification setValue:@(YES) forKey:@"_showsButtons"];
275 [(NSObject*)notification setValue:@(YES)
276 forKey:@"_alwaysShowAlternateActionMenu"];
277 [(NSObject*)notification setValue:@[
278 nsCocoaUtils::ToNSString(disableButtonTitle),
279 nsCocoaUtils::ToNSString(settingsButtonTitle)
281 forKey:@"_alternateActionButtonTitles"];
285 rv = aAlert->GetName(name);
286 // Don't let an alert name be more than MAX_NOTIFICATION_NAME_LEN characters.
287 // More than that shouldn't be necessary and userInfo (assigned to below) has
288 // a length limit of 16k on OS X 10.11. Exception thrown if limit exceeded.
289 if (name.Length() > MAX_NOTIFICATION_NAME_LEN) {
290 return NS_ERROR_FAILURE;
293 NS_ENSURE_SUCCESS(rv, rv);
294 NSString* alertName = nsCocoaUtils::ToNSString(name);
296 return NS_ERROR_FAILURE;
298 notification.userInfo = [NSDictionary
299 dictionaryWithObjects:[NSArray arrayWithObjects:alertName, nil]
300 forKeys:[NSArray arrayWithObjects:@"name", nil]];
303 rv = aAlert->GetCookie(cookie);
304 NS_ENSURE_SUCCESS(rv, rv);
306 OSXNotificationInfo* osxni =
307 new OSXNotificationInfo(alertName, aAlertListener, cookie);
309 // Show the favicon if supported on this version of OS X.
311 [notification respondsToSelector:@selector(set_identityImage:)] &&
313 respondsToSelector:@selector(set_identityImageHasBorder:)]) {
314 NSData* iconData = [NSData dataWithBytes:aIconData length:aIconSize];
315 NSImage* icon = [[[NSImage alloc] initWithData:iconData] autorelease];
317 [(NSObject*)notification setValue:icon forKey:@"_identityImage"];
318 [(NSObject*)notification setValue:@(NO) forKey:@"_identityImageHasBorder"];
321 bool inPrivateBrowsing;
322 rv = aAlert->GetInPrivateBrowsing(&inPrivateBrowsing);
323 NS_ENSURE_SUCCESS(rv, rv);
325 // Show the notification without waiting for an image if there is no icon URL
326 // or notification icons are not supported on this version of OS X.
327 if (![unClass instancesRespondToSelector:@selector(setContentImage:)]) {
328 CloseAlertCocoaString(alertName);
329 mActiveAlerts.AppendElement(osxni);
330 [GetNotificationCenter() deliverNotification:notification];
331 [notification release];
332 if (aAlertListener) {
333 aAlertListener->Observe(nullptr, "alertshow", cookie.get());
336 mPendingAlerts.AppendElement(osxni);
337 osxni->mPendingNotification = notification;
338 // Wait six seconds for the image to load.
339 rv = aAlert->LoadImage(6000, this, osxni,
340 getter_AddRefs(osxni->mIconRequest));
341 if (NS_WARN_IF(NS_FAILED(rv))) {
342 ShowPendingNotification(osxni);
348 NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
352 OSXNotificationCenter::CloseAlert(const nsAString& aAlertName,
353 bool aContextClosed) {
354 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
356 NSString* alertName = nsCocoaUtils::ToNSString(aAlertName);
357 CloseAlertCocoaString(alertName);
360 NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
363 void OSXNotificationCenter::CloseAlertCocoaString(NSString* aAlertName) {
364 NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
367 return; // Can't do anything without a name
370 NSArray* notifications = [GetNotificationCenter() deliveredNotifications];
371 for (NSUserNotification* notification in notifications) {
372 NSString* name = [[notification userInfo] valueForKey:@"name"];
373 if ([name isEqualToString:aAlertName]) {
374 [GetNotificationCenter() removeDeliveredNotification:notification];
379 for (unsigned int i = 0; i < mActiveAlerts.Length(); i++) {
380 OSXNotificationInfo* osxni = mActiveAlerts[i];
381 if ([aAlertName isEqualToString:osxni->mName]) {
382 if (osxni->mObserver) {
383 osxni->mObserver->Observe(nullptr, "alertfinished",
384 osxni->mCookie.get());
386 if (osxni->mIconRequest) {
387 osxni->mIconRequest->Cancel(NS_BINDING_ABORTED);
388 osxni->mIconRequest = nullptr;
390 mActiveAlerts.RemoveElementAt(i);
395 NS_OBJC_END_TRY_IGNORE_BLOCK;
398 void OSXNotificationCenter::OnActivate(
399 NSString* aAlertName, NSUserNotificationActivationType aActivationType,
400 unsigned long long aAdditionalActionIndex) {
401 NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
404 return; // Can't do anything without a name
407 for (unsigned int i = 0; i < mActiveAlerts.Length(); i++) {
408 OSXNotificationInfo* osxni = mActiveAlerts[i];
409 if ([aAlertName isEqualToString:osxni->mName]) {
410 if (osxni->mObserver) {
411 switch ((int)aActivationType) {
412 case NSUserNotificationActivationTypeAdditionalActionClicked:
413 case NSUserNotificationActivationTypeActionButtonClicked:
414 switch (aAdditionalActionIndex) {
415 case OSXNotificationActionDisable:
416 osxni->mObserver->Observe(nullptr, "alertdisablecallback",
417 osxni->mCookie.get());
419 case OSXNotificationActionSettings:
420 osxni->mObserver->Observe(nullptr, "alertsettingscallback",
421 osxni->mCookie.get());
425 "Unknown NSUserNotification additional action clicked");
430 osxni->mObserver->Observe(nullptr, "alertclickcallback",
431 osxni->mCookie.get());
439 NS_OBJC_END_TRY_IGNORE_BLOCK;
442 void OSXNotificationCenter::ShowPendingNotification(
443 OSXNotificationInfo* osxni) {
444 NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
446 if (osxni->mIconRequest) {
447 osxni->mIconRequest->Cancel(NS_BINDING_ABORTED);
448 osxni->mIconRequest = nullptr;
451 CloseAlertCocoaString(osxni->mName);
453 for (unsigned int i = 0; i < mPendingAlerts.Length(); i++) {
454 if (mPendingAlerts[i] == osxni) {
455 mActiveAlerts.AppendElement(osxni);
456 mPendingAlerts.RemoveElementAt(i);
461 [GetNotificationCenter() deliverNotification:osxni->mPendingNotification];
463 if (osxni->mObserver) {
464 osxni->mObserver->Observe(nullptr, "alertshow", osxni->mCookie.get());
467 [osxni->mPendingNotification release];
468 osxni->mPendingNotification = nil;
470 NS_OBJC_END_TRY_IGNORE_BLOCK;
474 OSXNotificationCenter::OnImageMissing(nsISupports* aUserData) {
475 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
477 OSXNotificationInfo* osxni = static_cast<OSXNotificationInfo*>(aUserData);
478 if (osxni->mPendingNotification) {
479 // If there was an error getting the image, or the request timed out, show
480 // the notification without a content image.
481 ShowPendingNotification(osxni);
485 NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
489 OSXNotificationCenter::OnImageReady(nsISupports* aUserData,
490 imgIRequest* aRequest) {
491 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
493 nsCOMPtr<imgIContainer> image;
494 nsresult rv = aRequest->GetImage(getter_AddRefs(image));
495 if (NS_WARN_IF(NS_FAILED(rv) || !image)) {
499 OSXNotificationInfo* osxni = static_cast<OSXNotificationInfo*>(aUserData);
500 if (!osxni->mPendingNotification) {
501 return NS_ERROR_FAILURE;
504 NSImage* cocoaImage = nil;
505 // TODO: Pass pres context / ComputedStyle here to support context paint
507 // TODO: Do we have a reasonable size to pass around here?
508 nsCocoaUtils::CreateDualRepresentationNSImageFromImageContainer(
509 image, imgIContainer::FRAME_FIRST, nullptr, NSMakeSize(0, 0),
511 (osxni->mPendingNotification).contentImage = cocoaImage;
512 [cocoaImage release];
513 ShowPendingNotification(osxni);
517 NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
520 // nsIAlertsDoNotDisturb
522 OSXNotificationCenter::GetManualDoNotDisturb(bool* aRetVal) {
523 return NS_ERROR_NOT_IMPLEMENTED;
527 OSXNotificationCenter::SetManualDoNotDisturb(bool aDoNotDisturb) {
528 return NS_ERROR_NOT_IMPLEMENTED;
532 OSXNotificationCenter::GetSuppressForScreenSharing(bool* aRetVal) {
533 NS_OBJC_BEGIN_TRY_BLOCK_RETURN
535 NS_ENSURE_ARG(aRetVal);
536 *aRetVal = mSuppressForScreenSharing;
539 NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE)
543 OSXNotificationCenter::SetSuppressForScreenSharing(bool aSuppress) {
544 NS_OBJC_BEGIN_TRY_BLOCK_RETURN
546 mSuppressForScreenSharing = aSuppress;
549 NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE)
552 } // namespace mozilla