|
1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ |
|
2 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
3 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
5 |
|
6 #include "OSXNotificationCenter.h" |
|
7 #import <AppKit/AppKit.h> |
|
8 #include "imgIRequest.h" |
|
9 #include "imgIContainer.h" |
|
10 #include "nsNetUtil.h" |
|
11 #include "imgLoader.h" |
|
12 #import "nsCocoaUtils.h" |
|
13 #include "nsObjCExceptions.h" |
|
14 #include "nsString.h" |
|
15 #include "nsCOMPtr.h" |
|
16 #include "nsIObserver.h" |
|
17 #include "imgRequestProxy.h" |
|
18 |
|
19 using namespace mozilla; |
|
20 |
|
21 #if !defined(MAC_OS_X_VERSION_10_8) || (MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_8) |
|
22 @protocol NSUserNotificationCenterDelegate |
|
23 @end |
|
24 static NSString * const NSUserNotificationDefaultSoundName = @"DefaultSoundName"; |
|
25 enum { |
|
26 NSUserNotificationActivationTypeNone = 0, |
|
27 NSUserNotificationActivationTypeContentsClicked = 1, |
|
28 NSUserNotificationActivationTypeActionButtonClicked = 2 |
|
29 }; |
|
30 typedef NSInteger NSUserNotificationActivationType; |
|
31 #endif |
|
32 |
|
33 @protocol FakeNSUserNotification <NSObject> |
|
34 @property (copy) NSString* title; |
|
35 @property (copy) NSString* subtitle; |
|
36 @property (copy) NSString* informativeText; |
|
37 @property (copy) NSString* actionButtonTitle; |
|
38 @property (copy) NSDictionary* userInfo; |
|
39 @property (copy) NSDate* deliveryDate; |
|
40 @property (copy) NSTimeZone* deliveryTimeZone; |
|
41 @property (copy) NSDateComponents* deliveryRepeatInterval; |
|
42 @property (readonly) NSDate* actualDeliveryDate; |
|
43 @property (readonly, getter=isPresented) BOOL presented; |
|
44 @property (readonly, getter=isRemote) BOOL remote; |
|
45 @property (copy) NSString* soundName; |
|
46 @property BOOL hasActionButton; |
|
47 @property (readonly) NSUserNotificationActivationType activationType; |
|
48 @property (copy) NSString *otherButtonTitle; |
|
49 @property (copy) NSImage *contentImage; |
|
50 @end |
|
51 |
|
52 @protocol FakeNSUserNotificationCenter <NSObject> |
|
53 + (id<FakeNSUserNotificationCenter>)defaultUserNotificationCenter; |
|
54 @property (assign) id <NSUserNotificationCenterDelegate> delegate; |
|
55 @property (copy) NSArray *scheduledNotifications; |
|
56 - (void)scheduleNotification:(id<FakeNSUserNotification>)notification; |
|
57 - (void)removeScheduledNotification:(id<FakeNSUserNotification>)notification; |
|
58 @property (readonly) NSArray *deliveredNotifications; |
|
59 - (void)deliverNotification:(id<FakeNSUserNotification>)notification; |
|
60 - (void)removeDeliveredNotification:(id<FakeNSUserNotification>)notification; |
|
61 - (void)removeAllDeliveredNotifications; |
|
62 - (void)_removeAllDisplayedNotifications; |
|
63 - (void)_removeDisplayedNotification:(id<FakeNSUserNotification>)notification; |
|
64 @end |
|
65 |
|
66 @interface mozNotificationCenterDelegate : NSObject <NSUserNotificationCenterDelegate> |
|
67 { |
|
68 OSXNotificationCenter *mOSXNC; |
|
69 } |
|
70 - (id)initWithOSXNC:(OSXNotificationCenter*)osxnc; |
|
71 @end |
|
72 |
|
73 @implementation mozNotificationCenterDelegate |
|
74 |
|
75 - (id)initWithOSXNC:(OSXNotificationCenter*)osxnc |
|
76 { |
|
77 [super init]; |
|
78 // We should *never* outlive this OSXNotificationCenter. |
|
79 mOSXNC = osxnc; |
|
80 return self; |
|
81 } |
|
82 |
|
83 - (void)userNotificationCenter:(id<FakeNSUserNotificationCenter>)center |
|
84 didDeliverNotification:(id<FakeNSUserNotification>)notification |
|
85 { |
|
86 |
|
87 } |
|
88 |
|
89 - (void)userNotificationCenter:(id<FakeNSUserNotificationCenter>)center |
|
90 didActivateNotification:(id<FakeNSUserNotification>)notification |
|
91 { |
|
92 mOSXNC->OnClick([[notification userInfo] valueForKey:@"name"]); |
|
93 } |
|
94 |
|
95 - (BOOL)userNotificationCenter:(id<FakeNSUserNotificationCenter>)center |
|
96 shouldPresentNotification:(id<FakeNSUserNotification>)notification |
|
97 { |
|
98 return YES; |
|
99 } |
|
100 |
|
101 // This is an undocumented method that we need for parity with Safari. |
|
102 // Apple bug #15440664. |
|
103 - (void)userNotificationCenter:(id<FakeNSUserNotificationCenter>)center |
|
104 didRemoveDeliveredNotifications:(NSArray *)notifications |
|
105 { |
|
106 for (id<FakeNSUserNotification> notification in notifications) { |
|
107 NSString *name = [[notification userInfo] valueForKey:@"name"]; |
|
108 mOSXNC->CloseAlertCocoaString(name); |
|
109 } |
|
110 } |
|
111 |
|
112 @end |
|
113 |
|
114 namespace mozilla { |
|
115 |
|
116 class OSXNotificationInfo : public RefCounted<OSXNotificationInfo> { |
|
117 public: |
|
118 MOZ_DECLARE_REFCOUNTED_TYPENAME(OSXNotificationInfo) |
|
119 OSXNotificationInfo(NSString *name, nsIObserver *observer, |
|
120 const nsAString & alertCookie); |
|
121 ~OSXNotificationInfo(); |
|
122 |
|
123 NSString *mName; |
|
124 nsCOMPtr<nsIObserver> mObserver; |
|
125 nsString mCookie; |
|
126 nsRefPtr<imgRequestProxy> mIconRequest; |
|
127 id<FakeNSUserNotification> mPendingNotifiction; |
|
128 nsCOMPtr<nsITimer> mIconTimeoutTimer; |
|
129 }; |
|
130 |
|
131 OSXNotificationInfo::OSXNotificationInfo(NSString *name, nsIObserver *observer, |
|
132 const nsAString & alertCookie) |
|
133 { |
|
134 NS_OBJC_BEGIN_TRY_ABORT_BLOCK; |
|
135 |
|
136 NS_ASSERTION(name, "Cannot create OSXNotificationInfo without a name!"); |
|
137 mName = [name retain]; |
|
138 mObserver = observer; |
|
139 mCookie = alertCookie; |
|
140 mPendingNotifiction = nil; |
|
141 |
|
142 NS_OBJC_END_TRY_ABORT_BLOCK; |
|
143 } |
|
144 |
|
145 OSXNotificationInfo::~OSXNotificationInfo() |
|
146 { |
|
147 NS_OBJC_BEGIN_TRY_ABORT_BLOCK; |
|
148 |
|
149 [mName release]; |
|
150 [mPendingNotifiction release]; |
|
151 |
|
152 NS_OBJC_END_TRY_ABORT_BLOCK; |
|
153 } |
|
154 |
|
155 static id<FakeNSUserNotificationCenter> GetNotificationCenter() { |
|
156 NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; |
|
157 |
|
158 Class c = NSClassFromString(@"NSUserNotificationCenter"); |
|
159 return [c performSelector:@selector(defaultUserNotificationCenter)]; |
|
160 |
|
161 NS_OBJC_END_TRY_ABORT_BLOCK_NIL; |
|
162 } |
|
163 |
|
164 OSXNotificationCenter::OSXNotificationCenter() |
|
165 { |
|
166 NS_OBJC_BEGIN_TRY_ABORT_BLOCK; |
|
167 |
|
168 mDelegate = [[mozNotificationCenterDelegate alloc] initWithOSXNC:this]; |
|
169 GetNotificationCenter().delegate = mDelegate; |
|
170 |
|
171 NS_OBJC_END_TRY_ABORT_BLOCK; |
|
172 } |
|
173 |
|
174 OSXNotificationCenter::~OSXNotificationCenter() |
|
175 { |
|
176 NS_OBJC_BEGIN_TRY_ABORT_BLOCK; |
|
177 |
|
178 [GetNotificationCenter() removeAllDeliveredNotifications]; |
|
179 [mDelegate release]; |
|
180 |
|
181 NS_OBJC_END_TRY_ABORT_BLOCK; |
|
182 } |
|
183 |
|
184 NS_IMPL_ISUPPORTS(OSXNotificationCenter, nsIAlertsService, imgINotificationObserver, nsITimerCallback) |
|
185 |
|
186 nsresult OSXNotificationCenter::Init() |
|
187 { |
|
188 NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; |
|
189 |
|
190 return (!!NSClassFromString(@"NSUserNotification")) ? NS_OK : NS_ERROR_FAILURE; |
|
191 |
|
192 NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; |
|
193 } |
|
194 |
|
195 NS_IMETHODIMP |
|
196 OSXNotificationCenter::ShowAlertNotification(const nsAString & aImageUrl, const nsAString & aAlertTitle, |
|
197 const nsAString & aAlertText, bool aAlertTextClickable, |
|
198 const nsAString & aAlertCookie, |
|
199 nsIObserver * aAlertListener, |
|
200 const nsAString & aAlertName, |
|
201 const nsAString & aBidi, |
|
202 const nsAString & aLang, |
|
203 nsIPrincipal * aPrincipal) |
|
204 { |
|
205 NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; |
|
206 |
|
207 Class unClass = NSClassFromString(@"NSUserNotification"); |
|
208 id<FakeNSUserNotification> notification = [[unClass alloc] init]; |
|
209 notification.title = [NSString stringWithCharacters:(const unichar *)aAlertTitle.BeginReading() |
|
210 length:aAlertTitle.Length()]; |
|
211 notification.informativeText = [NSString stringWithCharacters:(const unichar *)aAlertText.BeginReading() |
|
212 length:aAlertText.Length()]; |
|
213 notification.soundName = NSUserNotificationDefaultSoundName; |
|
214 notification.hasActionButton = NO; |
|
215 NSString *alertName = [NSString stringWithCharacters:(const unichar *)aAlertName.BeginReading() length:aAlertName.Length()]; |
|
216 if (!alertName) { |
|
217 return NS_ERROR_FAILURE; |
|
218 } |
|
219 notification.userInfo = [NSDictionary dictionaryWithObjects:[NSArray arrayWithObjects:alertName, nil] |
|
220 forKeys:[NSArray arrayWithObjects:@"name", nil]]; |
|
221 |
|
222 OSXNotificationInfo *osxni = new OSXNotificationInfo(alertName, aAlertListener, aAlertCookie); |
|
223 |
|
224 // Show the notification without waiting for an image if there is no icon URL or |
|
225 // notification icons are not supported on this version of OS X. |
|
226 if (aImageUrl.IsEmpty() || ![unClass instancesRespondToSelector:@selector(setContentImage:)]) { |
|
227 CloseAlertCocoaString(alertName); |
|
228 mActiveAlerts.AppendElement(osxni); |
|
229 [GetNotificationCenter() deliverNotification:notification]; |
|
230 [notification release]; |
|
231 if (aAlertListener) { |
|
232 aAlertListener->Observe(nullptr, "alertshow", PromiseFlatString(aAlertCookie).get()); |
|
233 } |
|
234 } else { |
|
235 mPendingAlerts.AppendElement(osxni); |
|
236 osxni->mPendingNotifiction = notification; |
|
237 nsRefPtr<imgLoader> il = imgLoader::GetInstance(); |
|
238 if (il) { |
|
239 nsCOMPtr<nsIURI> imageUri; |
|
240 NS_NewURI(getter_AddRefs(imageUri), aImageUrl); |
|
241 if (imageUri) { |
|
242 nsresult rv = il->LoadImage(imageUri, nullptr, nullptr, aPrincipal, nullptr, |
|
243 this, nullptr, nsIRequest::LOAD_NORMAL, nullptr, |
|
244 nullptr, EmptyString(), |
|
245 getter_AddRefs(osxni->mIconRequest)); |
|
246 if (NS_SUCCEEDED(rv)) { |
|
247 // Set a timer for six seconds. If we don't have an icon by the time this |
|
248 // goes off then we go ahead without an icon. |
|
249 nsCOMPtr<nsITimer> timer = do_CreateInstance(NS_TIMER_CONTRACTID); |
|
250 osxni->mIconTimeoutTimer = timer; |
|
251 timer->InitWithCallback(this, 6000, nsITimer::TYPE_ONE_SHOT); |
|
252 return NS_OK; |
|
253 } |
|
254 } |
|
255 } |
|
256 ShowPendingNotification(osxni); |
|
257 } |
|
258 |
|
259 return NS_OK; |
|
260 |
|
261 NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; |
|
262 } |
|
263 |
|
264 NS_IMETHODIMP |
|
265 OSXNotificationCenter::CloseAlert(const nsAString& aAlertName, |
|
266 nsIPrincipal* aPrincipal) |
|
267 { |
|
268 NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; |
|
269 |
|
270 NSString *alertName = [NSString stringWithCharacters:(const unichar *)aAlertName.BeginReading() length:aAlertName.Length()]; |
|
271 CloseAlertCocoaString(alertName); |
|
272 return NS_OK; |
|
273 |
|
274 NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; |
|
275 } |
|
276 |
|
277 void |
|
278 OSXNotificationCenter::CloseAlertCocoaString(NSString *aAlertName) |
|
279 { |
|
280 NS_OBJC_BEGIN_TRY_ABORT_BLOCK; |
|
281 |
|
282 if (!aAlertName) { |
|
283 return; // Can't do anything without a name |
|
284 } |
|
285 |
|
286 NSArray *notifications = [GetNotificationCenter() deliveredNotifications]; |
|
287 for (id<FakeNSUserNotification> notification in notifications) { |
|
288 NSString *name = [[notification userInfo] valueForKey:@"name"]; |
|
289 if ([name isEqualToString:aAlertName]) { |
|
290 [GetNotificationCenter() removeDeliveredNotification:notification]; |
|
291 [GetNotificationCenter() _removeDisplayedNotification:notification]; |
|
292 break; |
|
293 } |
|
294 } |
|
295 |
|
296 for (unsigned int i = 0; i < mActiveAlerts.Length(); i++) { |
|
297 OSXNotificationInfo *osxni = mActiveAlerts[i]; |
|
298 if ([aAlertName isEqualToString:osxni->mName]) { |
|
299 if (osxni->mObserver) { |
|
300 osxni->mObserver->Observe(nullptr, "alertfinished", osxni->mCookie.get()); |
|
301 } |
|
302 mActiveAlerts.RemoveElementAt(i); |
|
303 break; |
|
304 } |
|
305 } |
|
306 |
|
307 NS_OBJC_END_TRY_ABORT_BLOCK; |
|
308 } |
|
309 |
|
310 void |
|
311 OSXNotificationCenter::OnClick(NSString *aAlertName) |
|
312 { |
|
313 NS_OBJC_BEGIN_TRY_ABORT_BLOCK; |
|
314 |
|
315 if (!aAlertName) { |
|
316 return; // Can't do anything without a name |
|
317 } |
|
318 |
|
319 for (unsigned int i = 0; i < mActiveAlerts.Length(); i++) { |
|
320 OSXNotificationInfo *osxni = mActiveAlerts[i]; |
|
321 if ([aAlertName isEqualToString:osxni->mName]) { |
|
322 if (osxni->mObserver) { |
|
323 osxni->mObserver->Observe(nullptr, "alertclickcallback", osxni->mCookie.get()); |
|
324 } |
|
325 return; |
|
326 } |
|
327 } |
|
328 |
|
329 NS_OBJC_END_TRY_ABORT_BLOCK; |
|
330 } |
|
331 |
|
332 void |
|
333 OSXNotificationCenter::ShowPendingNotification(OSXNotificationInfo *osxni) |
|
334 { |
|
335 NS_OBJC_BEGIN_TRY_ABORT_BLOCK; |
|
336 |
|
337 if (osxni->mIconTimeoutTimer) { |
|
338 osxni->mIconTimeoutTimer->Cancel(); |
|
339 osxni->mIconTimeoutTimer = nullptr; |
|
340 } |
|
341 |
|
342 if (osxni->mIconRequest) { |
|
343 osxni->mIconRequest->Cancel(NS_BINDING_ABORTED); |
|
344 osxni->mIconRequest = nullptr; |
|
345 } |
|
346 |
|
347 CloseAlertCocoaString(osxni->mName); |
|
348 |
|
349 for (unsigned int i = 0; i < mPendingAlerts.Length(); i++) { |
|
350 if (mPendingAlerts[i] == osxni) { |
|
351 mActiveAlerts.AppendElement(osxni); |
|
352 mPendingAlerts.RemoveElementAt(i); |
|
353 break; |
|
354 } |
|
355 } |
|
356 |
|
357 [GetNotificationCenter() deliverNotification:osxni->mPendingNotifiction]; |
|
358 |
|
359 if (osxni->mObserver) { |
|
360 osxni->mObserver->Observe(nullptr, "alertshow", osxni->mCookie.get()); |
|
361 } |
|
362 |
|
363 [osxni->mPendingNotifiction release]; |
|
364 osxni->mPendingNotifiction = nil; |
|
365 |
|
366 NS_OBJC_END_TRY_ABORT_BLOCK; |
|
367 } |
|
368 |
|
369 NS_IMETHODIMP |
|
370 OSXNotificationCenter::Notify(imgIRequest *aRequest, int32_t aType, const nsIntRect* aData) |
|
371 { |
|
372 NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; |
|
373 |
|
374 if (aType == imgINotificationObserver::LOAD_COMPLETE) { |
|
375 OSXNotificationInfo *osxni = nullptr; |
|
376 for (unsigned int i = 0; i < mPendingAlerts.Length(); i++) { |
|
377 if (aRequest == mPendingAlerts[i]->mIconRequest) { |
|
378 osxni = mPendingAlerts[i]; |
|
379 break; |
|
380 } |
|
381 } |
|
382 if (!osxni || !osxni->mPendingNotifiction) { |
|
383 return NS_ERROR_FAILURE; |
|
384 } |
|
385 NSImage *cocoaImage = nil; |
|
386 uint32_t imgStatus = imgIRequest::STATUS_ERROR; |
|
387 nsresult rv = aRequest->GetImageStatus(&imgStatus); |
|
388 if (NS_SUCCEEDED(rv) && imgStatus != imgIRequest::STATUS_ERROR) { |
|
389 nsCOMPtr<imgIContainer> image; |
|
390 rv = aRequest->GetImage(getter_AddRefs(image)); |
|
391 if (NS_SUCCEEDED(rv)) { |
|
392 nsCocoaUtils::CreateNSImageFromImageContainer(image, imgIContainer::FRAME_FIRST, &cocoaImage, 1.0f); |
|
393 } |
|
394 } |
|
395 (osxni->mPendingNotifiction).contentImage = cocoaImage; |
|
396 [cocoaImage release]; |
|
397 ShowPendingNotification(osxni); |
|
398 } |
|
399 return NS_OK; |
|
400 |
|
401 NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; |
|
402 } |
|
403 |
|
404 NS_IMETHODIMP |
|
405 OSXNotificationCenter::Notify(nsITimer *timer) |
|
406 { |
|
407 NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; |
|
408 |
|
409 if (!timer) { |
|
410 return NS_ERROR_FAILURE; |
|
411 } |
|
412 |
|
413 for (unsigned int i = 0; i < mPendingAlerts.Length(); i++) { |
|
414 OSXNotificationInfo *osxni = mPendingAlerts[i]; |
|
415 if (timer == osxni->mIconTimeoutTimer.get()) { |
|
416 osxni->mIconTimeoutTimer = nullptr; |
|
417 if (osxni->mPendingNotifiction) { |
|
418 ShowPendingNotification(osxni); |
|
419 break; |
|
420 } |
|
421 } |
|
422 } |
|
423 return NS_OK; |
|
424 |
|
425 NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; |
|
426 } |
|
427 |
|
428 } // namespace mozilla |