michael@0: /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: #include "OSXNotificationCenter.h" michael@0: #import michael@0: #include "imgIRequest.h" michael@0: #include "imgIContainer.h" michael@0: #include "nsNetUtil.h" michael@0: #include "imgLoader.h" michael@0: #import "nsCocoaUtils.h" michael@0: #include "nsObjCExceptions.h" michael@0: #include "nsString.h" michael@0: #include "nsCOMPtr.h" michael@0: #include "nsIObserver.h" michael@0: #include "imgRequestProxy.h" michael@0: michael@0: using namespace mozilla; michael@0: michael@0: #if !defined(MAC_OS_X_VERSION_10_8) || (MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_8) michael@0: @protocol NSUserNotificationCenterDelegate michael@0: @end michael@0: static NSString * const NSUserNotificationDefaultSoundName = @"DefaultSoundName"; michael@0: enum { michael@0: NSUserNotificationActivationTypeNone = 0, michael@0: NSUserNotificationActivationTypeContentsClicked = 1, michael@0: NSUserNotificationActivationTypeActionButtonClicked = 2 michael@0: }; michael@0: typedef NSInteger NSUserNotificationActivationType; michael@0: #endif michael@0: michael@0: @protocol FakeNSUserNotification michael@0: @property (copy) NSString* title; michael@0: @property (copy) NSString* subtitle; michael@0: @property (copy) NSString* informativeText; michael@0: @property (copy) NSString* actionButtonTitle; michael@0: @property (copy) NSDictionary* userInfo; michael@0: @property (copy) NSDate* deliveryDate; michael@0: @property (copy) NSTimeZone* deliveryTimeZone; michael@0: @property (copy) NSDateComponents* deliveryRepeatInterval; michael@0: @property (readonly) NSDate* actualDeliveryDate; michael@0: @property (readonly, getter=isPresented) BOOL presented; michael@0: @property (readonly, getter=isRemote) BOOL remote; michael@0: @property (copy) NSString* soundName; michael@0: @property BOOL hasActionButton; michael@0: @property (readonly) NSUserNotificationActivationType activationType; michael@0: @property (copy) NSString *otherButtonTitle; michael@0: @property (copy) NSImage *contentImage; michael@0: @end michael@0: michael@0: @protocol FakeNSUserNotificationCenter michael@0: + (id)defaultUserNotificationCenter; michael@0: @property (assign) id delegate; michael@0: @property (copy) NSArray *scheduledNotifications; michael@0: - (void)scheduleNotification:(id)notification; michael@0: - (void)removeScheduledNotification:(id)notification; michael@0: @property (readonly) NSArray *deliveredNotifications; michael@0: - (void)deliverNotification:(id)notification; michael@0: - (void)removeDeliveredNotification:(id)notification; michael@0: - (void)removeAllDeliveredNotifications; michael@0: - (void)_removeAllDisplayedNotifications; michael@0: - (void)_removeDisplayedNotification:(id)notification; michael@0: @end michael@0: michael@0: @interface mozNotificationCenterDelegate : NSObject michael@0: { michael@0: OSXNotificationCenter *mOSXNC; michael@0: } michael@0: - (id)initWithOSXNC:(OSXNotificationCenter*)osxnc; michael@0: @end michael@0: michael@0: @implementation mozNotificationCenterDelegate michael@0: michael@0: - (id)initWithOSXNC:(OSXNotificationCenter*)osxnc michael@0: { michael@0: [super init]; michael@0: // We should *never* outlive this OSXNotificationCenter. michael@0: mOSXNC = osxnc; michael@0: return self; michael@0: } michael@0: michael@0: - (void)userNotificationCenter:(id)center michael@0: didDeliverNotification:(id)notification michael@0: { michael@0: michael@0: } michael@0: michael@0: - (void)userNotificationCenter:(id)center michael@0: didActivateNotification:(id)notification michael@0: { michael@0: mOSXNC->OnClick([[notification userInfo] valueForKey:@"name"]); michael@0: } michael@0: michael@0: - (BOOL)userNotificationCenter:(id)center michael@0: shouldPresentNotification:(id)notification michael@0: { michael@0: return YES; michael@0: } michael@0: michael@0: // This is an undocumented method that we need for parity with Safari. michael@0: // Apple bug #15440664. michael@0: - (void)userNotificationCenter:(id)center michael@0: didRemoveDeliveredNotifications:(NSArray *)notifications michael@0: { michael@0: for (id notification in notifications) { michael@0: NSString *name = [[notification userInfo] valueForKey:@"name"]; michael@0: mOSXNC->CloseAlertCocoaString(name); michael@0: } michael@0: } michael@0: michael@0: @end michael@0: michael@0: namespace mozilla { michael@0: michael@0: class OSXNotificationInfo : public RefCounted { michael@0: public: michael@0: MOZ_DECLARE_REFCOUNTED_TYPENAME(OSXNotificationInfo) michael@0: OSXNotificationInfo(NSString *name, nsIObserver *observer, michael@0: const nsAString & alertCookie); michael@0: ~OSXNotificationInfo(); michael@0: michael@0: NSString *mName; michael@0: nsCOMPtr mObserver; michael@0: nsString mCookie; michael@0: nsRefPtr mIconRequest; michael@0: id mPendingNotifiction; michael@0: nsCOMPtr mIconTimeoutTimer; michael@0: }; michael@0: michael@0: OSXNotificationInfo::OSXNotificationInfo(NSString *name, nsIObserver *observer, michael@0: const nsAString & alertCookie) michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK; michael@0: michael@0: NS_ASSERTION(name, "Cannot create OSXNotificationInfo without a name!"); michael@0: mName = [name retain]; michael@0: mObserver = observer; michael@0: mCookie = alertCookie; michael@0: mPendingNotifiction = nil; michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK; michael@0: } michael@0: michael@0: OSXNotificationInfo::~OSXNotificationInfo() michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK; michael@0: michael@0: [mName release]; michael@0: [mPendingNotifiction release]; michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK; michael@0: } michael@0: michael@0: static id GetNotificationCenter() { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; michael@0: michael@0: Class c = NSClassFromString(@"NSUserNotificationCenter"); michael@0: return [c performSelector:@selector(defaultUserNotificationCenter)]; michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK_NIL; michael@0: } michael@0: michael@0: OSXNotificationCenter::OSXNotificationCenter() michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK; michael@0: michael@0: mDelegate = [[mozNotificationCenterDelegate alloc] initWithOSXNC:this]; michael@0: GetNotificationCenter().delegate = mDelegate; michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK; michael@0: } michael@0: michael@0: OSXNotificationCenter::~OSXNotificationCenter() michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK; michael@0: michael@0: [GetNotificationCenter() removeAllDeliveredNotifications]; michael@0: [mDelegate release]; michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK; michael@0: } michael@0: michael@0: NS_IMPL_ISUPPORTS(OSXNotificationCenter, nsIAlertsService, imgINotificationObserver, nsITimerCallback) michael@0: michael@0: nsresult OSXNotificationCenter::Init() michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; michael@0: michael@0: return (!!NSClassFromString(@"NSUserNotification")) ? NS_OK : NS_ERROR_FAILURE; michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: OSXNotificationCenter::ShowAlertNotification(const nsAString & aImageUrl, const nsAString & aAlertTitle, michael@0: const nsAString & aAlertText, bool aAlertTextClickable, michael@0: const nsAString & aAlertCookie, michael@0: nsIObserver * aAlertListener, michael@0: const nsAString & aAlertName, michael@0: const nsAString & aBidi, michael@0: const nsAString & aLang, michael@0: nsIPrincipal * aPrincipal) michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; michael@0: michael@0: Class unClass = NSClassFromString(@"NSUserNotification"); michael@0: id notification = [[unClass alloc] init]; michael@0: notification.title = [NSString stringWithCharacters:(const unichar *)aAlertTitle.BeginReading() michael@0: length:aAlertTitle.Length()]; michael@0: notification.informativeText = [NSString stringWithCharacters:(const unichar *)aAlertText.BeginReading() michael@0: length:aAlertText.Length()]; michael@0: notification.soundName = NSUserNotificationDefaultSoundName; michael@0: notification.hasActionButton = NO; michael@0: NSString *alertName = [NSString stringWithCharacters:(const unichar *)aAlertName.BeginReading() length:aAlertName.Length()]; michael@0: if (!alertName) { michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: notification.userInfo = [NSDictionary dictionaryWithObjects:[NSArray arrayWithObjects:alertName, nil] michael@0: forKeys:[NSArray arrayWithObjects:@"name", nil]]; michael@0: michael@0: OSXNotificationInfo *osxni = new OSXNotificationInfo(alertName, aAlertListener, aAlertCookie); michael@0: michael@0: // Show the notification without waiting for an image if there is no icon URL or michael@0: // notification icons are not supported on this version of OS X. michael@0: if (aImageUrl.IsEmpty() || ![unClass instancesRespondToSelector:@selector(setContentImage:)]) { michael@0: CloseAlertCocoaString(alertName); michael@0: mActiveAlerts.AppendElement(osxni); michael@0: [GetNotificationCenter() deliverNotification:notification]; michael@0: [notification release]; michael@0: if (aAlertListener) { michael@0: aAlertListener->Observe(nullptr, "alertshow", PromiseFlatString(aAlertCookie).get()); michael@0: } michael@0: } else { michael@0: mPendingAlerts.AppendElement(osxni); michael@0: osxni->mPendingNotifiction = notification; michael@0: nsRefPtr il = imgLoader::GetInstance(); michael@0: if (il) { michael@0: nsCOMPtr imageUri; michael@0: NS_NewURI(getter_AddRefs(imageUri), aImageUrl); michael@0: if (imageUri) { michael@0: nsresult rv = il->LoadImage(imageUri, nullptr, nullptr, aPrincipal, nullptr, michael@0: this, nullptr, nsIRequest::LOAD_NORMAL, nullptr, michael@0: nullptr, EmptyString(), michael@0: getter_AddRefs(osxni->mIconRequest)); michael@0: if (NS_SUCCEEDED(rv)) { michael@0: // Set a timer for six seconds. If we don't have an icon by the time this michael@0: // goes off then we go ahead without an icon. michael@0: nsCOMPtr timer = do_CreateInstance(NS_TIMER_CONTRACTID); michael@0: osxni->mIconTimeoutTimer = timer; michael@0: timer->InitWithCallback(this, 6000, nsITimer::TYPE_ONE_SHOT); michael@0: return NS_OK; michael@0: } michael@0: } michael@0: } michael@0: ShowPendingNotification(osxni); michael@0: } michael@0: michael@0: return NS_OK; michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: OSXNotificationCenter::CloseAlert(const nsAString& aAlertName, michael@0: nsIPrincipal* aPrincipal) michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; michael@0: michael@0: NSString *alertName = [NSString stringWithCharacters:(const unichar *)aAlertName.BeginReading() length:aAlertName.Length()]; michael@0: CloseAlertCocoaString(alertName); michael@0: return NS_OK; michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; michael@0: } michael@0: michael@0: void michael@0: OSXNotificationCenter::CloseAlertCocoaString(NSString *aAlertName) michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK; michael@0: michael@0: if (!aAlertName) { michael@0: return; // Can't do anything without a name michael@0: } michael@0: michael@0: NSArray *notifications = [GetNotificationCenter() deliveredNotifications]; michael@0: for (id notification in notifications) { michael@0: NSString *name = [[notification userInfo] valueForKey:@"name"]; michael@0: if ([name isEqualToString:aAlertName]) { michael@0: [GetNotificationCenter() removeDeliveredNotification:notification]; michael@0: [GetNotificationCenter() _removeDisplayedNotification:notification]; michael@0: break; michael@0: } michael@0: } michael@0: michael@0: for (unsigned int i = 0; i < mActiveAlerts.Length(); i++) { michael@0: OSXNotificationInfo *osxni = mActiveAlerts[i]; michael@0: if ([aAlertName isEqualToString:osxni->mName]) { michael@0: if (osxni->mObserver) { michael@0: osxni->mObserver->Observe(nullptr, "alertfinished", osxni->mCookie.get()); michael@0: } michael@0: mActiveAlerts.RemoveElementAt(i); michael@0: break; michael@0: } michael@0: } michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK; michael@0: } michael@0: michael@0: void michael@0: OSXNotificationCenter::OnClick(NSString *aAlertName) michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK; michael@0: michael@0: if (!aAlertName) { michael@0: return; // Can't do anything without a name michael@0: } michael@0: michael@0: for (unsigned int i = 0; i < mActiveAlerts.Length(); i++) { michael@0: OSXNotificationInfo *osxni = mActiveAlerts[i]; michael@0: if ([aAlertName isEqualToString:osxni->mName]) { michael@0: if (osxni->mObserver) { michael@0: osxni->mObserver->Observe(nullptr, "alertclickcallback", osxni->mCookie.get()); michael@0: } michael@0: return; michael@0: } michael@0: } michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK; michael@0: } michael@0: michael@0: void michael@0: OSXNotificationCenter::ShowPendingNotification(OSXNotificationInfo *osxni) michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK; michael@0: michael@0: if (osxni->mIconTimeoutTimer) { michael@0: osxni->mIconTimeoutTimer->Cancel(); michael@0: osxni->mIconTimeoutTimer = nullptr; michael@0: } michael@0: michael@0: if (osxni->mIconRequest) { michael@0: osxni->mIconRequest->Cancel(NS_BINDING_ABORTED); michael@0: osxni->mIconRequest = nullptr; michael@0: } michael@0: michael@0: CloseAlertCocoaString(osxni->mName); michael@0: michael@0: for (unsigned int i = 0; i < mPendingAlerts.Length(); i++) { michael@0: if (mPendingAlerts[i] == osxni) { michael@0: mActiveAlerts.AppendElement(osxni); michael@0: mPendingAlerts.RemoveElementAt(i); michael@0: break; michael@0: } michael@0: } michael@0: michael@0: [GetNotificationCenter() deliverNotification:osxni->mPendingNotifiction]; michael@0: michael@0: if (osxni->mObserver) { michael@0: osxni->mObserver->Observe(nullptr, "alertshow", osxni->mCookie.get()); michael@0: } michael@0: michael@0: [osxni->mPendingNotifiction release]; michael@0: osxni->mPendingNotifiction = nil; michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: OSXNotificationCenter::Notify(imgIRequest *aRequest, int32_t aType, const nsIntRect* aData) michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; michael@0: michael@0: if (aType == imgINotificationObserver::LOAD_COMPLETE) { michael@0: OSXNotificationInfo *osxni = nullptr; michael@0: for (unsigned int i = 0; i < mPendingAlerts.Length(); i++) { michael@0: if (aRequest == mPendingAlerts[i]->mIconRequest) { michael@0: osxni = mPendingAlerts[i]; michael@0: break; michael@0: } michael@0: } michael@0: if (!osxni || !osxni->mPendingNotifiction) { michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: NSImage *cocoaImage = nil; michael@0: uint32_t imgStatus = imgIRequest::STATUS_ERROR; michael@0: nsresult rv = aRequest->GetImageStatus(&imgStatus); michael@0: if (NS_SUCCEEDED(rv) && imgStatus != imgIRequest::STATUS_ERROR) { michael@0: nsCOMPtr image; michael@0: rv = aRequest->GetImage(getter_AddRefs(image)); michael@0: if (NS_SUCCEEDED(rv)) { michael@0: nsCocoaUtils::CreateNSImageFromImageContainer(image, imgIContainer::FRAME_FIRST, &cocoaImage, 1.0f); michael@0: } michael@0: } michael@0: (osxni->mPendingNotifiction).contentImage = cocoaImage; michael@0: [cocoaImage release]; michael@0: ShowPendingNotification(osxni); michael@0: } michael@0: return NS_OK; michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: OSXNotificationCenter::Notify(nsITimer *timer) michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; michael@0: michael@0: if (!timer) { michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: michael@0: for (unsigned int i = 0; i < mPendingAlerts.Length(); i++) { michael@0: OSXNotificationInfo *osxni = mPendingAlerts[i]; michael@0: if (timer == osxni->mIconTimeoutTimer.get()) { michael@0: osxni->mIconTimeoutTimer = nullptr; michael@0: if (osxni->mPendingNotifiction) { michael@0: ShowPendingNotification(osxni); michael@0: break; michael@0: } michael@0: } michael@0: } michael@0: return NS_OK; michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; michael@0: } michael@0: michael@0: } // namespace mozilla