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 105089 67c5213dfe2b944d0c2d019f9a47cb7da74823a2
parent 105088 41a3cdf9206337399613463f3f008b24d764d1a4
child 105090 5253021171872eada9416ea7294a53588cec7408
push id55
push usershu@rfrn.org
push dateThu, 30 Aug 2012 01:33:09 +0000
reviewerssmichaud, dougt
bugs728106
milestone17.0a1
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