Bug 728106: Add Notification Center support for OS X 10.8. r=smichaud,dougt
authorJosh Aas <joshmoz@gmail.com>
Wed, 22 Aug 2012 15:30:13 -0400
changeset 103043 67c5213dfe2b944d0c2d019f9a47cb7da74823a2
parent 103042 41a3cdf9206337399613463f3f008b24d764d1a4
child 103044 5253021171872eada9416ea7294a53588cec7408
push id13794
push userjosh@mozilla.com
push dateWed, 22 Aug 2012 19:30:29 +0000
treeherdermozilla-inbound@67c5213dfe2b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmichaud, dougt
bugs728106
milestone17.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 728106: Add Notification Center support for OS X 10.8. r=smichaud,dougt
toolkit/components/alerts/mac/Makefile.in
toolkit/components/alerts/mac/ObserverPair.h
toolkit/components/alerts/mac/ObserverPair.mm
toolkit/components/alerts/mac/mozGrowlDelegate.mm
toolkit/components/alerts/mac/mozNotificationCenterDelegate.h
toolkit/components/alerts/mac/mozNotificationCenterDelegate.mm
toolkit/components/alerts/mac/nsAlertsServiceModule.cpp
toolkit/components/alerts/mac/nsGrowlAlertsService.h
toolkit/components/alerts/mac/nsGrowlAlertsService.mm
toolkit/components/alerts/mac/nsMacAlertsService.h
toolkit/components/alerts/mac/nsMacAlertsService.mm
toolkit/components/alerts/mac/nsNotificationCenterCompat.h
--- a/toolkit/components/alerts/mac/Makefile.in
+++ b/toolkit/components/alerts/mac/Makefile.in
@@ -10,33 +10,35 @@ VPATH = @srcdir@
 include $(DEPTH)/config/autoconf.mk
 
 MODULE = alerts
 LIBRARY_NAME = alerts
 IS_COMPONENT = 1
 FORCE_SHARED_LIB = 1
 
 CMMSRCS = \
-  nsGrowlAlertsService.mm \
+  nsMacAlertsService.mm \
   mozGrowlDelegate.mm \
+  mozNotificationCenterDelegate.mm \
   nsAlertsImageLoadListener.mm \
   nsNotificationsList.mm \
+  ObserverPair.mm \
   $(NULL)
 
 CPPSRCS = \
   nsAlertsServiceModule.cpp \
   $(NULL)
 
 LOCAL_INCLUDES += \
   -I$(srcdir)/growl/ \
   -I$(topsrcdir)/toolkit/components/build/ \
   $(NULL)
 
 EXPORTS = \
-  nsGrowlAlertsService.h \
+  nsMacAlertsService.h \
   $(NULL)
 
 SHARED_LIBRARY_LIBS = \
   growl/$(LIB_PREFIX)growl_s.$(LIB_SUFFIX) \
   $(NULL)
 
 ifdef JS_SHARED_LIBRARY
 js_ldopts = $(MOZ_JS_LIBS)
new file mode 100644
--- /dev/null
+++ b/toolkit/components/alerts/mac/ObserverPair.h
@@ -0,0 +1,73 @@
+/* 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/. */
+
+#import <Cocoa/Cocoa.h>
+
+#include "nsIXPConnect.h"
+#include "nsIObserver.h"
+#include "nsIDOMWindow.h"
+#include "nsIJSContextStack.h"
+#include "nsServiceManagerUtils.h"
+
+@interface ObserverPair : NSObject
+{
+@public
+  nsIObserver *observer;
+  nsIDOMWindow *window;
+}
+
+- (id) initWithObserver:(nsIObserver *)aObserver window:(nsIDOMWindow *)aWindow;
+- (void) dealloc;
+
+@end
+
+/**
+ * Returns the DOM window that owns the given observer in the case that the
+ * observer is implemented in JS and was created in a DOM window's scope.
+ *
+ * We need this so that we can properly clean up in cases where the window gets
+ * closed before the growl timeout/click notifications have fired. Otherwise we
+ * leak those windows.
+ */
+static already_AddRefed<nsIDOMWindow> __attribute__((unused))
+GetWindowOfObserver(nsIObserver* aObserver)
+{
+  nsCOMPtr<nsIXPConnectWrappedJS> wrappedJS(do_QueryInterface(aObserver));
+  if (!wrappedJS) {
+    // We can't do anything with objects that aren't implemented in JS...
+    return nullptr;
+  }
+
+  JSObject* obj;
+  nsresult rv = wrappedJS->GetJSObject(&obj);
+  NS_ENSURE_SUCCESS(rv, nullptr);
+
+  nsCOMPtr<nsIThreadJSContextStack> stack =
+    do_GetService("@mozilla.org/js/xpc/ContextStack;1", &rv);
+  NS_ENSURE_SUCCESS(rv, nullptr);
+
+  JSContext* cx = stack->GetSafeJSContext();
+  NS_ENSURE_TRUE(cx, nullptr);
+
+  JSAutoRequest ar(cx);
+  JSAutoEnterCompartment ac;
+  if (!ac.enter(cx, obj)) {
+    return nullptr;
+  }
+
+  JSObject* global = JS_GetGlobalForObject(cx, obj);
+  NS_ENSURE_TRUE(global, nullptr);
+
+  nsCOMPtr<nsIXPConnect> xpc(do_GetService(nsIXPConnect::GetCID()));
+  NS_ENSURE_TRUE(xpc, nullptr);
+
+  nsCOMPtr<nsIXPConnectWrappedNative> wrapper;
+  rv = xpc->GetWrappedNativeOfJSObject(cx, global, getter_AddRefs(wrapper));
+  NS_ENSURE_SUCCESS(rv, nullptr);
+
+  nsCOMPtr<nsIDOMWindow> window = do_QueryWrappedNative(wrapper);
+  NS_ENSURE_TRUE(window, nullptr);
+
+  return window.forget();
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/alerts/mac/ObserverPair.mm
@@ -0,0 +1,36 @@
+/* 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/. */
+
+#import "ObserverPair.h"
+
+#include "nsObjCExceptions.h"
+
+@implementation ObserverPair
+
+- (id) initWithObserver:(nsIObserver *)aObserver window:(nsIDOMWindow *)aWindow
+{
+  NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL;
+
+  if ((self = [super init])) {
+    NS_ADDREF(observer = aObserver);
+    NS_IF_ADDREF(window = aWindow);
+  }
+
+  return self;
+
+  NS_OBJC_END_TRY_ABORT_BLOCK_NIL;
+}
+
+- (void) dealloc
+{
+  NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+  NS_IF_RELEASE(observer);
+  NS_IF_RELEASE(window);
+  [super dealloc];
+
+  NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+@end
--- a/toolkit/components/alerts/mac/mozGrowlDelegate.mm
+++ b/toolkit/components/alerts/mac/mozGrowlDelegate.mm
@@ -1,122 +1,21 @@
 /* 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/. */
 
 #import "mozGrowlDelegate.h"
+#import "ObserverPair.h"
 
 #include "nsIObserver.h"
-#include "nsIXPConnect.h"
 #include "nsIXULAppInfo.h"
 #include "nsIStringBundle.h"
-#include "nsIJSContextStack.h"
 #include "nsIDOMWindow.h"
 
-#include "jsapi.h"
-#include "nsCOMPtr.h"
 #include "nsObjCExceptions.h"
-#include "nsServiceManagerUtils.h"
-#include "nsWeakReference.h"
-
-/**
- * Returns the DOM window that owns the given observer in the case that the
- * observer is implemented in JS and was created in a DOM window's scope.
- *
- * We need this so that we can properly clean up in cases where the window gets
- * closed before the growl timeout/click notifications have fired. Otherwise we
- * leak those windows.
- */
-static already_AddRefed<nsIDOMWindow>
-GetWindowOfObserver(nsIObserver* aObserver)
-{
-  nsCOMPtr<nsIXPConnectWrappedJS> wrappedJS(do_QueryInterface(aObserver));
-  if (!wrappedJS) {
-    // We can't do anything with objects that aren't implemented in JS...
-    return nullptr;
-  }
-
-  JSObject* obj;
-  nsresult rv = wrappedJS->GetJSObject(&obj);
-  NS_ENSURE_SUCCESS(rv, nullptr);
-
-  nsCOMPtr<nsIThreadJSContextStack> stack =
-    do_GetService("@mozilla.org/js/xpc/ContextStack;1", &rv);
-  NS_ENSURE_SUCCESS(rv, nullptr);
-
-  JSContext* cx = stack->GetSafeJSContext();
-  NS_ENSURE_TRUE(cx, nullptr);
-
-  JSAutoRequest ar(cx);
-  JSAutoEnterCompartment ac;
-  if (!ac.enter(cx, obj)) {
-    return nullptr;
-  }
-
-  JSObject* global = JS_GetGlobalForObject(cx, obj);
-  NS_ENSURE_TRUE(global, nullptr);
-
-  nsCOMPtr<nsIXPConnect> xpc(do_GetService(nsIXPConnect::GetCID()));
-  NS_ENSURE_TRUE(xpc, nullptr);
-
-  nsCOMPtr<nsIXPConnectWrappedNative> wrapper;
-  rv = xpc->GetWrappedNativeOfJSObject(cx, global, getter_AddRefs(wrapper));
-  NS_ENSURE_SUCCESS(rv, nullptr);
-
-  nsCOMPtr<nsIDOMWindow> window = do_QueryWrappedNative(wrapper);
-  NS_ENSURE_TRUE(window, nullptr);
-
-  return window.forget();
-}
-
-@interface ObserverPair : NSObject
-{
-@public
-  nsIObserver *observer;
-  nsIDOMWindow *window;
-}
-
-- (id) initWithObserver:(nsIObserver *)aObserver window:(nsIDOMWindow *)aWindow;
-- (void) dealloc;
-
-@end
-
-@implementation ObserverPair
-
-- (id) initWithObserver:(nsIObserver *)aObserver window:(nsIDOMWindow *)aWindow
-{
-  NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL;
-
-  if ((self = [super init])) {
-    NS_ADDREF(observer = aObserver);
-    NS_IF_ADDREF(window = aWindow);
-    return self;
-  }
-
-  // Safeguard against calling NS_RELEASE on uninitialized memory.
-  observer = nullptr;
-  window = nullptr;
-
-  return nil;
-
-  NS_OBJC_END_TRY_ABORT_BLOCK_NIL;
-}
-
-- (void) dealloc
-{
-  NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
-
-  NS_IF_RELEASE(observer);
-  NS_IF_RELEASE(window);
-  [super dealloc];
-
-  NS_OBJC_END_TRY_ABORT_BLOCK;
-}
-
-@end
 
 @implementation mozGrowlDelegate
 
 - (id) init
 {
   NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL;
 
   if ((self = [super init])) {
new file mode 100644
--- /dev/null
+++ b/toolkit/components/alerts/mac/mozNotificationCenterDelegate.h
@@ -0,0 +1,58 @@
+/* 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/. */
+
+#import <Cocoa/Cocoa.h>
+
+#include "nscore.h"
+#include "nsStringGlue.h"
+#include "nsNotificationCenterCompat.h"
+
+#define OBSERVER_KEY @"ALERT_OBSERVER"
+#define COOKIE_KEY   @"ALERT_COOKIE"
+
+class nsIObserver;
+class nsIDOMWindow;
+
+@interface mozNotificationCenterDelegate : NSObject <NSUserNotificationCenterDelegate>
+{
+@private
+  PRUint32 mKey;
+  NSMutableDictionary *mObserverDict;
+  id<FakeNSUserNotificationCenter> mCenter;
+}
+
+/**
+ * Dispatches a notification to Notification Center
+ *
+ * @param aTitle  The title of the notification
+ * @param aText   The body of the notification
+ * @param aKey    The observer key to use as a lookup (or 0 if no observer)
+ * @param aCookie The string to be used as a cookie if there is an observer
+ */
+- (void)notifyWithTitle:(const nsAString&)aTitle
+            description:(const nsAString&)aText
+                    key:(PRUint32)aKey
+                 cookie:(const nsAString&)aCookie;
+
+/**
+ * Adds an nsIObserver that we can query later for dispatching obsevers.
+ *
+ * @param aObserver The observer we are adding.
+ * @return The key it was stored in.
+ */
+- (PRUint32)addObserver:(nsIObserver*)aObserver;
+
+/**
+ * Called when a window was closed and the observers are no longer valid.
+ *
+ * @param window The window that was closed.
+ */
+- (void)forgetObserversForWindow:(nsIDOMWindow*)window;
+
+/**
+ * Called when all observers should be released.
+ */
+- (void)forgetObservers;
+
+@end
new file mode 100644
--- /dev/null
+++ b/toolkit/components/alerts/mac/mozNotificationCenterDelegate.mm
@@ -0,0 +1,193 @@
+/* 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/. */
+
+#import "mozNotificationCenterDelegate.h"
+#import "ObserverPair.h"
+
+#include "nsIObserver.h"
+#include "nsIXULAppInfo.h"
+#include "nsIStringBundle.h"
+#include "nsIDOMWindow.h"
+#include "nsObjCExceptions.h"
+
+// Note that all calls to the Notification Center API are not made normally.
+// We do this to maintain backwards compatibility.
+
+id<FakeNSUserNotificationCenter> GetNotificationCenter() {
+  Class c = NSClassFromString(@"NSUserNotificationCenter");
+  return [c performSelector:@selector(defaultUserNotificationCenter)];
+}
+
+@implementation mozNotificationCenterDelegate
+
+- (id)init
+{
+  NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL;
+
+  if ((self = [super init])) {
+    mCenter = [GetNotificationCenter() retain];
+    mKey = 0;
+    mObserverDict = [[NSMutableDictionary dictionaryWithCapacity:8] retain];
+  }
+
+  return self;
+
+  NS_OBJC_END_TRY_ABORT_BLOCK_NIL;
+}
+
+- (void)dealloc
+{
+  NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+  [mObserverDict release];
+  [mCenter release];
+
+  [super dealloc];
+
+  NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+-(void) notifyWithTitle:(const nsAString&)aTitle
+            description:(const nsAString&)aText
+                    key:(PRUint32)aKey
+                 cookie:(const nsAString&)aCookie;
+{
+  NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+  Class unClass = NSClassFromString(@"NSUserNotification");
+  id<FakeNSUserNotification> notification = [[unClass alloc] init];
+  notification.title = [NSString stringWithCharacters:aTitle.BeginReading()
+                                               length:aTitle.Length()];
+  notification.informativeText = [NSString stringWithCharacters:aText.BeginReading()
+                                                         length:aText.Length()];
+  NSDictionary* clickContext = nil;
+  if (aKey) {
+    clickContext = [NSDictionary dictionaryWithObjectsAndKeys:
+                    [NSNumber numberWithUnsignedInt:aKey],
+                    OBSERVER_KEY,
+                    [NSArray arrayWithObject:
+                     [NSString stringWithCharacters:aCookie.BeginReading()
+                                             length:aCookie.Length()]],
+                    COOKIE_KEY,
+                    nil];
+  }
+  notification.userInfo = clickContext;
+  notification.soundName = NSUserNotificationDefaultSoundName;
+  notification.hasActionButton = NO;
+
+  [GetNotificationCenter() deliverNotification:notification];
+
+  NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+- (PRUint32) addObserver:(nsIObserver *)aObserver
+{
+  NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN;
+
+  nsCOMPtr<nsIDOMWindow> parentWindow = GetWindowOfObserver(aObserver);
+
+  ObserverPair* pair = [[ObserverPair alloc] initWithObserver:aObserver
+                                                       window:parentWindow];
+
+  [mObserverDict setObject:pair forKey:[NSNumber numberWithUnsignedInt:++mKey]];
+
+  [pair release];
+
+  return mKey;
+
+  NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(0);
+}
+
+- (void) forgetObserversForWindow:(nsIDOMWindow*)window
+{
+  NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+  NS_ASSERTION(window, "No window!");
+
+  NSMutableArray *keysToRemove = [NSMutableArray array];
+
+  NSEnumerator *keyEnumerator = [[mObserverDict allKeys] objectEnumerator];
+  NSNumber *key;
+  while ((key = [keyEnumerator nextObject])) {
+    ObserverPair *pair = [mObserverDict objectForKey:key];
+    if (pair && pair->window == window)
+      [keysToRemove addObject:key];
+  }
+
+  [mObserverDict removeObjectsForKeys:keysToRemove];
+
+  NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+- (void) forgetObservers
+{
+  NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+  [mObserverDict removeAllObjects];
+
+  NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+// Implement NSUserNotificationCenterDelegate
+
+- (void)userNotificationCenter:(id<FakeNSUserNotificationCenter>)center
+        didDeliverNotification:(id<FakeNSUserNotification>)notification
+{
+  NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+  // Internally we have two notifications, "alertclickcallback" and "alertfinished".
+  // Notification Center does not tell us when the alert finished, but we always need
+  // to send alert finished, or otherwise consider it finished in order to free up
+  // observers. This is a conundrum. For now we'll do the unfortunately conservative
+  // thing and not support actions so that we can consistently end alerts when they
+  // are shown.
+
+  NSDictionary* userInfo = notification.userInfo;
+
+  NS_ASSERTION([userInfo valueForKey:OBSERVER_KEY] != nil,
+               "OBSERVER_KEY not found!");
+  NS_ASSERTION([userInfo valueForKey:COOKIE_KEY] != nil,
+               "COOKIE_KEY not found!");
+
+  ObserverPair* pair = [mObserverDict objectForKey:[userInfo valueForKey:OBSERVER_KEY]];
+  nsCOMPtr<nsIObserver> observer = pair ? pair->observer : nullptr;
+  [mObserverDict removeObjectForKey:[userInfo valueForKey:OBSERVER_KEY]];
+
+  if (observer) {
+    NSString* cookie = [[userInfo valueForKey:COOKIE_KEY] objectAtIndex:0];
+
+    nsAutoString tmp;
+    tmp.SetLength([cookie length]);
+    [cookie getCharacters:tmp.BeginWriting()];
+
+    observer->Observe(nullptr, "alertfinished", tmp.get());
+  }
+
+  // We don't want alerts to persist in the notification center because they
+  // are likely to refer to potentially disappearing contexts like a download.
+  //
+  // However, since we don't react to clicks right now, it is OK to let them
+  // persist.
+  //[mCenter removeDeliveredNotification:notification];
+
+  NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+- (void)userNotificationCenter:(id<FakeNSUserNotificationCenter>)center
+       didActivateNotification:(id<FakeNSUserNotification>)notification
+{
+  // We don't support activation right now because it'd be sent after
+  // we send 'alertfinished' to observers. This is unfortunate but necessary
+  // because we can't reliably know when a notification is finished, we
+  // can only know when it is shown or activated.
+}
+
+- (BOOL)userNotificationCenter:(id<FakeNSUserNotificationCenter>)center
+     shouldPresentNotification:(id<FakeNSUserNotification>)notification
+{
+  // Respect OS choice.
+  return NO;
+}
+
+@end
--- a/toolkit/components/alerts/mac/nsAlertsServiceModule.cpp
+++ b/toolkit/components/alerts/mac/nsAlertsServiceModule.cpp
@@ -1,26 +1,26 @@
 /* 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 "nsGrowlAlertsService.h"
+#include "nsMacAlertsService.h"
 #include "nsToolkitCompsCID.h"
 #include "mozilla/ModuleUtils.h"
 #include "nsCOMPtr.h"
 #include "nsIServiceManager.h"
 #include "nsServiceManagerUtils.h"
 #include "nsICategoryManager.h"
 #include "nsMemory.h"
 
-NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(nsGrowlAlertsService, Init)
+NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(nsMacAlertsService, Init)
 NS_DEFINE_NAMED_CID(NS_SYSTEMALERTSSERVICE_CID);
 
 static const mozilla::Module::CIDEntry kAlertsCIDs[] = {
-  { &kNS_SYSTEMALERTSSERVICE_CID, false, NULL, nsGrowlAlertsServiceConstructor },
+  { &kNS_SYSTEMALERTSSERVICE_CID, false, NULL, nsMacAlertsServiceConstructor },
   { NULL }
 };
 
 static const mozilla::Module::ContractIDEntry kAlertsContracts[] = {
   { NS_SYSTEMALERTSERVICE_CONTRACTID, &kNS_SYSTEMALERTSSERVICE_CID },
   { NULL }
 };
 
rename from toolkit/components/alerts/mac/nsGrowlAlertsService.h
rename to toolkit/components/alerts/mac/nsMacAlertsService.h
--- a/toolkit/components/alerts/mac/nsGrowlAlertsService.h
+++ b/toolkit/components/alerts/mac/nsMacAlertsService.h
@@ -4,25 +4,31 @@
 
 #ifndef nsGrowlAlertsService_h_
 #define nsGrowlAlertsService_h_
 
 #include "nsIAlertsService.h"
 #include "nsIObserver.h"
 
 struct GrowlDelegateWrapper;
+struct NotificationCenterDelegateWrapper;
 
-class nsGrowlAlertsService : public nsIAlertsService,
-                             public nsIObserver
+class nsMacAlertsService : public nsIAlertsService,
+                           public nsIObserver
 {
 public:
   NS_DECL_ISUPPORTS
   NS_DECL_NSIALERTSSERVICE
   NS_DECL_NSIOBSERVER
 
-  nsGrowlAlertsService();
+  nsMacAlertsService();
   nsresult Init();
+
 private:
-  GrowlDelegateWrapper* mDelegate;
-  virtual ~nsGrowlAlertsService();
+  virtual ~nsMacAlertsService();
+  nsresult InitGrowl();
+  nsresult InitNotificationCenter();
+
+  GrowlDelegateWrapper* mGrowlDelegate;
+  NotificationCenterDelegateWrapper* mNCDelegate;
 };
 
 #endif // nsGrowlAlertsService_h_
rename from toolkit/components/alerts/mac/nsGrowlAlertsService.mm
rename to toolkit/components/alerts/mac/nsMacAlertsService.mm
--- a/toolkit/components/alerts/mac/nsGrowlAlertsService.mm
+++ b/toolkit/components/alerts/mac/nsMacAlertsService.mm
@@ -1,222 +1,344 @@
-/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
- * vim: sw=2 ts=2 sts=2 expandtab
- * This Source Code Form is subject to the terms of the Mozilla Public
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: sw=2 ts=2 sts=2 expandtab */
+/* 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 "nsGrowlAlertsService.h"
+#import <CoreServices/CoreServices.h>
+
+#include "nsMacAlertsService.h"
 #include "nsStringAPI.h"
 #include "nsAlertsImageLoadListener.h"
 #include "nsIURI.h"
 #include "nsIStreamLoader.h"
 #include "nsNetUtil.h"
 #include "nsCOMPtr.h"
 #include "nsIStringBundle.h"
 #include "nsIObserverService.h"
 #include "nsAutoPtr.h"
 #include "nsNotificationsList.h"
 #include "nsObjCExceptions.h"
 #include "nsPIDOMWindow.h"
 
 #import "mozGrowlDelegate.h"
+#import "mozNotificationCenterDelegate.h"
 #import "GrowlApplicationBridge.h"
+#import "nsNotificationCenterCompat.h"
 
 ////////////////////////////////////////////////////////////////////////////////
 //// GrowlDelegateWrapper
 
 struct GrowlDelegateWrapper
 {
-  mozGrowlDelegate* delegate;
+  mozGrowlDelegate* mDelegate;
 
   GrowlDelegateWrapper()
   {
     NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
 
-    delegate = [[mozGrowlDelegate alloc] init];
+    mDelegate = [[mozGrowlDelegate alloc] init];
 
     NS_OBJC_END_TRY_ABORT_BLOCK;
   }
 
   ~GrowlDelegateWrapper()
   {
     NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
 
-    [delegate release];
+    [mDelegate release];
 
     NS_OBJC_END_TRY_ABORT_BLOCK;
   }
 };
 
 /**
  * Helper function to dispatch a notification to Growl
  */
 static nsresult
-DispatchNamedNotification(const nsAString &aName,
+DispatchGrowlNotification(const nsAString &aName,
                           const nsAString &aImage,
                           const nsAString &aTitle,
                           const nsAString &aMessage,
                           const nsAString &aCookie,
                           nsIObserver *aListener)
 {
   NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
 
-  if ([GrowlApplicationBridge isGrowlRunning] == NO)
+  if ([GrowlApplicationBridge isGrowlRunning] == NO) {
     return NS_ERROR_NOT_AVAILABLE;
+  }
 
   mozGrowlDelegate *delegate =
     static_cast<mozGrowlDelegate *>([GrowlApplicationBridge growlDelegate]);
-  if (!delegate)
+  if (!delegate) {
     return NS_ERROR_NOT_AVAILABLE;
+  }
 
   uint32_t ind = 0;
-  if (aListener)
+  if (aListener) {
     ind = [delegate addObserver: aListener];
+  }
 
   nsCOMPtr<nsIURI> uri;
   nsresult rv = NS_NewURI(getter_AddRefs(uri), aImage);
   if (NS_FAILED(rv)) {
     // image uri failed to resolve, so dispatch to growl with no image
     [mozGrowlDelegate notifyWithName: aName
                                title: aTitle
                          description: aMessage
                             iconData: [NSData data]
                                  key: ind
                               cookie: aCookie];
     return NS_OK;
   }
 
   nsCOMPtr<nsAlertsImageLoadListener> listener =
     new nsAlertsImageLoadListener(aName, aTitle, aMessage, aCookie, ind);
-  if (!listener)
+  if (!listener) {
     return NS_ERROR_OUT_OF_MEMORY;
+  }
 
   nsCOMPtr<nsIStreamLoader> loader;
   return NS_NewStreamLoader(getter_AddRefs(loader), uri, listener);
 
   NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
 }
 
 ////////////////////////////////////////////////////////////////////////////////
-//// nsGrowlAlertsService
+//// NotificationCenterDelegateWrapper
+
+struct NotificationCenterDelegateWrapper
+{
+  mozNotificationCenterDelegate* mDelegate;
+
+  NotificationCenterDelegateWrapper()
+  {
+    NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+    mDelegate = [[mozNotificationCenterDelegate alloc] init];
+    
+    Class c = NSClassFromString(@"NSUserNotificationCenter");
+    id<FakeNSUserNotificationCenter> nc = [c performSelector:@selector(defaultUserNotificationCenter)];
+    nc.delegate = mDelegate;
+
+    NS_OBJC_END_TRY_ABORT_BLOCK;
+  }
+
+  ~NotificationCenterDelegateWrapper()
+  {
+    NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+    Class c = NSClassFromString(@"NSUserNotificationCenter");
+    id<FakeNSUserNotificationCenter> nc = [c performSelector:@selector(defaultUserNotificationCenter)];
+    nc.delegate = nil;
+
+    [mDelegate release];
+
+    NS_OBJC_END_TRY_ABORT_BLOCK;
+  }
+};
 
-NS_IMPL_THREADSAFE_ADDREF(nsGrowlAlertsService)
-NS_IMPL_THREADSAFE_RELEASE(nsGrowlAlertsService)
+static nsresult
+DispatchNCNotification(NotificationCenterDelegateWrapper* aDelegateWrapper,
+                       const nsAString &aTitle,
+                       const nsAString &aMessage,
+                       const nsAString &aCookie,
+                       nsIObserver *aListener)
+{
+  NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
+
+  mozNotificationCenterDelegate* delegate = aDelegateWrapper->mDelegate;
+
+  if (!delegate) {
+    return NS_ERROR_NOT_AVAILABLE;
+  }
 
-NS_INTERFACE_MAP_BEGIN(nsGrowlAlertsService)
+  uint32_t ind = 0;
+  if (aListener) {
+    ind = [delegate addObserver:aListener];
+  }
+
+  // Notification Center doesn't allow images so we'll just drop that
+  [delegate notifyWithTitle:aTitle
+                description:aMessage
+                        key:ind
+                     cookie:aCookie];
+
+  return NS_OK;
+
+  NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// nsMacAlertsService
+
+NS_IMPL_THREADSAFE_ADDREF(nsMacAlertsService)
+NS_IMPL_THREADSAFE_RELEASE(nsMacAlertsService)
+
+NS_INTERFACE_MAP_BEGIN(nsMacAlertsService)
   NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIAlertsService)
   NS_INTERFACE_MAP_ENTRY(nsIObserver)
   NS_INTERFACE_MAP_ENTRY(nsIAlertsService)
 NS_INTERFACE_MAP_END_THREADSAFE
 
 nsresult
-nsGrowlAlertsService::Init()
+nsMacAlertsService::Init()
 {
   NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
 
+  nsresult rv;
+  nsCOMPtr<nsIObserverService> os =
+    do_GetService("@mozilla.org/observer-service;1", &rv);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  // Only use Growl with OS version < 10.8.
+  SInt32 osVersion = 0;
+  OSErr err = ::Gestalt (gestaltSystemVersion, &osVersion);
+  osVersion &= 0xFFFF; // The system version is in the low order word
+  if ((err != noErr) || (osVersion < 0x00001080)) {
+    rv = InitGrowl();
+  }
+  else {
+    rv = InitNotificationCenter();
+  }
+
+  (void)os->AddObserver(this, DOM_WINDOW_DESTROYED_TOPIC, false);
+  (void)os->AddObserver(this, "profile-before-change", false);
+
+  return NS_OK;
+
+  NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
+}
+
+nsresult
+nsMacAlertsService::InitGrowl() {
+  NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
+
   NS_ASSERTION([GrowlApplicationBridge growlDelegate] == nil,
                "We already registered with Growl!");
 
   nsresult rv;
   nsCOMPtr<nsIObserverService> os =
     do_GetService("@mozilla.org/observer-service;1", &rv);
   NS_ENSURE_SUCCESS(rv, rv);
 
   nsRefPtr<nsNotificationsList> notifications = new nsNotificationsList();
 
-  if (notifications)
+  if (notifications) {
     (void)os->NotifyObservers(notifications, "before-growl-registration", nullptr);
+  }
 
-  mDelegate = new GrowlDelegateWrapper();
+  mGrowlDelegate = new GrowlDelegateWrapper();
 
-  if (notifications)
-    notifications->informController(mDelegate->delegate);
+  if (notifications) {
+    notifications->informController(mGrowlDelegate->mDelegate);
+  }
 
-  // registers with Growl
-  [GrowlApplicationBridge setGrowlDelegate: mDelegate->delegate];
-
-  (void)os->AddObserver(this, DOM_WINDOW_DESTROYED_TOPIC, false);
-  (void)os->AddObserver(this, "profile-before-change", false);
+  [GrowlApplicationBridge setGrowlDelegate: mGrowlDelegate->mDelegate];
 
   return NS_OK;
 
   NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
 }
 
-nsGrowlAlertsService::nsGrowlAlertsService() : mDelegate(nullptr) {}
+nsresult
+nsMacAlertsService::InitNotificationCenter() {
+  mNCDelegate = new NotificationCenterDelegateWrapper();
+  return NS_OK;
+}
 
-nsGrowlAlertsService::~nsGrowlAlertsService()
+nsMacAlertsService::nsMacAlertsService()
+: mGrowlDelegate(nullptr)
+, mNCDelegate(nullptr)
+{}
+
+nsMacAlertsService::~nsMacAlertsService()
 {
-  delete mDelegate;
+  delete mGrowlDelegate;
+  delete mNCDelegate;
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 //// nsIAlertsService
 
 NS_IMETHODIMP
-nsGrowlAlertsService::ShowAlertNotification(const nsAString& aImageUrl,
+nsMacAlertsService::ShowAlertNotification(const nsAString& aImageUrl,
                                             const nsAString& aAlertTitle,
                                             const nsAString& aAlertText,
                                             bool aAlertClickable,
                                             const nsAString& aAlertCookie,
                                             nsIObserver* aAlertListener,
                                             const nsAString& aAlertName)
 {
   NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
 
-  NS_ASSERTION(mDelegate->delegate == [GrowlApplicationBridge growlDelegate],
-               "Growl Delegate was not registered properly.");
+  // Growl requires a few extra steps, like checking that it's registered and
+  // the alert has a name.
+  if (mGrowlDelegate) {
+    NS_ASSERTION(mGrowlDelegate->mDelegate == [GrowlApplicationBridge growlDelegate],
+                 "Growl Delegate was not registered properly.");
 
-  if (!aAlertName.IsEmpty()) {
-    return DispatchNamedNotification(aAlertName, aImageUrl, aAlertTitle,
-                                     aAlertText, aAlertCookie, aAlertListener);
-  }
+    if (!aAlertName.IsEmpty()) {
+      return DispatchGrowlNotification(aAlertName, aImageUrl, aAlertTitle,
+                                       aAlertText, aAlertCookie, aAlertListener);
+    }
 
-  nsresult rv;
-  nsCOMPtr<nsIStringBundleService> bundleService =
-    do_GetService("@mozilla.org/intl/stringbundle;1", &rv);
+    nsresult rv;
+    nsCOMPtr<nsIStringBundleService> bundleService =
+      do_GetService("@mozilla.org/intl/stringbundle;1", &rv);
 
-  // We don't want to fail just yet if we can't get the alert name
-  nsString name = NS_LITERAL_STRING("General Notification");
-  if (NS_SUCCEEDED(rv)) {
-    nsCOMPtr<nsIStringBundle> bundle;
-    rv = bundleService->CreateBundle(GROWL_STRING_BUNDLE_LOCATION,
-                                     getter_AddRefs(bundle));
+    // We don't want to fail just yet if we can't get the alert name
+    nsString name = NS_LITERAL_STRING("General Notification");
     if (NS_SUCCEEDED(rv)) {
-      rv = bundle->GetStringFromName(NS_LITERAL_STRING("general").get(),
-                                     getter_Copies(name));
-      if (NS_FAILED(rv))
-        name = NS_LITERAL_STRING("General Notification");
+      nsCOMPtr<nsIStringBundle> bundle;
+      rv = bundleService->CreateBundle(GROWL_STRING_BUNDLE_LOCATION,
+                                       getter_AddRefs(bundle));
+      if (NS_SUCCEEDED(rv)) {
+        rv = bundle->GetStringFromName(NS_LITERAL_STRING("general").get(),
+                                       getter_Copies(name));
+        if (NS_FAILED(rv)) {
+          name = NS_LITERAL_STRING("General Notification");
+        }
+      }
     }
+
+    return DispatchGrowlNotification(name, aImageUrl, aAlertTitle,
+                                     aAlertText, aAlertCookie, aAlertListener);
+  } else {
+    // Notification Center is easy: no image, no name.
+    return DispatchNCNotification(mNCDelegate, aAlertTitle, aAlertText,
+                                  aAlertCookie, aAlertListener);
   }
 
-  return DispatchNamedNotification(name, aImageUrl, aAlertTitle,
-                                   aAlertText, aAlertCookie, aAlertListener);
-
   NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 //// nsIObserver
 
 NS_IMETHODIMP
-nsGrowlAlertsService::Observe(nsISupports* aSubject, const char* aTopic,
+nsMacAlertsService::Observe(nsISupports* aSubject, const char* aTopic,
                               const PRUnichar* aData)
 {
   NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
 
-  if (!mDelegate)
+  if (!mGrowlDelegate && !mNCDelegate) {
     return NS_OK;
+  }
 
   if (strcmp(aTopic, DOM_WINDOW_DESTROYED_TOPIC) == 0) {
     nsCOMPtr<nsIDOMWindow> window(do_QueryInterface(aSubject));
-    if (window)
-      [mDelegate->delegate forgetObserversForWindow:window];
+    if (window) {
+      mGrowlDelegate ? [mGrowlDelegate->mDelegate forgetObserversForWindow:window]
+                     : [mNCDelegate->mDelegate forgetObserversForWindow:window];
+    }
   }
   else if (strcmp(aTopic, "profile-before-change") == 0) {
-    [mDelegate->delegate forgetObservers];
+    mGrowlDelegate ? [mGrowlDelegate->mDelegate forgetObservers]
+                   : [mNCDelegate->mDelegate forgetObservers];
   }
 
   return NS_OK;
 
   NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
 }
new file mode 100644
--- /dev/null
+++ b/toolkit/components/alerts/mac/nsNotificationCenterCompat.h
@@ -0,0 +1,54 @@
+/* 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/. */
+
+#ifndef NS_NOTIFICATION_CENTER_COMPAT_H
+#define NS_NOTIFICATION_CENTER_COMPAT_H
+
+#import <Cocoa/Cocoa.h>
+
+#if !defined(MAC_OS_X_VERSION_10_8) || (MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_8)
+@protocol NSUserNotificationCenterDelegate
+@end
+
+static NSString * const NSUserNotificationDefaultSoundName = @"DefaultSoundName";
+
+enum {
+  NSUserNotificationActivationTypeNone = 0,
+  NSUserNotificationActivationTypeContentsClicked = 1,
+  NSUserNotificationActivationTypeActionButtonClicked = 2
+};
+typedef NSInteger NSUserNotificationActivationType;
+#endif
+
+@protocol FakeNSUserNotification <NSObject>
+@property (copy) NSString* title;
+@property (copy) NSString* subtitle;
+@property (copy) NSString* informativeText;
+@property (copy) NSString* actionButtonTitle;
+@property (copy) NSDictionary* userInfo;
+@property (copy) NSDate* deliveryDate;
+@property (copy) NSTimeZone* deliveryTimeZone;
+@property (copy) NSDateComponents* deliveryRepeatInterval;
+@property (readonly) NSDate* actualDeliveryDate;
+@property (readonly, getter=isPresented) BOOL presented;
+@property (readonly, getter=isRemote) BOOL remote;
+@property (copy) NSString* soundName;
+@property BOOL hasActionButton;
+@property (readonly) NSUserNotificationActivationType activationType;
+@property (copy) NSString *otherButtonTitle;
+@end
+
+@protocol FakeNSUserNotificationCenter <NSObject>
++ (id<FakeNSUserNotificationCenter>)defaultUserNotificationCenter;
+@property (assign) id <NSUserNotificationCenterDelegate> delegate;
+@property (copy) NSArray *scheduledNotifications;
+- (void)scheduleNotification:(id<FakeNSUserNotification>)notification;
+- (void)removeScheduledNotification:(id<FakeNSUserNotification>)notification;
+@property (readonly) NSArray *deliveredNotifications;
+- (void)deliverNotification:(id<FakeNSUserNotification>)notification;
+- (void)removeDeliveredNotification:(id<FakeNSUserNotification>)notification;
+- (void)removeAllDeliveredNotifications;
+@end
+
+#endif