1 /*
2 ==============================================================================
3
4 This file is part of the JUCE library.
5 Copyright (c) 2020 - Raw Material Software Limited
6
7 JUCE is an open source library subject to commercial or open-source
8 licensing.
9
10 By using JUCE, you agree to the terms of both the JUCE 6 End-User License
11 Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
12
13 End User License Agreement: www.juce.com/juce-6-licence
14 Privacy Policy: www.juce.com/juce-privacy-policy
15
16 Or: You may also use this code under the terms of the GPL v3 (see
17 www.gnu.org/licenses).
18
19 JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
20 EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
21 DISCLAIMED.
22
23 ==============================================================================
24 */
25
26 JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations")
27
28 namespace juce
29 {
30
31 namespace PushNotificationsDelegateDetailsOsx
32 {
33 using Action = PushNotifications::Notification::Action;
34
35 //==============================================================================
juceNotificationToNSUserNotification(const PushNotifications::Notification & n,bool isEarlierThanMavericks,bool isEarlierThanYosemite)36 NSUserNotification* juceNotificationToNSUserNotification (const PushNotifications::Notification& n,
37 bool isEarlierThanMavericks,
38 bool isEarlierThanYosemite)
39 {
40 auto notification = [[NSUserNotification alloc] init];
41
42 notification.title = juceStringToNS (n.title);
43 notification.subtitle = juceStringToNS (n.subtitle);
44 notification.informativeText = juceStringToNS (n.body);
45 notification.userInfo = varObjectToNSDictionary (n.properties);
46
47 auto triggerTime = Time::getCurrentTime() + RelativeTime (n.triggerIntervalSec);
48 notification.deliveryDate = [NSDate dateWithTimeIntervalSince1970: triggerTime.toMilliseconds() / 1000.];
49
50 if (n.repeat && n.triggerIntervalSec >= 60)
51 {
52 auto dateComponents = [[NSDateComponents alloc] init];
53 auto intervalSec = NSInteger (n.triggerIntervalSec);
54 dateComponents.second = intervalSec;
55 dateComponents.nanosecond = NSInteger ((n.triggerIntervalSec - intervalSec) * 1000000000);
56
57 notification.deliveryRepeatInterval = dateComponents;
58
59 [dateComponents autorelease];
60 }
61
62 auto soundToPlayString = n.soundToPlay.toString (true);
63
64 if (soundToPlayString == "default_os_sound")
65 {
66 notification.soundName = NSUserNotificationDefaultSoundName;
67 }
68 else if (soundToPlayString.isNotEmpty())
69 {
70 auto* soundName = juceStringToNS (soundToPlayString.fromLastOccurrenceOf ("/", false, false)
71 .upToLastOccurrenceOf (".", false, false));
72
73 notification.soundName = soundName;
74 }
75
76 notification.hasActionButton = n.actions.size() > 0;
77
78 if (n.actions.size() > 0)
79 notification.actionButtonTitle = juceStringToNS (n.actions.getReference (0).title);
80
81 if (! isEarlierThanMavericks)
82 {
83 notification.identifier = juceStringToNS (n.identifier);
84
85 if (n.actions.size() > 0)
86 {
87 notification.hasReplyButton = n.actions.getReference (0).style == Action::text;
88 notification.responsePlaceholder = juceStringToNS (n.actions.getReference (0).textInputPlaceholder);
89 }
90
91 auto* imageDirectory = n.icon.contains ("/")
92 ? juceStringToNS (n.icon.upToLastOccurrenceOf ("/", false, true))
93 : [NSString string];
94
95 auto* imageName = juceStringToNS (n.icon.fromLastOccurrenceOf ("/", false, false)
96 .upToLastOccurrenceOf (".", false, false));
97 auto* imageExtension = juceStringToNS (n.icon.fromLastOccurrenceOf (".", false, false));
98
99 NSString* imagePath = nil;
100
101 if ([imageDirectory length] == NSUInteger (0))
102 {
103 imagePath = [[NSBundle mainBundle] pathForResource: imageName
104 ofType: imageExtension];
105 }
106 else
107 {
108 imagePath = [[NSBundle mainBundle] pathForResource: imageName
109 ofType: imageExtension
110 inDirectory: imageDirectory];
111 }
112
113 notification.contentImage = [[NSImage alloc] initWithContentsOfFile: imagePath];
114
115 if (! isEarlierThanYosemite)
116 {
117 if (n.actions.size() > 1)
118 {
119 auto additionalActions = [NSMutableArray arrayWithCapacity: (NSUInteger) n.actions.size() - 1];
120
121 for (int a = 1; a < n.actions.size(); ++a)
122 [additionalActions addObject: [NSUserNotificationAction actionWithIdentifier: juceStringToNS (n.actions[a].identifier)
123 title: juceStringToNS (n.actions[a].title)]];
124
125 notification.additionalActions = additionalActions;
126 }
127 }
128 }
129
130 [notification autorelease];
131
132 return notification;
133 }
134
135 //==============================================================================
nsUserNotificationToJuceNotification(NSUserNotification * n,bool isEarlierThanMavericks,bool isEarlierThanYosemite)136 PushNotifications::Notification nsUserNotificationToJuceNotification (NSUserNotification* n,
137 bool isEarlierThanMavericks,
138 bool isEarlierThanYosemite)
139 {
140 PushNotifications::Notification notif;
141
142 notif.title = nsStringToJuce (n.title);
143 notif.subtitle = nsStringToJuce (n.subtitle);
144 notif.body = nsStringToJuce (n.informativeText);
145
146 notif.repeat = n.deliveryRepeatInterval != nil;
147
148 if (n.deliveryRepeatInterval != nil)
149 {
150 notif.triggerIntervalSec = n.deliveryRepeatInterval.second + (n.deliveryRepeatInterval.nanosecond / 1000000000.);
151 }
152 else
153 {
154 NSDate* dateNow = [NSDate date];
155 NSDate* deliveryDate = n.deliveryDate;
156
157 notif.triggerIntervalSec = [dateNow timeIntervalSinceDate: deliveryDate];
158 }
159
160 notif.soundToPlay = URL (nsStringToJuce (n.soundName));
161 notif.properties = nsDictionaryToVar (n.userInfo);
162
163 if (! isEarlierThanMavericks)
164 {
165 notif.identifier = nsStringToJuce (n.identifier);
166
167 if (n.contentImage != nil)
168 notif.icon = nsStringToJuce ([n.contentImage name]);
169 }
170
171 Array<Action> actions;
172
173 if (n.actionButtonTitle != nil)
174 {
175 Action action;
176 action.title = nsStringToJuce (n.actionButtonTitle);
177
178 if (! isEarlierThanMavericks)
179 {
180 if (n.hasReplyButton)
181 action.style = Action::text;
182
183 if (n.responsePlaceholder != nil)
184 action.textInputPlaceholder = nsStringToJuce (n.responsePlaceholder);
185 }
186
187 actions.add (action);
188 }
189
190 if (! isEarlierThanYosemite)
191 {
192 if (n.additionalActions != nil)
193 {
194 for (NSUserNotificationAction* a in n.additionalActions)
195 {
196 Action action;
197 action.identifier = nsStringToJuce (a.identifier);
198 action.title = nsStringToJuce (a.title);
199
200 actions.add (action);
201 }
202 }
203 }
204
205 return notif;
206 }
207
208 //==============================================================================
getNotificationPropertiesFromDictionaryVar(const var & dictionaryVar)209 var getNotificationPropertiesFromDictionaryVar (const var& dictionaryVar)
210 {
211 auto* dictionaryVarObject = dictionaryVar.getDynamicObject();
212
213 if (dictionaryVarObject == nullptr)
214 return {};
215
216 const auto& properties = dictionaryVarObject->getProperties();
217
218 DynamicObject::Ptr propsVarObject = new DynamicObject();
219
220 for (int i = 0; i < properties.size(); ++i)
221 {
222 auto propertyName = properties.getName (i).toString();
223
224 if (propertyName == "aps")
225 continue;
226
227 propsVarObject->setProperty (propertyName, properties.getValueAt (i));
228 }
229
230 return var (propsVarObject.get());
231 }
232
nsDictionaryToJuceNotification(NSDictionary * dictionary)233 PushNotifications::Notification nsDictionaryToJuceNotification (NSDictionary* dictionary)
234 {
235 const var dictionaryVar = nsDictionaryToVar (dictionary);
236
237 const var apsVar = dictionaryVar.getProperty ("aps", {});
238
239 if (! apsVar.isObject())
240 return {};
241
242 var alertVar = apsVar.getProperty ("alert", {});
243
244 const var titleVar = alertVar.getProperty ("title", {});
245 const var bodyVar = alertVar.isObject() ? alertVar.getProperty ("body", {}) : alertVar;
246
247 const var categoryVar = apsVar.getProperty ("category", {});
248 const var soundVar = apsVar.getProperty ("sound", {});
249 const var badgeVar = apsVar.getProperty ("badge", {});
250 const var threadIdVar = apsVar.getProperty ("thread-id", {});
251
252 PushNotifications::Notification notification;
253
254 notification.title = titleVar .toString();
255 notification.body = bodyVar .toString();
256 notification.groupId = threadIdVar.toString();
257 notification.category = categoryVar.toString();
258 notification.soundToPlay = URL (soundVar.toString());
259 notification.badgeNumber = (int) badgeVar;
260 notification.properties = getNotificationPropertiesFromDictionaryVar (dictionaryVar);
261
262 return notification;
263 }
264 }
265
266 //==============================================================================
267 struct PushNotificationsDelegate
268 {
PushNotificationsDelegatejuce::PushNotificationsDelegate269 PushNotificationsDelegate() : delegate ([getClass().createInstance() init])
270 {
271 Class::setThis (delegate.get(), this);
272
273 id<NSApplicationDelegate> appDelegate = [[NSApplication sharedApplication] delegate];
274
275 SEL selector = NSSelectorFromString (@"setPushNotificationsDelegate:");
276
277 if ([appDelegate respondsToSelector: selector])
278 [appDelegate performSelector: selector withObject: delegate.get()];
279
280 [NSUserNotificationCenter defaultUserNotificationCenter].delegate = delegate.get();
281 }
282
~PushNotificationsDelegatejuce::PushNotificationsDelegate283 virtual ~PushNotificationsDelegate()
284 {
285 [NSUserNotificationCenter defaultUserNotificationCenter].delegate = nil;
286 }
287
288 virtual void registeredForRemoteNotifications (NSData* deviceToken) = 0;
289
290 virtual void failedToRegisterForRemoteNotifications (NSError* error) = 0;
291
292 virtual void didReceiveRemoteNotification (NSDictionary* userInfo) = 0;
293
294 virtual void didDeliverNotification (NSUserNotification* notification) = 0;
295
296 virtual void didActivateNotification (NSUserNotification* notification) = 0;
297
298 virtual bool shouldPresentNotification (NSUserNotification* notification) = 0;
299
300 protected:
301 std::unique_ptr<NSObject<NSApplicationDelegate, NSUserNotificationCenterDelegate>, NSObjectDeleter> delegate;
302
303 private:
304 struct Class : public ObjCClass<NSObject<NSApplicationDelegate, NSUserNotificationCenterDelegate>>
305 {
Classjuce::PushNotificationsDelegate::Class306 Class() : ObjCClass<NSObject<NSApplicationDelegate, NSUserNotificationCenterDelegate>> ("JucePushNotificationsDelegate_")
307 {
308 addIvar<PushNotificationsDelegate*> ("self");
309
310 addMethod (@selector (application:didRegisterForRemoteNotificationsWithDeviceToken:), registeredForRemoteNotifications, "v@:@@");
311 addMethod (@selector (application:didFailToRegisterForRemoteNotificationsWithError:), failedToRegisterForRemoteNotifications, "v@:@@");
312 addMethod (@selector (application:didReceiveRemoteNotification:), didReceiveRemoteNotification, "v@:@@");
313 addMethod (@selector (userNotificationCenter:didDeliverNotification:), didDeliverNotification, "v@:@@");
314 addMethod (@selector (userNotificationCenter:didActivateNotification:), didActivateNotification, "v@:@@");
315 addMethod (@selector (userNotificationCenter:shouldPresentNotification:), shouldPresentNotification, "B@:@@");
316
317 registerClass();
318 }
319
320 //==============================================================================
getThisjuce::PushNotificationsDelegate::Class321 static PushNotificationsDelegate& getThis (id self) { return *getIvar<PushNotificationsDelegate*> (self, "self"); }
setThisjuce::PushNotificationsDelegate::Class322 static void setThis (id self, PushNotificationsDelegate* d) { object_setInstanceVariable (self, "self", d); }
323
324 //==============================================================================
registeredForRemoteNotificationsjuce::PushNotificationsDelegate::Class325 static void registeredForRemoteNotifications (id self, SEL, NSApplication*,
326 NSData* deviceToken) { getThis (self).registeredForRemoteNotifications (deviceToken); }
327
failedToRegisterForRemoteNotificationsjuce::PushNotificationsDelegate::Class328 static void failedToRegisterForRemoteNotifications (id self, SEL, NSApplication*,
329 NSError* error) { getThis (self).failedToRegisterForRemoteNotifications (error); }
330
didReceiveRemoteNotificationjuce::PushNotificationsDelegate::Class331 static void didReceiveRemoteNotification (id self, SEL, NSApplication*,
332 NSDictionary* userInfo) { getThis (self).didReceiveRemoteNotification (userInfo); }
333
didDeliverNotificationjuce::PushNotificationsDelegate::Class334 static void didDeliverNotification (id self, SEL, NSUserNotificationCenter*,
335 NSUserNotification* notification) { getThis (self).didDeliverNotification (notification); }
336
didActivateNotificationjuce::PushNotificationsDelegate::Class337 static void didActivateNotification (id self, SEL, NSUserNotificationCenter*,
338 NSUserNotification* notification) { getThis (self).didActivateNotification (notification); }
339
shouldPresentNotificationjuce::PushNotificationsDelegate::Class340 static bool shouldPresentNotification (id self, SEL, NSUserNotificationCenter*,
341 NSUserNotification* notification) { return getThis (self).shouldPresentNotification (notification); }
342 };
343
344 //==============================================================================
getClassjuce::PushNotificationsDelegate345 static Class& getClass()
346 {
347 static Class c;
348 return c;
349 }
350 };
351
352 //==============================================================================
isValid() const353 bool PushNotifications::Notification::isValid() const noexcept { return true; }
354
355 //==============================================================================
356 struct PushNotifications::Pimpl : private PushNotificationsDelegate
357 {
Pimpljuce::PushNotifications::Pimpl358 Pimpl (PushNotifications& p)
359 : owner (p)
360 {
361 }
362
requestPermissionsWithSettingsjuce::PushNotifications::Pimpl363 void requestPermissionsWithSettings (const PushNotifications::Settings& settingsToUse)
364 {
365 if (isEarlierThanLion)
366 return;
367
368 settings = settingsToUse;
369
370 NSRemoteNotificationType types = NSUInteger ((bool) settings.allowBadge);
371
372 if (isAtLeastMountainLion)
373 types |= (NSUInteger) ((bool) settings.allowSound << 1 | (bool) settings.allowAlert << 2);
374
375 [[NSApplication sharedApplication] registerForRemoteNotificationTypes: types];
376 }
377
requestSettingsUsedjuce::PushNotifications::Pimpl378 void requestSettingsUsed()
379 {
380 if (isEarlierThanLion)
381 {
382 // no settings available
383 owner.listeners.call ([] (Listener& l) { l.notificationSettingsReceived ({}); });
384 return;
385 }
386
387 settings.allowBadge = [NSApplication sharedApplication].enabledRemoteNotificationTypes & NSRemoteNotificationTypeBadge;
388
389 if (isAtLeastMountainLion)
390 {
391 settings.allowSound = [NSApplication sharedApplication].enabledRemoteNotificationTypes & NSRemoteNotificationTypeSound;
392 settings.allowAlert = [NSApplication sharedApplication].enabledRemoteNotificationTypes & NSRemoteNotificationTypeAlert;
393 }
394
395 owner.listeners.call ([&] (Listener& l) { l.notificationSettingsReceived (settings); });
396 }
397
areNotificationsEnabledjuce::PushNotifications::Pimpl398 bool areNotificationsEnabled() const { return true; }
399
sendLocalNotificationjuce::PushNotifications::Pimpl400 void sendLocalNotification (const Notification& n)
401 {
402 auto* notification = PushNotificationsDelegateDetailsOsx::juceNotificationToNSUserNotification (n, isEarlierThanMavericks, isEarlierThanYosemite);
403
404 [[NSUserNotificationCenter defaultUserNotificationCenter] scheduleNotification: notification];
405 }
406
getDeliveredNotificationsjuce::PushNotifications::Pimpl407 void getDeliveredNotifications() const
408 {
409 Array<PushNotifications::Notification> notifs;
410
411 for (NSUserNotification* n in [NSUserNotificationCenter defaultUserNotificationCenter].deliveredNotifications)
412 notifs.add (PushNotificationsDelegateDetailsOsx::nsUserNotificationToJuceNotification (n, isEarlierThanMavericks, isEarlierThanYosemite));
413
414 owner.listeners.call ([&] (Listener& l) { l.deliveredNotificationsListReceived (notifs); });
415 }
416
removeAllDeliveredNotificationsjuce::PushNotifications::Pimpl417 void removeAllDeliveredNotifications()
418 {
419 [[NSUserNotificationCenter defaultUserNotificationCenter] removeAllDeliveredNotifications];
420 }
421
removeDeliveredNotificationjuce::PushNotifications::Pimpl422 void removeDeliveredNotification (const String& identifier)
423 {
424 PushNotifications::Notification n;
425 n.identifier = identifier;
426
427 auto nsNotification = PushNotificationsDelegateDetailsOsx::juceNotificationToNSUserNotification (n, isEarlierThanMavericks, isEarlierThanYosemite);
428
429 [[NSUserNotificationCenter defaultUserNotificationCenter] removeDeliveredNotification: nsNotification];
430 }
431
setupChannelsjuce::PushNotifications::Pimpl432 void setupChannels (const Array<ChannelGroup>& groups, const Array<Channel>& channels)
433 {
434 ignoreUnused (groups, channels);
435 }
436
getPendingLocalNotificationsjuce::PushNotifications::Pimpl437 void getPendingLocalNotifications() const
438 {
439 Array<PushNotifications::Notification> notifs;
440
441 for (NSUserNotification* n in [NSUserNotificationCenter defaultUserNotificationCenter].scheduledNotifications)
442 notifs.add (PushNotificationsDelegateDetailsOsx::nsUserNotificationToJuceNotification (n, isEarlierThanMavericks, isEarlierThanYosemite));
443
444 owner.listeners.call ([&] (Listener& l) { l.pendingLocalNotificationsListReceived (notifs); });
445 }
446
removePendingLocalNotificationjuce::PushNotifications::Pimpl447 void removePendingLocalNotification (const String& identifier)
448 {
449 PushNotifications::Notification n;
450 n.identifier = identifier;
451
452 auto nsNotification = PushNotificationsDelegateDetailsOsx::juceNotificationToNSUserNotification (n, isEarlierThanMavericks, isEarlierThanYosemite);
453
454 [[NSUserNotificationCenter defaultUserNotificationCenter] removeScheduledNotification: nsNotification];
455 }
456
removeAllPendingLocalNotificationsjuce::PushNotifications::Pimpl457 void removeAllPendingLocalNotifications()
458 {
459 for (NSUserNotification* n in [NSUserNotificationCenter defaultUserNotificationCenter].scheduledNotifications)
460 [[NSUserNotificationCenter defaultUserNotificationCenter] removeScheduledNotification: n];
461 }
462
getDeviceTokenjuce::PushNotifications::Pimpl463 String getDeviceToken()
464 {
465 // You need to call requestPermissionsWithSettings() first.
466 jassert (initialised);
467
468 return deviceToken;
469 }
470
471 //==============================================================================
472 //PushNotificationsDelegate
registeredForRemoteNotificationsjuce::PushNotifications::Pimpl473 void registeredForRemoteNotifications (NSData* deviceTokenToUse) override
474 {
475 deviceToken = [deviceTokenToUse]() -> String
476 {
477 auto length = deviceTokenToUse.length;
478
479 if (auto* buffer = (const unsigned char*) deviceTokenToUse.bytes)
480 {
481 NSMutableString* hexString = [NSMutableString stringWithCapacity: (length * 2)];
482
483 for (NSUInteger i = 0; i < length; ++i)
484 [hexString appendFormat:@"%02x", buffer[i]];
485
486 return nsStringToJuce ([hexString copy]);
487 }
488
489 return {};
490 }();
491
492 initialised = true;
493
494 owner.listeners.call ([&] (Listener& l) { l.deviceTokenRefreshed (deviceToken); });
495 }
496
failedToRegisterForRemoteNotificationsjuce::PushNotifications::Pimpl497 void failedToRegisterForRemoteNotifications (NSError* error) override
498 {
499 ignoreUnused (error);
500 deviceToken.clear();
501 }
502
didReceiveRemoteNotificationjuce::PushNotifications::Pimpl503 void didReceiveRemoteNotification (NSDictionary* userInfo) override
504 {
505 auto n = PushNotificationsDelegateDetailsOsx::nsDictionaryToJuceNotification (userInfo);
506 owner.listeners.call ([&] (Listener& l) { l.handleNotification (true, n); });
507 }
508
didDeliverNotificationjuce::PushNotifications::Pimpl509 void didDeliverNotification (NSUserNotification* notification) override
510 {
511 ignoreUnused (notification);
512 }
513
didActivateNotificationjuce::PushNotifications::Pimpl514 void didActivateNotification (NSUserNotification* notification) override
515 {
516 auto n = PushNotificationsDelegateDetailsOsx::nsUserNotificationToJuceNotification (notification, isEarlierThanMavericks, isEarlierThanYosemite);
517
518 if (notification.activationType == NSUserNotificationActivationTypeContentsClicked)
519 {
520 owner.listeners.call ([&] (Listener& l) { l.handleNotification (notification.remote, n); });
521 }
522 else
523 {
524 auto actionIdentifier = (! isEarlierThanYosemite && notification.additionalActivationAction != nil)
525 ? nsStringToJuce (notification.additionalActivationAction.identifier)
526 : nsStringToJuce (notification.actionButtonTitle);
527
528 auto reply = notification.activationType == NSUserNotificationActivationTypeReplied
529 ? nsStringToJuce ([notification.response string])
530 : String();
531
532 owner.listeners.call ([&] (Listener& l) { l.handleNotificationAction (notification.remote, n, actionIdentifier, reply); });
533 }
534 }
535
shouldPresentNotificationjuce::PushNotifications::Pimpl536 bool shouldPresentNotification (NSUserNotification*) override { return true; }
537
subscribeToTopicjuce::PushNotifications::Pimpl538 void subscribeToTopic (const String& topic) { ignoreUnused (topic); }
unsubscribeFromTopicjuce::PushNotifications::Pimpl539 void unsubscribeFromTopic (const String& topic) { ignoreUnused (topic); }
540
sendUpstreamMessagejuce::PushNotifications::Pimpl541 void sendUpstreamMessage (const String& serverSenderId,
542 const String& collapseKey,
543 const String& messageId,
544 const String& messageType,
545 int timeToLive,
546 const StringPairArray& additionalData)
547 {
548 ignoreUnused (serverSenderId, collapseKey, messageId, messageType);
549 ignoreUnused (timeToLive, additionalData);
550 }
551
552 private:
553 PushNotifications& owner;
554
555 const bool isEarlierThanLion = std::floor (NSFoundationVersionNumber) < std::floor (NSFoundationVersionNumber10_7);
556 const bool isAtLeastMountainLion = std::floor (NSFoundationVersionNumber) >= NSFoundationVersionNumber10_7;
557 const bool isEarlierThanMavericks = std::floor (NSFoundationVersionNumber) < NSFoundationVersionNumber10_9;
558 const bool isEarlierThanYosemite = std::floor (NSFoundationVersionNumber) <= NSFoundationVersionNumber10_9;
559
560 bool initialised = false;
561 String deviceToken;
562
563 PushNotifications::Settings settings;
564 };
565
566 } // namespace juce
567
568 JUCE_END_IGNORE_WARNINGS_GCC_LIKE
569