The JUCE cross-platform C++ framework, with DISTRHO/KXStudio specific changes
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

564 lines
23KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2022 - Raw Material Software Limited
  5. JUCE is an open source library subject to commercial or open-source
  6. licensing.
  7. By using JUCE, you agree to the terms of both the JUCE 7 End-User License
  8. Agreement and JUCE Privacy Policy.
  9. End User License Agreement: www.juce.com/juce-7-licence
  10. Privacy Policy: www.juce.com/juce-privacy-policy
  11. Or: You may also use this code under the terms of the GPL v3 (see
  12. www.gnu.org/licenses).
  13. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
  14. EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
  15. DISCLAIMED.
  16. ==============================================================================
  17. */
  18. JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations")
  19. namespace juce
  20. {
  21. namespace PushNotificationsDelegateDetailsOsx
  22. {
  23. using Action = PushNotifications::Notification::Action;
  24. //==============================================================================
  25. static NSUserNotification* juceNotificationToNSUserNotification (const PushNotifications::Notification& n)
  26. {
  27. auto notification = [[NSUserNotification alloc] init];
  28. notification.title = juceStringToNS (n.title);
  29. notification.subtitle = juceStringToNS (n.subtitle);
  30. notification.informativeText = juceStringToNS (n.body);
  31. notification.userInfo = varToNSDictionary (n.properties);
  32. auto triggerTime = Time::getCurrentTime() + RelativeTime (n.triggerIntervalSec);
  33. notification.deliveryDate = [NSDate dateWithTimeIntervalSince1970: (double) triggerTime.toMilliseconds() / 1000.0];
  34. if (n.repeat && n.triggerIntervalSec >= 60)
  35. {
  36. auto dateComponents = [[NSDateComponents alloc] init];
  37. auto intervalSec = NSInteger (n.triggerIntervalSec);
  38. dateComponents.second = intervalSec;
  39. dateComponents.nanosecond = NSInteger ((n.triggerIntervalSec - (double) intervalSec) * 1000000000);
  40. notification.deliveryRepeatInterval = dateComponents;
  41. [dateComponents autorelease];
  42. }
  43. auto soundToPlayString = n.soundToPlay.toString (true);
  44. if (soundToPlayString == "default_os_sound")
  45. {
  46. notification.soundName = NSUserNotificationDefaultSoundName;
  47. }
  48. else if (soundToPlayString.isNotEmpty())
  49. {
  50. auto* soundName = juceStringToNS (soundToPlayString.fromLastOccurrenceOf ("/", false, false)
  51. .upToLastOccurrenceOf (".", false, false));
  52. notification.soundName = soundName;
  53. }
  54. notification.hasActionButton = n.actions.size() > 0;
  55. if (n.actions.size() > 0)
  56. notification.actionButtonTitle = juceStringToNS (n.actions.getReference (0).title);
  57. if (@available (macOS 10.9, *))
  58. {
  59. notification.identifier = juceStringToNS (n.identifier);
  60. if (n.actions.size() > 0)
  61. {
  62. notification.hasReplyButton = n.actions.getReference (0).style == Action::text;
  63. notification.responsePlaceholder = juceStringToNS (n.actions.getReference (0).textInputPlaceholder);
  64. }
  65. auto* imageDirectory = n.icon.contains ("/")
  66. ? juceStringToNS (n.icon.upToLastOccurrenceOf ("/", false, true))
  67. : [NSString string];
  68. auto* imageName = juceStringToNS (n.icon.fromLastOccurrenceOf ("/", false, false)
  69. .upToLastOccurrenceOf (".", false, false));
  70. auto* imageExtension = juceStringToNS (n.icon.fromLastOccurrenceOf (".", false, false));
  71. NSString* imagePath = nil;
  72. if ([imageDirectory length] == NSUInteger (0))
  73. {
  74. imagePath = [[NSBundle mainBundle] pathForResource: imageName
  75. ofType: imageExtension];
  76. }
  77. else
  78. {
  79. imagePath = [[NSBundle mainBundle] pathForResource: imageName
  80. ofType: imageExtension
  81. inDirectory: imageDirectory];
  82. }
  83. notification.contentImage = [[NSImage alloc] initWithContentsOfFile: imagePath];
  84. if (@available (macOS 10.10, *))
  85. {
  86. if (n.actions.size() > 1)
  87. {
  88. auto additionalActions = [NSMutableArray arrayWithCapacity: (NSUInteger) n.actions.size() - 1];
  89. for (int a = 1; a < n.actions.size(); ++a)
  90. [additionalActions addObject: [NSUserNotificationAction actionWithIdentifier: juceStringToNS (n.actions[a].identifier)
  91. title: juceStringToNS (n.actions[a].title)]];
  92. notification.additionalActions = additionalActions;
  93. }
  94. }
  95. }
  96. [notification autorelease];
  97. return notification;
  98. }
  99. //==============================================================================
  100. static PushNotifications::Notification nsUserNotificationToJuceNotification (NSUserNotification* n)
  101. {
  102. PushNotifications::Notification notif;
  103. notif.title = nsStringToJuce (n.title);
  104. notif.subtitle = nsStringToJuce (n.subtitle);
  105. notif.body = nsStringToJuce (n.informativeText);
  106. notif.repeat = n.deliveryRepeatInterval != nil;
  107. if (n.deliveryRepeatInterval != nil)
  108. {
  109. notif.triggerIntervalSec = (double) n.deliveryRepeatInterval.second + ((double) n.deliveryRepeatInterval.nanosecond / 1000000000.0);
  110. }
  111. else
  112. {
  113. NSDate* dateNow = [NSDate date];
  114. NSDate* deliveryDate = n.deliveryDate;
  115. notif.triggerIntervalSec = [dateNow timeIntervalSinceDate: deliveryDate];
  116. }
  117. notif.soundToPlay = URL (nsStringToJuce (n.soundName));
  118. notif.properties = nsDictionaryToVar (n.userInfo);
  119. if (@available (macOS 10.9, *))
  120. {
  121. notif.identifier = nsStringToJuce (n.identifier);
  122. if (n.contentImage != nil)
  123. notif.icon = nsStringToJuce ([n.contentImage name]);
  124. }
  125. Array<Action> actions;
  126. if (n.actionButtonTitle != nil)
  127. {
  128. Action action;
  129. action.title = nsStringToJuce (n.actionButtonTitle);
  130. if (@available (macOS 10.9, *))
  131. {
  132. if (n.hasReplyButton)
  133. action.style = Action::text;
  134. if (n.responsePlaceholder != nil)
  135. action.textInputPlaceholder = nsStringToJuce (n.responsePlaceholder);
  136. }
  137. actions.add (action);
  138. }
  139. if (@available (macOS 10.10, *))
  140. {
  141. if (n.additionalActions != nil)
  142. {
  143. for (NSUserNotificationAction* a in n.additionalActions)
  144. {
  145. Action action;
  146. action.identifier = nsStringToJuce (a.identifier);
  147. action.title = nsStringToJuce (a.title);
  148. actions.add (action);
  149. }
  150. }
  151. }
  152. return notif;
  153. }
  154. //==============================================================================
  155. static var getNotificationPropertiesFromDictionaryVar (const var& dictionaryVar)
  156. {
  157. auto* dictionaryVarObject = dictionaryVar.getDynamicObject();
  158. if (dictionaryVarObject == nullptr)
  159. return {};
  160. const auto& properties = dictionaryVarObject->getProperties();
  161. DynamicObject::Ptr propsVarObject = new DynamicObject();
  162. for (int i = 0; i < properties.size(); ++i)
  163. {
  164. auto propertyName = properties.getName (i).toString();
  165. if (propertyName == "aps")
  166. continue;
  167. propsVarObject->setProperty (propertyName, properties.getValueAt (i));
  168. }
  169. return var (propsVarObject.get());
  170. }
  171. static PushNotifications::Notification nsDictionaryToJuceNotification (NSDictionary* dictionary)
  172. {
  173. const var dictionaryVar = nsDictionaryToVar (dictionary);
  174. const var apsVar = dictionaryVar.getProperty ("aps", {});
  175. if (! apsVar.isObject())
  176. return {};
  177. var alertVar = apsVar.getProperty ("alert", {});
  178. const var titleVar = alertVar.getProperty ("title", {});
  179. const var bodyVar = alertVar.isObject() ? alertVar.getProperty ("body", {}) : alertVar;
  180. const var categoryVar = apsVar.getProperty ("category", {});
  181. const var soundVar = apsVar.getProperty ("sound", {});
  182. const var badgeVar = apsVar.getProperty ("badge", {});
  183. const var threadIdVar = apsVar.getProperty ("thread-id", {});
  184. PushNotifications::Notification notification;
  185. notification.title = titleVar .toString();
  186. notification.body = bodyVar .toString();
  187. notification.groupId = threadIdVar.toString();
  188. notification.category = categoryVar.toString();
  189. notification.soundToPlay = URL (soundVar.toString());
  190. notification.badgeNumber = (int) badgeVar;
  191. notification.properties = getNotificationPropertiesFromDictionaryVar (dictionaryVar);
  192. return notification;
  193. }
  194. }
  195. //==============================================================================
  196. struct PushNotificationsDelegate
  197. {
  198. PushNotificationsDelegate() : delegate ([getClass().createInstance() init])
  199. {
  200. Class::setThis (delegate.get(), this);
  201. id<NSApplicationDelegate> appDelegate = [[NSApplication sharedApplication] delegate];
  202. JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wundeclared-selector")
  203. if ([appDelegate respondsToSelector: @selector (setPushNotificationsDelegate:)])
  204. [appDelegate performSelector: @selector (setPushNotificationsDelegate:) withObject: delegate.get()];
  205. JUCE_END_IGNORE_WARNINGS_GCC_LIKE
  206. [NSUserNotificationCenter defaultUserNotificationCenter].delegate = delegate.get();
  207. }
  208. virtual ~PushNotificationsDelegate()
  209. {
  210. [NSUserNotificationCenter defaultUserNotificationCenter].delegate = nil;
  211. }
  212. virtual void registeredForRemoteNotifications (NSData* deviceToken) = 0;
  213. virtual void failedToRegisterForRemoteNotifications (NSError* error) = 0;
  214. virtual void didReceiveRemoteNotification (NSDictionary* userInfo) = 0;
  215. virtual void didDeliverNotification (NSUserNotification* notification) = 0;
  216. virtual void didActivateNotification (NSUserNotification* notification) = 0;
  217. virtual bool shouldPresentNotification (NSUserNotification* notification) = 0;
  218. protected:
  219. NSUniquePtr<NSObject<NSApplicationDelegate, NSUserNotificationCenterDelegate>> delegate;
  220. private:
  221. struct Class final : public ObjCClass<NSObject<NSApplicationDelegate, NSUserNotificationCenterDelegate>>
  222. {
  223. Class() : ObjCClass<NSObject<NSApplicationDelegate, NSUserNotificationCenterDelegate>> ("JucePushNotificationsDelegate_")
  224. {
  225. addIvar<PushNotificationsDelegate*> ("self");
  226. addMethod (@selector (application:didRegisterForRemoteNotificationsWithDeviceToken:), registeredForRemoteNotifications);
  227. addMethod (@selector (application:didFailToRegisterForRemoteNotificationsWithError:), failedToRegisterForRemoteNotifications);
  228. addMethod (@selector (application:didReceiveRemoteNotification:), didReceiveRemoteNotification);
  229. addMethod (@selector (userNotificationCenter:didDeliverNotification:), didDeliverNotification);
  230. addMethod (@selector (userNotificationCenter:didActivateNotification:), didActivateNotification);
  231. addMethod (@selector (userNotificationCenter:shouldPresentNotification:), shouldPresentNotification);
  232. registerClass();
  233. }
  234. //==============================================================================
  235. static PushNotificationsDelegate& getThis (id self) { return *getIvar<PushNotificationsDelegate*> (self, "self"); }
  236. static void setThis (id self, PushNotificationsDelegate* d) { object_setInstanceVariable (self, "self", d); }
  237. //==============================================================================
  238. static void registeredForRemoteNotifications (id self, SEL, NSApplication*,
  239. NSData* deviceToken) { getThis (self).registeredForRemoteNotifications (deviceToken); }
  240. static void failedToRegisterForRemoteNotifications (id self, SEL, NSApplication*,
  241. NSError* error) { getThis (self).failedToRegisterForRemoteNotifications (error); }
  242. static void didReceiveRemoteNotification (id self, SEL, NSApplication*,
  243. NSDictionary* userInfo) { getThis (self).didReceiveRemoteNotification (userInfo); }
  244. static void didDeliverNotification (id self, SEL, NSUserNotificationCenter*,
  245. NSUserNotification* notification) { getThis (self).didDeliverNotification (notification); }
  246. static void didActivateNotification (id self, SEL, NSUserNotificationCenter*,
  247. NSUserNotification* notification) { getThis (self).didActivateNotification (notification); }
  248. static bool shouldPresentNotification (id self, SEL, NSUserNotificationCenter*,
  249. NSUserNotification* notification) { return getThis (self).shouldPresentNotification (notification); }
  250. };
  251. //==============================================================================
  252. static Class& getClass()
  253. {
  254. static Class c;
  255. return c;
  256. }
  257. };
  258. //==============================================================================
  259. bool PushNotifications::Notification::isValid() const noexcept { return true; }
  260. //==============================================================================
  261. struct PushNotifications::Pimpl : private PushNotificationsDelegate
  262. {
  263. Pimpl (PushNotifications& p)
  264. : owner (p)
  265. {
  266. }
  267. void requestPermissionsWithSettings (const PushNotifications::Settings& settingsToUse)
  268. {
  269. if (@available (macOS 10.7, *))
  270. {
  271. settings = settingsToUse;
  272. NSRemoteNotificationType types = NSUInteger ((bool) settings.allowBadge);
  273. if (@available (macOS 10.8, *))
  274. {
  275. types |= (NSUInteger) ((settings.allowSound ? NSRemoteNotificationTypeSound : 0)
  276. | (settings.allowAlert ? NSRemoteNotificationTypeAlert : 0));
  277. }
  278. [[NSApplication sharedApplication] registerForRemoteNotificationTypes: types];
  279. }
  280. }
  281. void requestSettingsUsed()
  282. {
  283. if (@available (macOS 10.7, *))
  284. {
  285. settings.allowBadge = [NSApplication sharedApplication].enabledRemoteNotificationTypes & NSRemoteNotificationTypeBadge;
  286. if (@available (macOS 10.8, *))
  287. {
  288. settings.allowSound = [NSApplication sharedApplication].enabledRemoteNotificationTypes & NSRemoteNotificationTypeSound;
  289. settings.allowAlert = [NSApplication sharedApplication].enabledRemoteNotificationTypes & NSRemoteNotificationTypeAlert;
  290. }
  291. owner.listeners.call ([&] (Listener& l) { l.notificationSettingsReceived (settings); });
  292. }
  293. else
  294. {
  295. // no settings available
  296. owner.listeners.call ([] (Listener& l) { l.notificationSettingsReceived ({}); });
  297. }
  298. }
  299. bool areNotificationsEnabled() const { return true; }
  300. void sendLocalNotification (const Notification& n)
  301. {
  302. auto* notification = PushNotificationsDelegateDetailsOsx::juceNotificationToNSUserNotification (n);
  303. [[NSUserNotificationCenter defaultUserNotificationCenter] scheduleNotification: notification];
  304. }
  305. void getDeliveredNotifications() const
  306. {
  307. Array<PushNotifications::Notification> notifs;
  308. for (NSUserNotification* n in [NSUserNotificationCenter defaultUserNotificationCenter].deliveredNotifications)
  309. notifs.add (PushNotificationsDelegateDetailsOsx::nsUserNotificationToJuceNotification (n));
  310. owner.listeners.call ([&] (Listener& l) { l.deliveredNotificationsListReceived (notifs); });
  311. }
  312. void removeAllDeliveredNotifications()
  313. {
  314. [[NSUserNotificationCenter defaultUserNotificationCenter] removeAllDeliveredNotifications];
  315. }
  316. void removeDeliveredNotification (const String& identifier)
  317. {
  318. PushNotifications::Notification n;
  319. n.identifier = identifier;
  320. auto nsNotification = PushNotificationsDelegateDetailsOsx::juceNotificationToNSUserNotification (n);
  321. [[NSUserNotificationCenter defaultUserNotificationCenter] removeDeliveredNotification: nsNotification];
  322. }
  323. void setupChannels ([[maybe_unused]] const Array<ChannelGroup>& groups, [[maybe_unused]] const Array<Channel>& channels)
  324. {
  325. }
  326. void getPendingLocalNotifications() const
  327. {
  328. Array<PushNotifications::Notification> notifs;
  329. for (NSUserNotification* n in [NSUserNotificationCenter defaultUserNotificationCenter].scheduledNotifications)
  330. notifs.add (PushNotificationsDelegateDetailsOsx::nsUserNotificationToJuceNotification (n));
  331. owner.listeners.call ([&] (Listener& l) { l.pendingLocalNotificationsListReceived (notifs); });
  332. }
  333. void removePendingLocalNotification (const String& identifier)
  334. {
  335. PushNotifications::Notification n;
  336. n.identifier = identifier;
  337. auto nsNotification = PushNotificationsDelegateDetailsOsx::juceNotificationToNSUserNotification (n);
  338. [[NSUserNotificationCenter defaultUserNotificationCenter] removeScheduledNotification: nsNotification];
  339. }
  340. void removeAllPendingLocalNotifications()
  341. {
  342. for (NSUserNotification* n in [NSUserNotificationCenter defaultUserNotificationCenter].scheduledNotifications)
  343. [[NSUserNotificationCenter defaultUserNotificationCenter] removeScheduledNotification: n];
  344. }
  345. String getDeviceToken()
  346. {
  347. // You need to call requestPermissionsWithSettings() first.
  348. jassert (initialised);
  349. return deviceToken;
  350. }
  351. //==============================================================================
  352. //PushNotificationsDelegate
  353. void registeredForRemoteNotifications (NSData* deviceTokenToUse) override
  354. {
  355. deviceToken = [deviceTokenToUse]() -> String
  356. {
  357. auto length = deviceTokenToUse.length;
  358. if (auto* buffer = (const unsigned char*) deviceTokenToUse.bytes)
  359. {
  360. NSMutableString* hexString = [NSMutableString stringWithCapacity: (length * 2)];
  361. for (NSUInteger i = 0; i < length; ++i)
  362. [hexString appendFormat:@"%02x", buffer[i]];
  363. return nsStringToJuce ([hexString copy]);
  364. }
  365. return {};
  366. }();
  367. initialised = true;
  368. owner.listeners.call ([&] (Listener& l) { l.deviceTokenRefreshed (deviceToken); });
  369. }
  370. void failedToRegisterForRemoteNotifications ([[maybe_unused]] NSError* error) override
  371. {
  372. deviceToken.clear();
  373. }
  374. void didReceiveRemoteNotification (NSDictionary* userInfo) override
  375. {
  376. auto n = PushNotificationsDelegateDetailsOsx::nsDictionaryToJuceNotification (userInfo);
  377. owner.listeners.call ([&] (Listener& l) { l.handleNotification (true, n); });
  378. }
  379. void didDeliverNotification ([[maybe_unused]] NSUserNotification* notification) override
  380. {
  381. }
  382. void didActivateNotification (NSUserNotification* notification) override
  383. {
  384. auto n = PushNotificationsDelegateDetailsOsx::nsUserNotificationToJuceNotification (notification);
  385. if (notification.activationType == NSUserNotificationActivationTypeContentsClicked)
  386. {
  387. owner.listeners.call ([&] (Listener& l) { l.handleNotification (notification.remote, n); });
  388. }
  389. else
  390. {
  391. const auto actionIdentifier = nsStringToJuce ([&]
  392. {
  393. if (@available (macOS 10.10, *))
  394. if (notification.additionalActivationAction != nil)
  395. return notification.additionalActivationAction.identifier;
  396. return notification.actionButtonTitle;
  397. }());
  398. auto reply = notification.activationType == NSUserNotificationActivationTypeReplied
  399. ? nsStringToJuce ([notification.response string])
  400. : String();
  401. owner.listeners.call ([&] (Listener& l) { l.handleNotificationAction (notification.remote, n, actionIdentifier, reply); });
  402. }
  403. }
  404. bool shouldPresentNotification (NSUserNotification*) override { return true; }
  405. void subscribeToTopic ([[maybe_unused]] const String& topic) {}
  406. void unsubscribeFromTopic ([[maybe_unused]] const String& topic) {}
  407. void sendUpstreamMessage ([[maybe_unused]] const String& serverSenderId,
  408. [[maybe_unused]] const String& collapseKey,
  409. [[maybe_unused]] const String& messageId,
  410. [[maybe_unused]] const String& messageType,
  411. [[maybe_unused]] int timeToLive,
  412. [[maybe_unused]] const StringPairArray& additionalData)
  413. {
  414. }
  415. private:
  416. PushNotifications& owner;
  417. bool initialised = false;
  418. String deviceToken;
  419. PushNotifications::Settings settings;
  420. };
  421. } // namespace juce
  422. JUCE_END_IGNORE_WARNINGS_GCC_LIKE