Backed out changeset b71c8c052463 (bug 1943846) for causing mass failures. CLOSED...
[gecko.git] / widget / cocoa / OSXNotificationCenter.mm
blob753acbc7665d167a8786619f4bba7f141a9f5ad4
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"
17 #include "nsString.h"
18 #include "nsCOMPtr.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;
30 @end
32 @implementation mozNotificationCenterDelegate
34 - (id)initWithOSXNC:(OSXNotificationCenter*)osxnc {
35   [super init];
36   // We should *never* outlive this OSXNotificationCenter.
37   mOSXNC = osxnc;
38   return self;
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];
52   }
53   mOSXNC->OnActivate([[notification userInfo] valueForKey:@"name"],
54                      notification.activationType, additionalActionIndex);
57 - (BOOL)userNotificationCenter:(NSUserNotificationCenter*)center
58      shouldPresentNotification:(NSUserNotification*)notification {
59   return YES;
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);
69   }
72 // This is an undocumented method that we need to be notified if a user clicks
73 // the close button.
74 - (void)userNotificationCenter:(NSUserNotificationCenter*)center
75                didDismissAlert:(NSUserNotification*)notification {
76   NSString* name = [[notification userInfo] valueForKey:@"name"];
77   mOSXNC->CloseAlertCocoaString(name);
80 @end
82 namespace mozilla {
84 enum {
85   OSXNotificationActionDisable = 0,
86   OSXNotificationActionSettings = 1,
89 class OSXNotificationInfo final : public nsISupports {
90  private:
91   virtual ~OSXNotificationInfo();
93  public:
94   NS_DECL_ISUPPORTS
95   OSXNotificationInfo(NSString* name, nsIObserver* observer,
96                       const nsAString& alertCookie);
98   NSString* mName;
99   nsCOMPtr<nsIObserver> mObserver;
100   nsString mCookie;
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;
123   [mName release];
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];
152   [mDelegate release];
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
164                                                       : NS_ERROR_FAILURE;
166   NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
169 NS_IMETHODIMP
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);
190 NS_IMETHODIMP
191 OSXNotificationCenter::ShowAlert(nsIAlertNotification* aAlert,
192                                  nsIObserver* aAlertListener) {
193   return ShowAlertWithIconData(aAlert, aAlertListener, 0, nullptr);
196 NS_IMETHODIMP
197 OSXNotificationCenter::ShowAlertWithIconData(nsIAlertNotification* aAlert,
198                                              nsIObserver* aAlertListener,
199                                              uint32_t aIconSize,
200                                              const uint8_t* aIconData) {
201   NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
203   NS_ENSURE_ARG(aAlert);
205   if (mSuppressForScreenSharing) {
206     return NS_OK;
207   }
209   Class unClass = NSClassFromString(@"NSUserNotification");
210   NSUserNotification* notification = [[unClass alloc] init];
212   nsAutoString title;
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,
230                                  notificationSource);
231     notification.subtitle = nsCocoaUtils::ToNSString(notificationSource);
232   }
234   nsAutoString text;
235   rv = aAlert->GetText(text);
236   NS_ENSURE_SUCCESS(rv, rv);
237   notification.informativeText = nsCocoaUtils::ToNSString(text);
239   bool isSilent;
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.
246   bool isActionable;
247   if (bundle && NS_SUCCEEDED(aAlert->GetActionable(&isActionable)) &&
248       isActionable) {
249     nsAutoString closeButtonTitle, actionButtonTitle, disableButtonTitle,
250         settingsButtonTitle;
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);
257     }
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
264     // action menu.
265     if ([notification respondsToSelector:@selector(set_showsButtons:)] &&
266         [notification
267             respondsToSelector:@selector(set_alwaysShowAlternateActionMenu:)] &&
268         [notification
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)
280       ]
281                                  forKey:@"_alternateActionButtonTitles"];
282     }
283   }
284   nsAutoString name;
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;
291   }
293   NS_ENSURE_SUCCESS(rv, rv);
294   NSString* alertName = nsCocoaUtils::ToNSString(name);
295   if (!alertName) {
296     return NS_ERROR_FAILURE;
297   }
298   notification.userInfo = [NSDictionary
299       dictionaryWithObjects:[NSArray arrayWithObjects:alertName, nil]
300                     forKeys:[NSArray arrayWithObjects:@"name", nil]];
302   nsAutoString cookie;
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.
310   if (aIconSize > 0 &&
311       [notification respondsToSelector:@selector(set_identityImage:)] &&
312       [notification
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"];
319   }
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());
334     }
335   } else {
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);
343     }
344   }
346   return NS_OK;
348   NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
351 NS_IMETHODIMP
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);
358   return NS_OK;
360   NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
363 void OSXNotificationCenter::CloseAlertCocoaString(NSString* aAlertName) {
364   NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
366   if (!aAlertName) {
367     return;  // Can't do anything without a name
368   }
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];
375       break;
376     }
377   }
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());
385       }
386       if (osxni->mIconRequest) {
387         osxni->mIconRequest->Cancel(NS_BINDING_ABORTED);
388         osxni->mIconRequest = nullptr;
389       }
390       mActiveAlerts.RemoveElementAt(i);
391       break;
392     }
393   }
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;
403   if (!aAlertName) {
404     return;  // Can't do anything without a name
405   }
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());
418                 break;
419               case OSXNotificationActionSettings:
420                 osxni->mObserver->Observe(nullptr, "alertsettingscallback",
421                                           osxni->mCookie.get());
422                 break;
423               default:
424                 NS_WARNING(
425                     "Unknown NSUserNotification additional action clicked");
426                 break;
427             }
428             break;
429           default:
430             osxni->mObserver->Observe(nullptr, "alertclickcallback",
431                                       osxni->mCookie.get());
432             break;
433         }
434       }
435       return;
436     }
437   }
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;
449   }
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);
457       break;
458     }
459   }
461   [GetNotificationCenter() deliverNotification:osxni->mPendingNotification];
463   if (osxni->mObserver) {
464     osxni->mObserver->Observe(nullptr, "alertshow", osxni->mCookie.get());
465   }
467   [osxni->mPendingNotification release];
468   osxni->mPendingNotification = nil;
470   NS_OBJC_END_TRY_IGNORE_BLOCK;
473 NS_IMETHODIMP
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);
482   }
483   return NS_OK;
485   NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
488 NS_IMETHODIMP
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)) {
496     return rv;
497   }
499   OSXNotificationInfo* osxni = static_cast<OSXNotificationInfo*>(aUserData);
500   if (!osxni->mPendingNotification) {
501     return NS_ERROR_FAILURE;
502   }
504   NSImage* cocoaImage = nil;
505   // TODO: Pass pres context / ComputedStyle here to support context paint
506   // properties.
507   // TODO: Do we have a reasonable size to pass around here?
508   nsCocoaUtils::CreateDualRepresentationNSImageFromImageContainer(
509       image, imgIContainer::FRAME_FIRST, nullptr, NSMakeSize(0, 0),
510       &cocoaImage);
511   (osxni->mPendingNotification).contentImage = cocoaImage;
512   [cocoaImage release];
513   ShowPendingNotification(osxni);
515   return NS_OK;
517   NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
520 // nsIAlertsDoNotDisturb
521 NS_IMETHODIMP
522 OSXNotificationCenter::GetManualDoNotDisturb(bool* aRetVal) {
523   return NS_ERROR_NOT_IMPLEMENTED;
526 NS_IMETHODIMP
527 OSXNotificationCenter::SetManualDoNotDisturb(bool aDoNotDisturb) {
528   return NS_ERROR_NOT_IMPLEMENTED;
531 NS_IMETHODIMP
532 OSXNotificationCenter::GetSuppressForScreenSharing(bool* aRetVal) {
533   NS_OBJC_BEGIN_TRY_BLOCK_RETURN
535   NS_ENSURE_ARG(aRetVal);
536   *aRetVal = mSuppressForScreenSharing;
537   return NS_OK;
539   NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE)
542 NS_IMETHODIMP
543 OSXNotificationCenter::SetSuppressForScreenSharing(bool aSuppress) {
544   NS_OBJC_BEGIN_TRY_BLOCK_RETURN
546   mSuppressForScreenSharing = aSuppress;
547   return NS_OK;
549   NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE)
552 }  // namespace mozilla