1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/widget/cocoa/OSXNotificationCenter.mm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,428 @@ 1.4 +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ 1.5 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.7 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.8 + 1.9 +#include "OSXNotificationCenter.h" 1.10 +#import <AppKit/AppKit.h> 1.11 +#include "imgIRequest.h" 1.12 +#include "imgIContainer.h" 1.13 +#include "nsNetUtil.h" 1.14 +#include "imgLoader.h" 1.15 +#import "nsCocoaUtils.h" 1.16 +#include "nsObjCExceptions.h" 1.17 +#include "nsString.h" 1.18 +#include "nsCOMPtr.h" 1.19 +#include "nsIObserver.h" 1.20 +#include "imgRequestProxy.h" 1.21 + 1.22 +using namespace mozilla; 1.23 + 1.24 +#if !defined(MAC_OS_X_VERSION_10_8) || (MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_8) 1.25 +@protocol NSUserNotificationCenterDelegate 1.26 +@end 1.27 +static NSString * const NSUserNotificationDefaultSoundName = @"DefaultSoundName"; 1.28 +enum { 1.29 + NSUserNotificationActivationTypeNone = 0, 1.30 + NSUserNotificationActivationTypeContentsClicked = 1, 1.31 + NSUserNotificationActivationTypeActionButtonClicked = 2 1.32 +}; 1.33 +typedef NSInteger NSUserNotificationActivationType; 1.34 +#endif 1.35 + 1.36 +@protocol FakeNSUserNotification <NSObject> 1.37 +@property (copy) NSString* title; 1.38 +@property (copy) NSString* subtitle; 1.39 +@property (copy) NSString* informativeText; 1.40 +@property (copy) NSString* actionButtonTitle; 1.41 +@property (copy) NSDictionary* userInfo; 1.42 +@property (copy) NSDate* deliveryDate; 1.43 +@property (copy) NSTimeZone* deliveryTimeZone; 1.44 +@property (copy) NSDateComponents* deliveryRepeatInterval; 1.45 +@property (readonly) NSDate* actualDeliveryDate; 1.46 +@property (readonly, getter=isPresented) BOOL presented; 1.47 +@property (readonly, getter=isRemote) BOOL remote; 1.48 +@property (copy) NSString* soundName; 1.49 +@property BOOL hasActionButton; 1.50 +@property (readonly) NSUserNotificationActivationType activationType; 1.51 +@property (copy) NSString *otherButtonTitle; 1.52 +@property (copy) NSImage *contentImage; 1.53 +@end 1.54 + 1.55 +@protocol FakeNSUserNotificationCenter <NSObject> 1.56 ++ (id<FakeNSUserNotificationCenter>)defaultUserNotificationCenter; 1.57 +@property (assign) id <NSUserNotificationCenterDelegate> delegate; 1.58 +@property (copy) NSArray *scheduledNotifications; 1.59 +- (void)scheduleNotification:(id<FakeNSUserNotification>)notification; 1.60 +- (void)removeScheduledNotification:(id<FakeNSUserNotification>)notification; 1.61 +@property (readonly) NSArray *deliveredNotifications; 1.62 +- (void)deliverNotification:(id<FakeNSUserNotification>)notification; 1.63 +- (void)removeDeliveredNotification:(id<FakeNSUserNotification>)notification; 1.64 +- (void)removeAllDeliveredNotifications; 1.65 +- (void)_removeAllDisplayedNotifications; 1.66 +- (void)_removeDisplayedNotification:(id<FakeNSUserNotification>)notification; 1.67 +@end 1.68 + 1.69 +@interface mozNotificationCenterDelegate : NSObject <NSUserNotificationCenterDelegate> 1.70 +{ 1.71 + OSXNotificationCenter *mOSXNC; 1.72 +} 1.73 + - (id)initWithOSXNC:(OSXNotificationCenter*)osxnc; 1.74 +@end 1.75 + 1.76 +@implementation mozNotificationCenterDelegate 1.77 + 1.78 +- (id)initWithOSXNC:(OSXNotificationCenter*)osxnc 1.79 +{ 1.80 + [super init]; 1.81 + // We should *never* outlive this OSXNotificationCenter. 1.82 + mOSXNC = osxnc; 1.83 + return self; 1.84 +} 1.85 + 1.86 +- (void)userNotificationCenter:(id<FakeNSUserNotificationCenter>)center 1.87 + didDeliverNotification:(id<FakeNSUserNotification>)notification 1.88 +{ 1.89 + 1.90 +} 1.91 + 1.92 +- (void)userNotificationCenter:(id<FakeNSUserNotificationCenter>)center 1.93 + didActivateNotification:(id<FakeNSUserNotification>)notification 1.94 +{ 1.95 + mOSXNC->OnClick([[notification userInfo] valueForKey:@"name"]); 1.96 +} 1.97 + 1.98 +- (BOOL)userNotificationCenter:(id<FakeNSUserNotificationCenter>)center 1.99 + shouldPresentNotification:(id<FakeNSUserNotification>)notification 1.100 +{ 1.101 + return YES; 1.102 +} 1.103 + 1.104 +// This is an undocumented method that we need for parity with Safari. 1.105 +// Apple bug #15440664. 1.106 +- (void)userNotificationCenter:(id<FakeNSUserNotificationCenter>)center 1.107 + didRemoveDeliveredNotifications:(NSArray *)notifications 1.108 +{ 1.109 + for (id<FakeNSUserNotification> notification in notifications) { 1.110 + NSString *name = [[notification userInfo] valueForKey:@"name"]; 1.111 + mOSXNC->CloseAlertCocoaString(name); 1.112 + } 1.113 +} 1.114 + 1.115 +@end 1.116 + 1.117 +namespace mozilla { 1.118 + 1.119 +class OSXNotificationInfo : public RefCounted<OSXNotificationInfo> { 1.120 +public: 1.121 + MOZ_DECLARE_REFCOUNTED_TYPENAME(OSXNotificationInfo) 1.122 + OSXNotificationInfo(NSString *name, nsIObserver *observer, 1.123 + const nsAString & alertCookie); 1.124 + ~OSXNotificationInfo(); 1.125 + 1.126 + NSString *mName; 1.127 + nsCOMPtr<nsIObserver> mObserver; 1.128 + nsString mCookie; 1.129 + nsRefPtr<imgRequestProxy> mIconRequest; 1.130 + id<FakeNSUserNotification> mPendingNotifiction; 1.131 + nsCOMPtr<nsITimer> mIconTimeoutTimer; 1.132 +}; 1.133 + 1.134 +OSXNotificationInfo::OSXNotificationInfo(NSString *name, nsIObserver *observer, 1.135 + const nsAString & alertCookie) 1.136 +{ 1.137 + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; 1.138 + 1.139 + NS_ASSERTION(name, "Cannot create OSXNotificationInfo without a name!"); 1.140 + mName = [name retain]; 1.141 + mObserver = observer; 1.142 + mCookie = alertCookie; 1.143 + mPendingNotifiction = nil; 1.144 + 1.145 + NS_OBJC_END_TRY_ABORT_BLOCK; 1.146 +} 1.147 + 1.148 +OSXNotificationInfo::~OSXNotificationInfo() 1.149 +{ 1.150 + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; 1.151 + 1.152 + [mName release]; 1.153 + [mPendingNotifiction release]; 1.154 + 1.155 + NS_OBJC_END_TRY_ABORT_BLOCK; 1.156 +} 1.157 + 1.158 +static id<FakeNSUserNotificationCenter> GetNotificationCenter() { 1.159 + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; 1.160 + 1.161 + Class c = NSClassFromString(@"NSUserNotificationCenter"); 1.162 + return [c performSelector:@selector(defaultUserNotificationCenter)]; 1.163 + 1.164 + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; 1.165 +} 1.166 + 1.167 +OSXNotificationCenter::OSXNotificationCenter() 1.168 +{ 1.169 + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; 1.170 + 1.171 + mDelegate = [[mozNotificationCenterDelegate alloc] initWithOSXNC:this]; 1.172 + GetNotificationCenter().delegate = mDelegate; 1.173 + 1.174 + NS_OBJC_END_TRY_ABORT_BLOCK; 1.175 +} 1.176 + 1.177 +OSXNotificationCenter::~OSXNotificationCenter() 1.178 +{ 1.179 + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; 1.180 + 1.181 + [GetNotificationCenter() removeAllDeliveredNotifications]; 1.182 + [mDelegate release]; 1.183 + 1.184 + NS_OBJC_END_TRY_ABORT_BLOCK; 1.185 +} 1.186 + 1.187 +NS_IMPL_ISUPPORTS(OSXNotificationCenter, nsIAlertsService, imgINotificationObserver, nsITimerCallback) 1.188 + 1.189 +nsresult OSXNotificationCenter::Init() 1.190 +{ 1.191 + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; 1.192 + 1.193 + return (!!NSClassFromString(@"NSUserNotification")) ? NS_OK : NS_ERROR_FAILURE; 1.194 + 1.195 + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; 1.196 +} 1.197 + 1.198 +NS_IMETHODIMP 1.199 +OSXNotificationCenter::ShowAlertNotification(const nsAString & aImageUrl, const nsAString & aAlertTitle, 1.200 + const nsAString & aAlertText, bool aAlertTextClickable, 1.201 + const nsAString & aAlertCookie, 1.202 + nsIObserver * aAlertListener, 1.203 + const nsAString & aAlertName, 1.204 + const nsAString & aBidi, 1.205 + const nsAString & aLang, 1.206 + nsIPrincipal * aPrincipal) 1.207 +{ 1.208 + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; 1.209 + 1.210 + Class unClass = NSClassFromString(@"NSUserNotification"); 1.211 + id<FakeNSUserNotification> notification = [[unClass alloc] init]; 1.212 + notification.title = [NSString stringWithCharacters:(const unichar *)aAlertTitle.BeginReading() 1.213 + length:aAlertTitle.Length()]; 1.214 + notification.informativeText = [NSString stringWithCharacters:(const unichar *)aAlertText.BeginReading() 1.215 + length:aAlertText.Length()]; 1.216 + notification.soundName = NSUserNotificationDefaultSoundName; 1.217 + notification.hasActionButton = NO; 1.218 + NSString *alertName = [NSString stringWithCharacters:(const unichar *)aAlertName.BeginReading() length:aAlertName.Length()]; 1.219 + if (!alertName) { 1.220 + return NS_ERROR_FAILURE; 1.221 + } 1.222 + notification.userInfo = [NSDictionary dictionaryWithObjects:[NSArray arrayWithObjects:alertName, nil] 1.223 + forKeys:[NSArray arrayWithObjects:@"name", nil]]; 1.224 + 1.225 + OSXNotificationInfo *osxni = new OSXNotificationInfo(alertName, aAlertListener, aAlertCookie); 1.226 + 1.227 + // Show the notification without waiting for an image if there is no icon URL or 1.228 + // notification icons are not supported on this version of OS X. 1.229 + if (aImageUrl.IsEmpty() || ![unClass instancesRespondToSelector:@selector(setContentImage:)]) { 1.230 + CloseAlertCocoaString(alertName); 1.231 + mActiveAlerts.AppendElement(osxni); 1.232 + [GetNotificationCenter() deliverNotification:notification]; 1.233 + [notification release]; 1.234 + if (aAlertListener) { 1.235 + aAlertListener->Observe(nullptr, "alertshow", PromiseFlatString(aAlertCookie).get()); 1.236 + } 1.237 + } else { 1.238 + mPendingAlerts.AppendElement(osxni); 1.239 + osxni->mPendingNotifiction = notification; 1.240 + nsRefPtr<imgLoader> il = imgLoader::GetInstance(); 1.241 + if (il) { 1.242 + nsCOMPtr<nsIURI> imageUri; 1.243 + NS_NewURI(getter_AddRefs(imageUri), aImageUrl); 1.244 + if (imageUri) { 1.245 + nsresult rv = il->LoadImage(imageUri, nullptr, nullptr, aPrincipal, nullptr, 1.246 + this, nullptr, nsIRequest::LOAD_NORMAL, nullptr, 1.247 + nullptr, EmptyString(), 1.248 + getter_AddRefs(osxni->mIconRequest)); 1.249 + if (NS_SUCCEEDED(rv)) { 1.250 + // Set a timer for six seconds. If we don't have an icon by the time this 1.251 + // goes off then we go ahead without an icon. 1.252 + nsCOMPtr<nsITimer> timer = do_CreateInstance(NS_TIMER_CONTRACTID); 1.253 + osxni->mIconTimeoutTimer = timer; 1.254 + timer->InitWithCallback(this, 6000, nsITimer::TYPE_ONE_SHOT); 1.255 + return NS_OK; 1.256 + } 1.257 + } 1.258 + } 1.259 + ShowPendingNotification(osxni); 1.260 + } 1.261 + 1.262 + return NS_OK; 1.263 + 1.264 + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; 1.265 +} 1.266 + 1.267 +NS_IMETHODIMP 1.268 +OSXNotificationCenter::CloseAlert(const nsAString& aAlertName, 1.269 + nsIPrincipal* aPrincipal) 1.270 +{ 1.271 + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; 1.272 + 1.273 + NSString *alertName = [NSString stringWithCharacters:(const unichar *)aAlertName.BeginReading() length:aAlertName.Length()]; 1.274 + CloseAlertCocoaString(alertName); 1.275 + return NS_OK; 1.276 + 1.277 + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; 1.278 +} 1.279 + 1.280 +void 1.281 +OSXNotificationCenter::CloseAlertCocoaString(NSString *aAlertName) 1.282 +{ 1.283 + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; 1.284 + 1.285 + if (!aAlertName) { 1.286 + return; // Can't do anything without a name 1.287 + } 1.288 + 1.289 + NSArray *notifications = [GetNotificationCenter() deliveredNotifications]; 1.290 + for (id<FakeNSUserNotification> notification in notifications) { 1.291 + NSString *name = [[notification userInfo] valueForKey:@"name"]; 1.292 + if ([name isEqualToString:aAlertName]) { 1.293 + [GetNotificationCenter() removeDeliveredNotification:notification]; 1.294 + [GetNotificationCenter() _removeDisplayedNotification:notification]; 1.295 + break; 1.296 + } 1.297 + } 1.298 + 1.299 + for (unsigned int i = 0; i < mActiveAlerts.Length(); i++) { 1.300 + OSXNotificationInfo *osxni = mActiveAlerts[i]; 1.301 + if ([aAlertName isEqualToString:osxni->mName]) { 1.302 + if (osxni->mObserver) { 1.303 + osxni->mObserver->Observe(nullptr, "alertfinished", osxni->mCookie.get()); 1.304 + } 1.305 + mActiveAlerts.RemoveElementAt(i); 1.306 + break; 1.307 + } 1.308 + } 1.309 + 1.310 + NS_OBJC_END_TRY_ABORT_BLOCK; 1.311 +} 1.312 + 1.313 +void 1.314 +OSXNotificationCenter::OnClick(NSString *aAlertName) 1.315 +{ 1.316 + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; 1.317 + 1.318 + if (!aAlertName) { 1.319 + return; // Can't do anything without a name 1.320 + } 1.321 + 1.322 + for (unsigned int i = 0; i < mActiveAlerts.Length(); i++) { 1.323 + OSXNotificationInfo *osxni = mActiveAlerts[i]; 1.324 + if ([aAlertName isEqualToString:osxni->mName]) { 1.325 + if (osxni->mObserver) { 1.326 + osxni->mObserver->Observe(nullptr, "alertclickcallback", osxni->mCookie.get()); 1.327 + } 1.328 + return; 1.329 + } 1.330 + } 1.331 + 1.332 + NS_OBJC_END_TRY_ABORT_BLOCK; 1.333 +} 1.334 + 1.335 +void 1.336 +OSXNotificationCenter::ShowPendingNotification(OSXNotificationInfo *osxni) 1.337 +{ 1.338 + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; 1.339 + 1.340 + if (osxni->mIconTimeoutTimer) { 1.341 + osxni->mIconTimeoutTimer->Cancel(); 1.342 + osxni->mIconTimeoutTimer = nullptr; 1.343 + } 1.344 + 1.345 + if (osxni->mIconRequest) { 1.346 + osxni->mIconRequest->Cancel(NS_BINDING_ABORTED); 1.347 + osxni->mIconRequest = nullptr; 1.348 + } 1.349 + 1.350 + CloseAlertCocoaString(osxni->mName); 1.351 + 1.352 + for (unsigned int i = 0; i < mPendingAlerts.Length(); i++) { 1.353 + if (mPendingAlerts[i] == osxni) { 1.354 + mActiveAlerts.AppendElement(osxni); 1.355 + mPendingAlerts.RemoveElementAt(i); 1.356 + break; 1.357 + } 1.358 + } 1.359 + 1.360 + [GetNotificationCenter() deliverNotification:osxni->mPendingNotifiction]; 1.361 + 1.362 + if (osxni->mObserver) { 1.363 + osxni->mObserver->Observe(nullptr, "alertshow", osxni->mCookie.get()); 1.364 + } 1.365 + 1.366 + [osxni->mPendingNotifiction release]; 1.367 + osxni->mPendingNotifiction = nil; 1.368 + 1.369 + NS_OBJC_END_TRY_ABORT_BLOCK; 1.370 +} 1.371 + 1.372 +NS_IMETHODIMP 1.373 +OSXNotificationCenter::Notify(imgIRequest *aRequest, int32_t aType, const nsIntRect* aData) 1.374 +{ 1.375 + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; 1.376 + 1.377 + if (aType == imgINotificationObserver::LOAD_COMPLETE) { 1.378 + OSXNotificationInfo *osxni = nullptr; 1.379 + for (unsigned int i = 0; i < mPendingAlerts.Length(); i++) { 1.380 + if (aRequest == mPendingAlerts[i]->mIconRequest) { 1.381 + osxni = mPendingAlerts[i]; 1.382 + break; 1.383 + } 1.384 + } 1.385 + if (!osxni || !osxni->mPendingNotifiction) { 1.386 + return NS_ERROR_FAILURE; 1.387 + } 1.388 + NSImage *cocoaImage = nil; 1.389 + uint32_t imgStatus = imgIRequest::STATUS_ERROR; 1.390 + nsresult rv = aRequest->GetImageStatus(&imgStatus); 1.391 + if (NS_SUCCEEDED(rv) && imgStatus != imgIRequest::STATUS_ERROR) { 1.392 + nsCOMPtr<imgIContainer> image; 1.393 + rv = aRequest->GetImage(getter_AddRefs(image)); 1.394 + if (NS_SUCCEEDED(rv)) { 1.395 + nsCocoaUtils::CreateNSImageFromImageContainer(image, imgIContainer::FRAME_FIRST, &cocoaImage, 1.0f); 1.396 + } 1.397 + } 1.398 + (osxni->mPendingNotifiction).contentImage = cocoaImage; 1.399 + [cocoaImage release]; 1.400 + ShowPendingNotification(osxni); 1.401 + } 1.402 + return NS_OK; 1.403 + 1.404 + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; 1.405 +} 1.406 + 1.407 +NS_IMETHODIMP 1.408 +OSXNotificationCenter::Notify(nsITimer *timer) 1.409 +{ 1.410 + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; 1.411 + 1.412 + if (!timer) { 1.413 + return NS_ERROR_FAILURE; 1.414 + } 1.415 + 1.416 + for (unsigned int i = 0; i < mPendingAlerts.Length(); i++) { 1.417 + OSXNotificationInfo *osxni = mPendingAlerts[i]; 1.418 + if (timer == osxni->mIconTimeoutTimer.get()) { 1.419 + osxni->mIconTimeoutTimer = nullptr; 1.420 + if (osxni->mPendingNotifiction) { 1.421 + ShowPendingNotification(osxni); 1.422 + break; 1.423 + } 1.424 + } 1.425 + } 1.426 + return NS_OK; 1.427 + 1.428 + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; 1.429 +} 1.430 + 1.431 +} // namespace mozilla