Bug 1755924 [wpt PR 32876] - Handle resumed blocks that get sliced by floats correctl...
[gecko.git] / widget / cocoa / OSXNotificationCenter.mm
blob6d79a65c21bf0fc66c3bd481f873a9e0b1de9270
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
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/. */
6 #include "OSXNotificationCenter.h"
7 #import <AppKit/AppKit.h>
8 #include "imgIRequest.h"
9 #include "imgIContainer.h"
10 #include "nsICancelable.h"
11 #include "nsIStringBundle.h"
12 #include "nsNetUtil.h"
13 #import "nsCocoaUtils.h"
14 #include "nsComponentManagerUtils.h"
15 #include "nsContentUtils.h"
16 #include "nsObjCExceptions.h"
17 #include "nsString.h"
18 #include "nsCOMPtr.h"
19 #include "nsIObserver.h"
21 using namespace mozilla;
23 #define MAX_NOTIFICATION_NAME_LEN 5000
25 @protocol FakeNSUserNotification <NSObject>
26 @property(copy) NSString* title;
27 @property(copy) NSString* subtitle;
28 @property(copy) NSString* informativeText;
29 @property(copy) NSString* actionButtonTitle;
30 @property(copy) NSDictionary* userInfo;
31 @property(copy) NSDate* deliveryDate;
32 @property(copy) NSTimeZone* deliveryTimeZone;
33 @property(copy) NSDateComponents* deliveryRepeatInterval;
34 @property(readonly) NSDate* actualDeliveryDate;
35 @property(readonly, getter=isPresented) BOOL presented;
36 @property(readonly, getter=isRemote) BOOL remote;
37 @property(copy) NSString* soundName;
38 @property BOOL hasActionButton;
39 @property(readonly) NSUserNotificationActivationType activationType;
40 @property(copy) NSString* otherButtonTitle;
41 @property(copy) NSImage* contentImage;
42 @end
44 @protocol FakeNSUserNotificationCenter <NSObject>
45 + (id<FakeNSUserNotificationCenter>)defaultUserNotificationCenter;
46 @property(assign) id<NSUserNotificationCenterDelegate> delegate;
47 @property(copy) NSArray* scheduledNotifications;
48 - (void)scheduleNotification:(id<FakeNSUserNotification>)notification;
49 - (void)removeScheduledNotification:(id<FakeNSUserNotification>)notification;
50 @property(readonly) NSArray* deliveredNotifications;
51 - (void)deliverNotification:(id<FakeNSUserNotification>)notification;
52 - (void)removeDeliveredNotification:(id<FakeNSUserNotification>)notification;
53 - (void)removeAllDeliveredNotifications;
54 - (void)_removeAllDisplayedNotifications;
55 - (void)_removeDisplayedNotification:(id<FakeNSUserNotification>)notification;
56 @end
58 @interface mozNotificationCenterDelegate : NSObject <NSUserNotificationCenterDelegate> {
59   OSXNotificationCenter* mOSXNC;
61 - (id)initWithOSXNC:(OSXNotificationCenter*)osxnc;
62 @end
64 @implementation mozNotificationCenterDelegate
66 - (id)initWithOSXNC:(OSXNotificationCenter*)osxnc {
67   [super init];
68   // We should *never* outlive this OSXNotificationCenter.
69   mOSXNC = osxnc;
70   return self;
73 - (void)userNotificationCenter:(id<FakeNSUserNotificationCenter>)center
74         didDeliverNotification:(id<FakeNSUserNotification>)notification {
77 - (void)userNotificationCenter:(id<FakeNSUserNotificationCenter>)center
78        didActivateNotification:(id<FakeNSUserNotification>)notification {
79   unsigned long long additionalActionIndex = ULLONG_MAX;
80   if ([notification respondsToSelector:@selector(_alternateActionIndex)]) {
81     NSNumber* alternateActionIndex = [(NSObject*)notification valueForKey:@"_alternateActionIndex"];
82     additionalActionIndex = [alternateActionIndex unsignedLongLongValue];
83   }
84   mOSXNC->OnActivate([[notification userInfo] valueForKey:@"name"], notification.activationType,
85                      additionalActionIndex);
88 - (BOOL)userNotificationCenter:(id<FakeNSUserNotificationCenter>)center
89      shouldPresentNotification:(id<FakeNSUserNotification>)notification {
90   return YES;
93 // This is an undocumented method that we need for parity with Safari.
94 // Apple bug #15440664.
95 - (void)userNotificationCenter:(id<FakeNSUserNotificationCenter>)center
96     didRemoveDeliveredNotifications:(NSArray*)notifications {
97   for (id<FakeNSUserNotification> notification in notifications) {
98     NSString* name = [[notification userInfo] valueForKey:@"name"];
99     mOSXNC->CloseAlertCocoaString(name);
100   }
103 // This is an undocumented method that we need to be notified if a user clicks the close button.
104 - (void)userNotificationCenter:(id<FakeNSUserNotificationCenter>)center
105                didDismissAlert:(id<FakeNSUserNotification>)notification {
106   NSString* name = [[notification userInfo] valueForKey:@"name"];
107   mOSXNC->CloseAlertCocoaString(name);
110 @end
112 namespace mozilla {
114 enum {
115   OSXNotificationActionDisable = 0,
116   OSXNotificationActionSettings = 1,
119 class OSXNotificationInfo final : public nsISupports {
120  private:
121   virtual ~OSXNotificationInfo();
123  public:
124   NS_DECL_ISUPPORTS
125   OSXNotificationInfo(NSString* name, nsIObserver* observer, const nsAString& alertCookie);
127   NSString* mName;
128   nsCOMPtr<nsIObserver> mObserver;
129   nsString mCookie;
130   RefPtr<nsICancelable> mIconRequest;
131   id<FakeNSUserNotification> mPendingNotification;
134 NS_IMPL_ISUPPORTS0(OSXNotificationInfo)
136 OSXNotificationInfo::OSXNotificationInfo(NSString* name, nsIObserver* observer,
137                                          const nsAString& alertCookie) {
138   NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
140   NS_ASSERTION(name, "Cannot create OSXNotificationInfo without a name!");
141   mName = [name retain];
142   mObserver = observer;
143   mCookie = alertCookie;
144   mPendingNotification = nil;
146   NS_OBJC_END_TRY_IGNORE_BLOCK;
149 OSXNotificationInfo::~OSXNotificationInfo() {
150   NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
152   [mName release];
153   [mPendingNotification release];
155   NS_OBJC_END_TRY_IGNORE_BLOCK;
158 static id<FakeNSUserNotificationCenter> GetNotificationCenter() {
159   NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
161   Class c = NSClassFromString(@"NSUserNotificationCenter");
162   return [c performSelector:@selector(defaultUserNotificationCenter)];
164   NS_OBJC_END_TRY_BLOCK_RETURN(nil);
167 OSXNotificationCenter::OSXNotificationCenter() {
168   NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
170   mDelegate = [[mozNotificationCenterDelegate alloc] initWithOSXNC:this];
171   GetNotificationCenter().delegate = mDelegate;
172   mSuppressForScreenSharing = false;
174   NS_OBJC_END_TRY_IGNORE_BLOCK;
177 OSXNotificationCenter::~OSXNotificationCenter() {
178   NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
180   [GetNotificationCenter() removeAllDeliveredNotifications];
181   [mDelegate release];
183   NS_OBJC_END_TRY_IGNORE_BLOCK;
186 NS_IMPL_ISUPPORTS(OSXNotificationCenter, nsIAlertsService, nsIAlertsIconData, nsIAlertsDoNotDisturb,
187                   nsIAlertNotificationImageListener)
189 nsresult OSXNotificationCenter::Init() {
190   NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
192   return (!!NSClassFromString(@"NSUserNotification")) ? NS_OK : NS_ERROR_FAILURE;
194   NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
197 NS_IMETHODIMP
198 OSXNotificationCenter::ShowAlertNotification(
199     const nsAString& aImageUrl, const nsAString& aAlertTitle, const nsAString& aAlertText,
200     bool aAlertTextClickable, const nsAString& aAlertCookie, nsIObserver* aAlertListener,
201     const nsAString& aAlertName, const nsAString& aBidi, const nsAString& aLang,
202     const nsAString& aData, nsIPrincipal* aPrincipal, bool aInPrivateBrowsing,
203     bool aRequireInteraction) {
204   nsCOMPtr<nsIAlertNotification> alert = do_CreateInstance(ALERT_NOTIFICATION_CONTRACTID);
205   NS_ENSURE_TRUE(alert, NS_ERROR_FAILURE);
206   // vibrate is unused for now
207   nsTArray<uint32_t> vibrate;
208   nsresult rv = alert->Init(aAlertName, aImageUrl, aAlertTitle, aAlertText, aAlertTextClickable,
209                             aAlertCookie, aBidi, aLang, aData, aPrincipal, aInPrivateBrowsing,
210                             aRequireInteraction, false, vibrate);
211   NS_ENSURE_SUCCESS(rv, rv);
212   return ShowAlert(alert, aAlertListener);
215 NS_IMETHODIMP
216 OSXNotificationCenter::ShowPersistentNotification(const nsAString& aPersistentData,
217                                                   nsIAlertNotification* aAlert,
218                                                   nsIObserver* aAlertListener) {
219   return ShowAlert(aAlert, aAlertListener);
222 NS_IMETHODIMP
223 OSXNotificationCenter::ShowAlert(nsIAlertNotification* aAlert, nsIObserver* aAlertListener) {
224   return ShowAlertWithIconData(aAlert, aAlertListener, 0, nullptr);
227 NS_IMETHODIMP
228 OSXNotificationCenter::ShowAlertWithIconData(nsIAlertNotification* aAlert,
229                                              nsIObserver* aAlertListener, uint32_t aIconSize,
230                                              const uint8_t* aIconData) {
231   NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
233   NS_ENSURE_ARG(aAlert);
235   if (mSuppressForScreenSharing) {
236     return NS_OK;
237   }
239   Class unClass = NSClassFromString(@"NSUserNotification");
240   id<FakeNSUserNotification> notification = [[unClass alloc] init];
242   nsAutoString title;
243   nsresult rv = aAlert->GetTitle(title);
244   NS_ENSURE_SUCCESS(rv, rv);
245   notification.title = nsCocoaUtils::ToNSString(title);
247   nsAutoString hostPort;
248   rv = aAlert->GetSource(hostPort);
249   NS_ENSURE_SUCCESS(rv, rv);
250   nsCOMPtr<nsIStringBundle> bundle;
251   nsCOMPtr<nsIStringBundleService> sbs = do_GetService(NS_STRINGBUNDLE_CONTRACTID);
252   sbs->CreateBundle("chrome://alerts/locale/alert.properties", getter_AddRefs(bundle));
254   if (!hostPort.IsEmpty() && bundle) {
255     AutoTArray<nsString, 1> formatStrings = {hostPort};
256     nsAutoString notificationSource;
257     bundle->FormatStringFromName("source.label", formatStrings, notificationSource);
258     notification.subtitle = nsCocoaUtils::ToNSString(notificationSource);
259   }
261   nsAutoString text;
262   rv = aAlert->GetText(text);
263   NS_ENSURE_SUCCESS(rv, rv);
264   notification.informativeText = nsCocoaUtils::ToNSString(text);
266   notification.soundName = NSUserNotificationDefaultSoundName;
267   notification.hasActionButton = NO;
269   // If this is not an application/extension alert, show additional actions dealing with
270   // permissions.
271   bool isActionable;
272   if (bundle && NS_SUCCEEDED(aAlert->GetActionable(&isActionable)) && isActionable) {
273     nsAutoString closeButtonTitle, actionButtonTitle, disableButtonTitle, settingsButtonTitle;
274     bundle->GetStringFromName("closeButton.title", closeButtonTitle);
275     bundle->GetStringFromName("actionButton.label", actionButtonTitle);
276     if (!hostPort.IsEmpty()) {
277       AutoTArray<nsString, 1> formatStrings = {hostPort};
278       bundle->FormatStringFromName("webActions.disableForOrigin.label", formatStrings,
279                                    disableButtonTitle);
280     }
281     bundle->GetStringFromName("webActions.settings.label", settingsButtonTitle);
283     notification.otherButtonTitle = nsCocoaUtils::ToNSString(closeButtonTitle);
285     // OS X 10.8 only shows action buttons if the "Alerts" style is set in
286     // Notification Center preferences, and doesn't support the alternate
287     // action menu.
288     if ([notification respondsToSelector:@selector(set_showsButtons:)] &&
289         [notification respondsToSelector:@selector(set_alwaysShowAlternateActionMenu:)] &&
290         [notification respondsToSelector:@selector(set_alternateActionButtonTitles:)]) {
291       notification.hasActionButton = YES;
292       notification.actionButtonTitle = nsCocoaUtils::ToNSString(actionButtonTitle);
294       [(NSObject*)notification setValue:@(YES) forKey:@"_showsButtons"];
295       [(NSObject*)notification setValue:@(YES) forKey:@"_alwaysShowAlternateActionMenu"];
296       [(NSObject*)notification setValue:@[
297         nsCocoaUtils::ToNSString(disableButtonTitle), nsCocoaUtils::ToNSString(settingsButtonTitle)
298       ]
299                                  forKey:@"_alternateActionButtonTitles"];
300     }
301   }
302   nsAutoString name;
303   rv = aAlert->GetName(name);
304   // Don't let an alert name be more than MAX_NOTIFICATION_NAME_LEN characters.
305   // More than that shouldn't be necessary and userInfo (assigned to below) has
306   // a length limit of 16k on OS X 10.11. Exception thrown if limit exceeded.
307   if (name.Length() > MAX_NOTIFICATION_NAME_LEN) {
308     return NS_ERROR_FAILURE;
309   }
311   NS_ENSURE_SUCCESS(rv, rv);
312   NSString* alertName = nsCocoaUtils::ToNSString(name);
313   if (!alertName) {
314     return NS_ERROR_FAILURE;
315   }
316   notification.userInfo =
317       [NSDictionary dictionaryWithObjects:[NSArray arrayWithObjects:alertName, nil]
318                                   forKeys:[NSArray arrayWithObjects:@"name", nil]];
320   nsAutoString cookie;
321   rv = aAlert->GetCookie(cookie);
322   NS_ENSURE_SUCCESS(rv, rv);
324   OSXNotificationInfo* osxni = new OSXNotificationInfo(alertName, aAlertListener, cookie);
326   // Show the favicon if supported on this version of OS X.
327   if (aIconSize > 0 && [notification respondsToSelector:@selector(set_identityImage:)] &&
328       [notification respondsToSelector:@selector(set_identityImageHasBorder:)]) {
329     NSData* iconData = [NSData dataWithBytes:aIconData length:aIconSize];
330     NSImage* icon = [[[NSImage alloc] initWithData:iconData] autorelease];
332     [(NSObject*)notification setValue:icon forKey:@"_identityImage"];
333     [(NSObject*)notification setValue:@(NO) forKey:@"_identityImageHasBorder"];
334   }
336   bool inPrivateBrowsing;
337   rv = aAlert->GetInPrivateBrowsing(&inPrivateBrowsing);
338   NS_ENSURE_SUCCESS(rv, rv);
340   // Show the notification without waiting for an image if there is no icon URL or
341   // notification icons are not supported on this version of OS X.
342   if (![unClass instancesRespondToSelector:@selector(setContentImage:)]) {
343     CloseAlertCocoaString(alertName);
344     mActiveAlerts.AppendElement(osxni);
345     [GetNotificationCenter() deliverNotification:notification];
346     [notification release];
347     if (aAlertListener) {
348       aAlertListener->Observe(nullptr, "alertshow", cookie.get());
349     }
350   } else {
351     mPendingAlerts.AppendElement(osxni);
352     osxni->mPendingNotification = notification;
353     // Wait six seconds for the image to load.
354     rv = aAlert->LoadImage(6000, this, osxni, getter_AddRefs(osxni->mIconRequest));
355     if (NS_WARN_IF(NS_FAILED(rv))) {
356       ShowPendingNotification(osxni);
357     }
358   }
360   return NS_OK;
362   NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
365 NS_IMETHODIMP
366 OSXNotificationCenter::CloseAlert(const nsAString& aAlertName) {
367   NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
369   NSString* alertName = nsCocoaUtils::ToNSString(aAlertName);
370   CloseAlertCocoaString(alertName);
371   return NS_OK;
373   NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
376 void OSXNotificationCenter::CloseAlertCocoaString(NSString* aAlertName) {
377   NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
379   if (!aAlertName) {
380     return;  // Can't do anything without a name
381   }
383   NSArray* notifications = [GetNotificationCenter() deliveredNotifications];
384   for (id<FakeNSUserNotification> notification in notifications) {
385     NSString* name = [[notification userInfo] valueForKey:@"name"];
386     if ([name isEqualToString:aAlertName]) {
387       [GetNotificationCenter() removeDeliveredNotification:notification];
388       [GetNotificationCenter() _removeDisplayedNotification:notification];
389       break;
390     }
391   }
393   for (unsigned int i = 0; i < mActiveAlerts.Length(); i++) {
394     OSXNotificationInfo* osxni = mActiveAlerts[i];
395     if ([aAlertName isEqualToString:osxni->mName]) {
396       if (osxni->mObserver) {
397         osxni->mObserver->Observe(nullptr, "alertfinished", osxni->mCookie.get());
398       }
399       if (osxni->mIconRequest) {
400         osxni->mIconRequest->Cancel(NS_BINDING_ABORTED);
401         osxni->mIconRequest = nullptr;
402       }
403       mActiveAlerts.RemoveElementAt(i);
404       break;
405     }
406   }
408   NS_OBJC_END_TRY_IGNORE_BLOCK;
411 void OSXNotificationCenter::OnActivate(NSString* aAlertName,
412                                        NSUserNotificationActivationType aActivationType,
413                                        unsigned long long aAdditionalActionIndex) {
414   NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
416   if (!aAlertName) {
417     return;  // Can't do anything without a name
418   }
420   for (unsigned int i = 0; i < mActiveAlerts.Length(); i++) {
421     OSXNotificationInfo* osxni = mActiveAlerts[i];
422     if ([aAlertName isEqualToString:osxni->mName]) {
423       if (osxni->mObserver) {
424         switch ((int)aActivationType) {
425           case NSUserNotificationActivationTypeAdditionalActionClicked:
426           case NSUserNotificationActivationTypeActionButtonClicked:
427             switch (aAdditionalActionIndex) {
428               case OSXNotificationActionDisable:
429                 osxni->mObserver->Observe(nullptr, "alertdisablecallback", osxni->mCookie.get());
430                 break;
431               case OSXNotificationActionSettings:
432                 osxni->mObserver->Observe(nullptr, "alertsettingscallback", osxni->mCookie.get());
433                 break;
434               default:
435                 NS_WARNING("Unknown NSUserNotification additional action clicked");
436                 break;
437             }
438             break;
439           default:
440             osxni->mObserver->Observe(nullptr, "alertclickcallback", osxni->mCookie.get());
441             break;
442         }
443       }
444       return;
445     }
446   }
448   NS_OBJC_END_TRY_IGNORE_BLOCK;
451 void OSXNotificationCenter::ShowPendingNotification(OSXNotificationInfo* osxni) {
452   NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
454   if (osxni->mIconRequest) {
455     osxni->mIconRequest->Cancel(NS_BINDING_ABORTED);
456     osxni->mIconRequest = nullptr;
457   }
459   CloseAlertCocoaString(osxni->mName);
461   for (unsigned int i = 0; i < mPendingAlerts.Length(); i++) {
462     if (mPendingAlerts[i] == osxni) {
463       mActiveAlerts.AppendElement(osxni);
464       mPendingAlerts.RemoveElementAt(i);
465       break;
466     }
467   }
469   [GetNotificationCenter() deliverNotification:osxni->mPendingNotification];
471   if (osxni->mObserver) {
472     osxni->mObserver->Observe(nullptr, "alertshow", osxni->mCookie.get());
473   }
475   [osxni->mPendingNotification release];
476   osxni->mPendingNotification = nil;
478   NS_OBJC_END_TRY_IGNORE_BLOCK;
481 NS_IMETHODIMP
482 OSXNotificationCenter::OnImageMissing(nsISupports* aUserData) {
483   NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
485   OSXNotificationInfo* osxni = static_cast<OSXNotificationInfo*>(aUserData);
486   if (osxni->mPendingNotification) {
487     // If there was an error getting the image, or the request timed out, show
488     // the notification without a content image.
489     ShowPendingNotification(osxni);
490   }
491   return NS_OK;
493   NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
496 NS_IMETHODIMP
497 OSXNotificationCenter::OnImageReady(nsISupports* aUserData, imgIRequest* aRequest) {
498   NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
500   nsCOMPtr<imgIContainer> image;
501   nsresult rv = aRequest->GetImage(getter_AddRefs(image));
502   if (NS_WARN_IF(NS_FAILED(rv) || !image)) {
503     return rv;
504   }
506   OSXNotificationInfo* osxni = static_cast<OSXNotificationInfo*>(aUserData);
507   if (!osxni->mPendingNotification) {
508     return NS_ERROR_FAILURE;
509   }
511   NSImage* cocoaImage = nil;
512   // TODO: Pass ComputedStyle here to support context paint properties
513   nsCocoaUtils::CreateDualRepresentationNSImageFromImageContainer(image, imgIContainer::FRAME_FIRST,
514                                                                   nullptr, &cocoaImage);
515   (osxni->mPendingNotification).contentImage = cocoaImage;
516   [cocoaImage release];
517   ShowPendingNotification(osxni);
519   return NS_OK;
521   NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
524 // nsIAlertsDoNotDisturb
525 NS_IMETHODIMP
526 OSXNotificationCenter::GetManualDoNotDisturb(bool* aRetVal) { return NS_ERROR_NOT_IMPLEMENTED; }
528 NS_IMETHODIMP
529 OSXNotificationCenter::SetManualDoNotDisturb(bool aDoNotDisturb) {
530   return NS_ERROR_NOT_IMPLEMENTED;
533 NS_IMETHODIMP
534 OSXNotificationCenter::GetSuppressForScreenSharing(bool* aRetVal) {
535   NS_OBJC_BEGIN_TRY_BLOCK_RETURN
537   NS_ENSURE_ARG(aRetVal);
538   *aRetVal = mSuppressForScreenSharing;
539   return NS_OK;
541   NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE)
544 NS_IMETHODIMP
545 OSXNotificationCenter::SetSuppressForScreenSharing(bool aSuppress) {
546   NS_OBJC_BEGIN_TRY_BLOCK_RETURN
548   mSuppressForScreenSharing = aSuppress;
549   return NS_OK;
551   NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE)
554 }  // namespace mozilla