From b8b304e4cdcf126261ad11c6543a56d115984312 Mon Sep 17 00:00:00 2001 From: Lukasz Kozakiewicz Date: Wed, 25 Oct 2017 18:44:02 +0200 Subject: [PATCH] PushNotifications: add OSX implementation. --- .../PushNotificationsDemo.jucer | 28 +- .../Source/MainComponent.cpp | 486 ++++++++++----- .../Source/MainComponent.h | 525 ++++------------- .../ProjectSaving/jucer_ProjectExport_Xcode.h | 16 +- .../juce_core/native/juce_osx_ObjCHelpers.h | 126 ++++ .../native/juce_mac_MessageManager.mm | 95 ++- modules/juce_gui_extra/juce_gui_extra.cpp | 6 + .../misc/juce_PushNotifications.cpp | 6 +- .../misc/juce_PushNotifications.h | 26 +- .../native/juce_ios_PushNotifications.cpp | 136 +---- .../native/juce_mac_PushNotifications.cpp | 557 ++++++++++++++++++ 11 files changed, 1313 insertions(+), 694 deletions(-) create mode 100644 modules/juce_gui_extra/native/juce_mac_PushNotifications.cpp diff --git a/examples/PushNotificationsDemo/PushNotificationsDemo.jucer b/examples/PushNotificationsDemo/PushNotificationsDemo.jucer index cc81f30f11..5fcfeffdb1 100644 --- a/examples/PushNotificationsDemo/PushNotificationsDemo.jucer +++ b/examples/PushNotificationsDemo/PushNotificationsDemo.jucer @@ -31,19 +31,25 @@ + file="BinaryResources/sounds/demonstrative.caf" xcodeResource="0"/> - - + file="BinaryResources/sounds/demonstrative.mp3" xcodeResource="1"/> + + + file="BinaryResources/sounds/jinglebellssms.caf" xcodeResource="0"/> - - - - + file="BinaryResources/sounds/jinglebellssms.mp3" xcodeResource="1"/> + + + + @@ -104,7 +110,7 @@ - + diff --git a/examples/PushNotificationsDemo/Source/MainComponent.cpp b/examples/PushNotificationsDemo/Source/MainComponent.cpp index 3712f03e10..b183fd74a2 100644 --- a/examples/PushNotificationsDemo/Source/MainComponent.cpp +++ b/examples/PushNotificationsDemo/Source/MainComponent.cpp @@ -29,7 +29,10 @@ //============================================================================== MainContentComponent::MainContentComponent() { - #if JUCE_ANDROID || JUCE_IOS + setupControls(); + distributeControls(); + + #if JUCE_PUSH_NOTIFICATIONS addAndMakeVisible (headerLabel); addAndMakeVisible (mainTabs); addAndMakeVisible (sendButton); @@ -40,12 +43,18 @@ MainContentComponent::MainContentComponent() headerLabel.setJustificationType (Justification::centred); notAvailableYetLabel.setJustificationType (Justification::centred); + #if JUCE_MAC + StringArray tabNames { "Params1", "Params2", "Params3", "Params4" }; + #else + StringArray tabNames { "Req. params", "Opt. params1", "Opt. params2", "Opt. params3" }; + #endif + const auto colour = getLookAndFeel().findColour (ResizableWindow::backgroundColourId); - localNotificationsTabs.addTab ("Req. params", colour, &requiredParamsView, false); - localNotificationsTabs.addTab ("Opt. params1", colour, &optionalParamsOneView, false); + localNotificationsTabs.addTab (tabNames[0], colour, ¶msOneView, false); + localNotificationsTabs.addTab (tabNames[1], colour, ¶msTwoView, false); #if JUCE_ANDROID - localNotificationsTabs.addTab ("Opt. params2", colour, &optionalParamsTwoView, false); - localNotificationsTabs.addTab ("Opt. params3", colour, &optionalParamsThreeView, false); + localNotificationsTabs.addTab (tabNames[2], colour, ¶msThreeView, false); + localNotificationsTabs.addTab (tabNames[3], colour, ¶msFourView, false); #endif localNotificationsTabs.addTab ("Aux. actions", colour, &auxActionsView, false); @@ -63,7 +72,7 @@ MainContentComponent::MainContentComponent() auxActionsView.getDeliveredNotificationsButton .addListener (this); auxActionsView.removeDeliveredNotifWithIdButton.addListener (this); auxActionsView.removeAllDeliveredNotifsButton .addListener (this); - #if JUCE_IOS + #if JUCE_IOS || JUCE_MAC auxActionsView.getPendingNotificationsButton .addListener (this); auxActionsView.removePendingNotifWithIdButton.addListener (this); auxActionsView.removeAllPendingNotifsButton .addListener (this); @@ -74,16 +83,16 @@ MainContentComponent::MainContentComponent() remoteView.subscribeToSportsButton .addListener (this); remoteView.unsubscribeFromSportsButton.addListener (this); - optionalParamsThreeView.accentColourButton.addListener (this); - optionalParamsThreeView.ledColourButton .addListener (this); + paramControls.accentColourButton.addListener (this); + paramControls.ledColourButton .addListener (this); jassert (PushNotifications::getInstance()->areNotificationsEnabled()); PushNotifications::getInstance()->addListener (this); - #if JUCE_IOS - optionalParamsOneView.fireInComboBox.addListener (this); - PushNotifications::getInstance()->requestPermissionsWithSettings (getIosSettings()); + #if JUCE_IOS || JUCE_MAC + paramControls.fireInComboBox.addListener (this); + PushNotifications::getInstance()->requestPermissionsWithSettings (getNotificationSettings()); #elif JUCE_ANDROID PushNotifications::ChannelGroup cg { "demoGroup", "demo group" }; PushNotifications::getInstance()->setupChannels ({{ cg }}, getAndroidChannels()); @@ -95,6 +104,200 @@ MainContentComponent::~MainContentComponent() PushNotifications::getInstance()->removeListener (this); } +void MainContentComponent::setupControls() +{ + auto& pc = paramControls; + + StringArray categories { "okCategory", "okCancelCategory", "textCategory" }; + + for (const auto& c : categories) + pc.categoryComboBox.addItem (c, pc.categoryComboBox.getNumItems() + 1); + pc.categoryComboBox.setSelectedItemIndex (0); + + for (int i = 1; i <= 3; ++i) + pc.channelIdComboBox.addItem (String (i), i); + pc.channelIdComboBox.setSelectedItemIndex (0); + + for (int i = 0; i < 5; ++i) + pc.iconComboBox.addItem ("icon" + String (i + 1), i + 1); + pc.iconComboBox.setSelectedItemIndex (0); + + #if JUCE_MAC + pc.iconComboBox.addItem ("none", 100); + #endif + + pc.fireInComboBox.addItem ("Now", 1); + + for (int i = 1; i < 11; ++i) + pc.fireInComboBox.addItem (String (10 * i) + "seconds", i + 1); + pc.fireInComboBox.setSelectedItemIndex (0); + + pc.largeIconComboBox.addItem ("none", 1); + + for (int i = 1; i < 5; ++i) + pc.largeIconComboBox.addItem ("icon" + String (i), i + 1); + pc.largeIconComboBox.setSelectedItemIndex (0); + + pc.badgeIconComboBox.addItem ("none", 1); + pc.badgeIconComboBox.addItem ("small", 2); + pc.badgeIconComboBox.addItem ("large", 3); + pc.badgeIconComboBox.setSelectedItemIndex (2); + + pc.actionsComboBox.addItem ("none", 1); + pc.actionsComboBox.addItem ("ok-cancel", 2); + pc.actionsComboBox.addItem ("text-input", 3); + #if JUCE_ANDROID + pc.actionsComboBox.addItem ("ok-cancel-icons", 4); + pc.actionsComboBox.addItem ("text-input-limited_responses", 5); + #endif + pc.actionsComboBox.setSelectedItemIndex (0); + + for (int i = 0; i < 7; ++i) + pc.badgeNumberComboBox.addItem (String (i), i + 1); + pc.badgeNumberComboBox.setSelectedItemIndex (0); + + #if JUCE_IOS + String prefix = "sounds/"; + String extension = ".caf"; + #else + String prefix; + String extension; + #endif + + pc.soundToPlayComboBox.addItem ("none", 1); + pc.soundToPlayComboBox.addItem ("default_os_sound", 2); + pc.soundToPlayComboBox.addItem (prefix + "demonstrative" + extension, 3); + pc.soundToPlayComboBox.addItem (prefix + "isntit" + extension, 4); + pc.soundToPlayComboBox.addItem (prefix + "jinglebellssms" + extension, 5); + pc.soundToPlayComboBox.addItem (prefix + "served" + extension, 6); + pc.soundToPlayComboBox.addItem (prefix + "solemn" + extension, 7); + pc.soundToPlayComboBox.setSelectedItemIndex (1); + + for (int i = 0; i < 11; ++i) + { + pc.progressMaxComboBox .addItem (String (i * 10) + "%", i + 1); + pc.progressCurrentComboBox.addItem (String (i * 10) + "%", i + 1); + } + + pc.progressMaxComboBox .setSelectedItemIndex (0); + pc.progressCurrentComboBox.setSelectedItemIndex (0); + + pc.notifCategoryComboBox.addItem ("unspecified", 1); + pc.notifCategoryComboBox.addItem ("alarm", 2); + pc.notifCategoryComboBox.addItem ("call", 3); + pc.notifCategoryComboBox.addItem ("email", 4); + pc.notifCategoryComboBox.addItem ("error", 5); + pc.notifCategoryComboBox.addItem ("event", 6); + pc.notifCategoryComboBox.addItem ("message", 7); + pc.notifCategoryComboBox.addItem ("progress", 8); + pc.notifCategoryComboBox.addItem ("promo", 9); + pc.notifCategoryComboBox.addItem ("recommendation", 10); + pc.notifCategoryComboBox.addItem ("reminder", 11); + pc.notifCategoryComboBox.addItem ("service", 12); + pc.notifCategoryComboBox.addItem ("social", 13); + pc.notifCategoryComboBox.addItem ("status", 14); + pc.notifCategoryComboBox.addItem ("system", 15); + pc.notifCategoryComboBox.addItem ("transport", 16); + pc.notifCategoryComboBox.setSelectedItemIndex (0); + + for (int i = -2; i < 3; ++i) + pc.priorityComboBox.addItem (String (i), i + 3); + pc.priorityComboBox.setSelectedItemIndex (2); + + pc.lockScreenVisibilityComboBox.addItem ("don't show", 1); + pc.lockScreenVisibilityComboBox.addItem ("show partially", 2); + pc.lockScreenVisibilityComboBox.addItem ("show completely", 3); + pc.lockScreenVisibilityComboBox.setSelectedItemIndex (1); + + pc.groupAlertBehaviourComboBox.addItem ("alert all", 1); + pc.groupAlertBehaviourComboBox.addItem ("alert summary", 2); + pc.groupAlertBehaviourComboBox.addItem ("alert children", 3); + pc.groupAlertBehaviourComboBox.setSelectedItemIndex (0); + + pc.timeoutAfterComboBox.addItem ("No timeout", 1); + + for (int i = 0; i < 10; ++i) + { + pc.ledMsToBeOnComboBox .addItem (String (i * 200) + "ms", i + 1); + pc.ledMsToBeOffComboBox .addItem (String (i * 200) + "ms", i + 1); + pc.vibratorMsToBeOnComboBox .addItem (String (i * 500) + "ms", i + 1); + pc.vibratorMsToBeOffComboBox.addItem (String (i * 500) + "ms", i + 1); + pc.timeoutAfterComboBox.addItem (String (5000 + 1000 * i) + "ms", i + 2); + } + + pc.ledMsToBeOnComboBox .setSelectedItemIndex (5); + pc.ledMsToBeOffComboBox .setSelectedItemIndex (5); + pc.vibratorMsToBeOnComboBox .setSelectedItemIndex (0); + pc.vibratorMsToBeOffComboBox.setSelectedItemIndex (0); + pc.timeoutAfterComboBox.setSelectedItemIndex (0); + + pc.timestampVisibilityComboBox.addItem ("off", 1); + pc.timestampVisibilityComboBox.addItem ("on", 2); + pc.timestampVisibilityComboBox.addItem ("chronometer", 3); + pc.timestampVisibilityComboBox.addItem ("count down", 4); + pc.timestampVisibilityComboBox.setSelectedItemIndex (1); +} + +void MainContentComponent::distributeControls() +{ + auto& pc = paramControls; + + paramsOneView.addRowComponent (new RowComponent (pc.identifierLabel, pc.identifierEditor)); + paramsOneView.addRowComponent (new RowComponent (pc.titleLabel, pc.titleEditor)); + paramsOneView.addRowComponent (new RowComponent (pc.bodyLabel, pc.bodyEditor, 4)); + #if JUCE_IOS + paramsOneView.addRowComponent (new RowComponent (pc.categoryLabel, pc.categoryComboBox)); + #elif JUCE_ANDROID + paramsOneView.addRowComponent (new RowComponent (pc.channelIdLabel, pc.channelIdComboBox)); + #endif + #if JUCE_ANDROID || JUCE_MAC + paramsOneView.addRowComponent (new RowComponent (pc.iconLabel, pc.iconComboBox)); + #endif + + paramsTwoView.addRowComponent (new RowComponent (pc.subtitleLabel, pc.subtitleEditor)); + #if ! JUCE_MAC + paramsTwoView.addRowComponent (new RowComponent (pc.badgeNumberLabel, pc.badgeNumberComboBox)); + #endif + paramsTwoView.addRowComponent (new RowComponent (pc.soundToPlayLabel, pc.soundToPlayComboBox)); + paramsTwoView.addRowComponent (new RowComponent (pc.propertiesLabel, pc.propertiesEditor, 3)); + #if JUCE_IOS || JUCE_MAC + paramsTwoView.addRowComponent (new RowComponent (pc.fireInLabel, pc.fireInComboBox)); + paramsTwoView.addRowComponent (new RowComponent (pc.repeatLabel, pc.repeatButton)); + #elif JUCE_ANDROID + paramsTwoView.addRowComponent (new RowComponent (pc.largeIconLabel, pc.largeIconComboBox)); + paramsTwoView.addRowComponent (new RowComponent (pc.badgeIconLabel, pc.badgeIconComboBox)); + paramsTwoView.addRowComponent (new RowComponent (pc.tickerTextLabel, pc.tickerTextEditor)); + paramsTwoView.addRowComponent (new RowComponent (pc.autoCancelLabel, pc.autoCancelButton)); + paramsTwoView.addRowComponent (new RowComponent (pc.alertOnlyOnceLabel, pc.alertOnlyOnceButton)); + #endif + #if JUCE_ANDROID || JUCE_MAC + paramsTwoView.addRowComponent (new RowComponent (pc.actionsLabel, pc.actionsComboBox)); + #endif + #if JUCE_ANDROID + paramsThreeView.addRowComponent (new RowComponent (pc.progressMaxLabel, pc.progressMaxComboBox)); + paramsThreeView.addRowComponent (new RowComponent (pc.progressCurrentLabel, pc.progressCurrentComboBox)); + paramsThreeView.addRowComponent (new RowComponent (pc.progressIndeterminateLabel, pc.progressIndeterminateButton)); + paramsThreeView.addRowComponent (new RowComponent (pc.categoryLabel, pc.categoryComboBox)); + paramsThreeView.addRowComponent (new RowComponent (pc.priorityLabel, pc.priorityComboBox)); + paramsThreeView.addRowComponent (new RowComponent (pc.personLabel, pc.personEditor)); + paramsThreeView.addRowComponent (new RowComponent (pc.lockScreenVisibilityLabel, pc.lockScreenVisibilityComboBox)); + paramsThreeView.addRowComponent (new RowComponent (pc.groupIdLabel, pc.groupIdEditor)); + paramsThreeView.addRowComponent (new RowComponent (pc.sortKeyLabel, pc.sortKeyEditor)); + paramsThreeView.addRowComponent (new RowComponent (pc.groupSummaryLabel, pc.groupSummaryButton)); + paramsThreeView.addRowComponent (new RowComponent (pc.groupAlertBehaviourLabel, pc.groupAlertBehaviourComboBox)); + paramsFourView.addRowComponent (new RowComponent (pc.accentColourLabel, pc.accentColourButton)); + paramsFourView.addRowComponent (new RowComponent (pc.ledColourLabel, pc.ledColourButton)); + paramsFourView.addRowComponent (new RowComponent (pc.ledMsToBeOffLabel, pc.ledMsToBeOffComboBox)); + paramsFourView.addRowComponent (new RowComponent (pc.ledMsToBeOnLabel, pc.ledMsToBeOnComboBox)); + paramsFourView.addRowComponent (new RowComponent (pc.vibratorMsToBeOffLabel, pc.vibratorMsToBeOffComboBox)); + paramsFourView.addRowComponent (new RowComponent (pc.vibratorMsToBeOnLabel, pc.vibratorMsToBeOnComboBox)); + paramsFourView.addRowComponent (new RowComponent (pc.localOnlyLabel, pc.localOnlyButton)); + paramsFourView.addRowComponent (new RowComponent (pc.ongoingLabel, pc.ongoingButton)); + paramsFourView.addRowComponent (new RowComponent (pc.timestampVisibilityLabel, pc.timestampVisibilityComboBox)); + paramsFourView.addRowComponent (new RowComponent (pc.timeoutAfterLabel, pc.timeoutAfterComboBox)); + #endif +} + void MainContentComponent::paint (Graphics& g) { g.fillAll (getLookAndFeel().findColour (ResizableWindow::backgroundColourId)); @@ -117,9 +320,9 @@ void MainContentComponent::buttonClicked (Button* b) { if (b == &sendButton) sendLocalNotification(); - else if (b == &optionalParamsThreeView.accentColourButton) + else if (b == ¶mControls.accentColourButton) setupAccentColour(); - else if (b == &optionalParamsThreeView.ledColourButton) + else if (b == ¶mControls.ledColourButton) setupLedColour(); else if (b == &auxActionsView.getDeliveredNotificationsButton) getDeliveredNotifications(); @@ -127,7 +330,7 @@ void MainContentComponent::buttonClicked (Button* b) PushNotifications::getInstance()->removeDeliveredNotification (auxActionsView.deliveredNotifIdentifier.getText()); else if (b == &auxActionsView.removeAllDeliveredNotifsButton) PushNotifications::getInstance()->removeAllDeliveredNotifications(); - #if JUCE_IOS + #if JUCE_IOS || JUCE_MAC else if (b == &auxActionsView.getPendingNotificationsButton) PushNotifications::getInstance()->getPendingLocalNotifications(); else if (b == &auxActionsView.removePendingNotifWithIdButton) @@ -141,7 +344,10 @@ void MainContentComponent::buttonClicked (Button* b) DBG ("token = " + token); - NativeMessageBox::showMessageBoxAsync (AlertWindow::InfoIcon, "Device token", token); + if (token.isEmpty()) + showRemoteInstructions(); + else + NativeMessageBox::showMessageBoxAsync (AlertWindow::InfoIcon, "Device token", token); } #if JUCE_ANDROID else if (b == &remoteView.sendRemoteMessageButton) @@ -172,19 +378,15 @@ void MainContentComponent::buttonClicked (Button* b) void MainContentComponent::comboBoxChanged (ComboBox* comboBoxThatHasChanged) { - #if JUCE_IOS - if (comboBoxThatHasChanged == &optionalParamsOneView.fireInComboBox) + if (comboBoxThatHasChanged == ¶mControls.fireInComboBox) { - const bool repeatsAllowed = optionalParamsOneView.fireInComboBox.getSelectedItemIndex() >= 6; + const bool repeatsAllowed = paramControls.fireInComboBox.getSelectedItemIndex() >= 6; - optionalParamsOneView.repeatButton.setEnabled (repeatsAllowed); + paramControls.repeatButton.setEnabled (repeatsAllowed); if (! repeatsAllowed) - optionalParamsOneView.repeatButton.setToggleState (false, NotificationType::sendNotification); + paramControls.repeatButton.setToggleState (false, NotificationType::sendNotification); } - #else - ignoreUnused (comboBoxThatHasChanged); - #endif } void MainContentComponent::sendLocalNotification() @@ -222,23 +424,32 @@ void MainContentComponent::sendLocalNotification() void MainContentComponent::fillRequiredParams (PushNotifications::Notification& n) { - n.identifier = requiredParamsView.identifierEditor.getText(); - n.title = requiredParamsView.titleEditor.getText(); - n.body = requiredParamsView.bodyEditor.getText(); + n.identifier = paramControls.identifierEditor.getText(); + n.title = paramControls.titleEditor.getText(); + n.body = paramControls.bodyEditor.getText(); #if JUCE_IOS - n.category = requiredParamsView.categories[requiredParamsView.categoryComboBox.getSelectedItemIndex()]; - #elif JUCE_ANDROID - if (requiredParamsView.iconComboBox.getSelectedItemIndex() == 0) - n.icon = "ic_stat_name"; - else if (requiredParamsView.iconComboBox.getSelectedItemIndex() == 1) - n.icon = "ic_stat_name2"; - else if (requiredParamsView.iconComboBox.getSelectedItemIndex() == 2) - n.icon = "ic_stat_name3"; - else if (requiredParamsView.iconComboBox.getSelectedItemIndex() == 3) - n.icon = "ic_stat_name4"; - else - n.icon = "ic_stat_name5"; + n.category = paramControls.categoryComboBox.getText(); + #elif JUCE_ANDROID || JUCE_MAC + #if JUCE_MAC + String prefix = "images/"; + String extension = ".png"; + #else + String prefix; + String extension; + #endif + if (paramControls.iconComboBox.getSelectedItemIndex() == 0) + n.icon = prefix + "ic_stat_name" + extension; + else if (paramControls.iconComboBox.getSelectedItemIndex() == 1) + n.icon = prefix + "ic_stat_name2" + extension; + else if (paramControls.iconComboBox.getSelectedItemIndex() == 2) + n.icon = prefix + "ic_stat_name3" + extension; + else if (paramControls.iconComboBox.getSelectedItemIndex() == 3) + n.icon = prefix + "ic_stat_name4" + extension; + else if (paramControls.iconComboBox.getSelectedItemIndex() == 4) + n.icon = prefix + "ic_stat_name5" + extension; + #endif + #if JUCE_ANDROID // Note: this is not strictly speaking required param, just doing it here because it is the fastest way! n.publicVersion = new PushNotifications::Notification(); n.publicVersion->identifier = "blahblahblah"; @@ -247,69 +458,59 @@ void MainContentComponent::fillRequiredParams (PushNotifications::Notification& n.publicVersion->icon = n.icon; #if __ANDROID_API__ >= 26 - n.channelId = String (requiredParamsView.channelIdComboBox.getSelectedItemIndex() + 1); + n.channelId = String (paramControls.channelIdComboBox.getSelectedItemIndex() + 1); #endif #endif } void MainContentComponent::fillOptionalParamsOne (PushNotifications::Notification& n) { - n.subtitle = optionalParamsOneView.subtitleEditor.getText(); - n.badgeNumber = optionalParamsOneView.badgeNumberComboBox.getSelectedItemIndex(); + n.subtitle = paramControls.subtitleEditor.getText(); + n.badgeNumber = paramControls.badgeNumberComboBox.getSelectedItemIndex(); - if (optionalParamsOneView.soundToPlayComboBox.getSelectedItemIndex() > 0) - n.soundToPlay = URL (optionalParamsOneView.soundToPlayComboBox.getItemText (optionalParamsOneView.soundToPlayComboBox.getSelectedItemIndex())); + if (paramControls.soundToPlayComboBox.getSelectedItemIndex() > 0) + n.soundToPlay = URL (paramControls.soundToPlayComboBox.getItemText (paramControls.soundToPlayComboBox.getSelectedItemIndex())); - n.properties = JSON::parse (optionalParamsOneView.propertiesEditor.getText()); + n.properties = JSON::parse (paramControls.propertiesEditor.getText()); - #if JUCE_IOS - n.triggerIntervalSec = double (optionalParamsOneView.fireInComboBox.getSelectedItemIndex() * 10); - n.repeat = optionalParamsOneView.repeatButton.getToggleState(); + #if JUCE_IOS || JUCE_MAC + n.triggerIntervalSec = double (paramControls.fireInComboBox.getSelectedItemIndex() * 10); + n.repeat = paramControls.repeatButton.getToggleState(); #elif JUCE_ANDROID - if (optionalParamsOneView.largeIconComboBox.getSelectedItemIndex() == 1) + if (paramControls.largeIconComboBox.getSelectedItemIndex() == 1) n.largeIcon = ImageFileFormat::loadFrom (BinaryData::ic_stat_name6_png, BinaryData::ic_stat_name6_pngSize); - else if (optionalParamsOneView.largeIconComboBox.getSelectedItemIndex() == 2) + else if (paramControls.largeIconComboBox.getSelectedItemIndex() == 2) n.largeIcon = ImageFileFormat::loadFrom (BinaryData::ic_stat_name7_png, BinaryData::ic_stat_name7_pngSize); - else if (optionalParamsOneView.largeIconComboBox.getSelectedItemIndex() == 3) + else if (paramControls.largeIconComboBox.getSelectedItemIndex() == 3) n.largeIcon = ImageFileFormat::loadFrom (BinaryData::ic_stat_name8_png, BinaryData::ic_stat_name8_pngSize); - else if (optionalParamsOneView.largeIconComboBox.getSelectedItemIndex() == 4) + else if (paramControls.largeIconComboBox.getSelectedItemIndex() == 4) n.largeIcon = ImageFileFormat::loadFrom (BinaryData::ic_stat_name9_png, BinaryData::ic_stat_name9_pngSize); - else if (optionalParamsOneView.largeIconComboBox.getSelectedItemIndex() == 5) + else if (paramControls.largeIconComboBox.getSelectedItemIndex() == 5) n.largeIcon = ImageFileFormat::loadFrom (BinaryData::ic_stat_name10_png, BinaryData::ic_stat_name10_pngSize); - n.badgeIconType = (PushNotifications::Notification::BadgeIconType) optionalParamsOneView.badgeIconComboBox.getSelectedItemIndex(); - n.tickerText = optionalParamsOneView.tickerTextEditor.getText(); + n.badgeIconType = (PushNotifications::Notification::BadgeIconType) paramControls.badgeIconComboBox.getSelectedItemIndex(); + n.tickerText = paramControls.tickerTextEditor.getText(); - n.shouldAutoCancel = optionalParamsOneView.autoCancelButton.getToggleState(); - n.alertOnlyOnce = optionalParamsOneView.alertOnlyOnceButton.getToggleState(); + n.shouldAutoCancel = paramControls.autoCancelButton.getToggleState(); + n.alertOnlyOnce = paramControls.alertOnlyOnceButton.getToggleState(); + #endif - if (optionalParamsOneView.actionsComboBox.getSelectedItemIndex() == 1) + #if JUCE_ANDROID || JUCE_MAC + if (paramControls.actionsComboBox.getSelectedItemIndex() == 1) { PushNotifications::Notification::Action a, a2; a .style = PushNotifications::Notification::Action::button; a2.style = PushNotifications::Notification::Action::button; - a .title = "Ok"; - a2.title = "Cancel"; + a .title = a .identifier = "Ok"; + a2.title = a2.identifier = "Cancel"; n.actions.add (a); n.actions.add (a2); } - else if (optionalParamsOneView.actionsComboBox.getSelectedItemIndex() == 2) + else if (paramControls.actionsComboBox.getSelectedItemIndex() == 2) { PushNotifications::Notification::Action a, a2; - a .title = "Ok"; - a2.title = "Cancel"; - a .style = PushNotifications::Notification::Action::button; - a2.style = PushNotifications::Notification::Action::button; - a .icon = "ic_stat_name4"; - a2.icon = "ic_stat_name5"; - n.actions.add (a); - n.actions.add (a2); - } - else if (optionalParamsOneView.actionsComboBox.getSelectedItemIndex() == 3) - { - PushNotifications::Notification::Action a, a2; - a .title = "Input Text Here"; - a2.title = "No"; + a .title = a .identifier = "Input Text Here"; + a2.title = a2.identifier = "No"; a .style = PushNotifications::Notification::Action::text; a2.style = PushNotifications::Notification::Action::button; a .icon = "ic_stat_name4"; @@ -318,11 +519,23 @@ void MainContentComponent::fillOptionalParamsOne (PushNotifications::Notificatio n.actions.add (a); n.actions.add (a2); } - else if (optionalParamsOneView.actionsComboBox.getSelectedItemIndex() == 4) + else if (paramControls.actionsComboBox.getSelectedItemIndex() == 3) { PushNotifications::Notification::Action a, a2; - a .title = "Input Text Here"; - a2.title = "No"; + a .title = a .identifier = "Ok"; + a2.title = a2.identifier = "Cancel"; + a .style = PushNotifications::Notification::Action::button; + a2.style = PushNotifications::Notification::Action::button; + a .icon = "ic_stat_name4"; + a2.icon = "ic_stat_name5"; + n.actions.add (a); + n.actions.add (a2); + } + else if (paramControls.actionsComboBox.getSelectedItemIndex() == 4) + { + PushNotifications::Notification::Action a, a2; + a .title = a .identifier = "Input Text Here"; + a2.title = a2.identifier = "No"; a .style = PushNotifications::Notification::Action::text; a2.style = PushNotifications::Notification::Action::button; a .icon = "ic_stat_name4"; @@ -342,102 +555,102 @@ void MainContentComponent::fillOptionalParamsTwo (PushNotifications::Notificatio using Notification = PushNotifications::Notification; Notification::Progress progress; - progress.max = optionalParamsTwoView.progressMaxComboBox.getSelectedItemIndex() * 10; - progress.current = optionalParamsTwoView.progressCurrentComboBox.getSelectedItemIndex() * 10; - progress.indeterminate = optionalParamsTwoView.progressIndeterminateButton.getToggleState(); + progress.max = paramControls.progressMaxComboBox.getSelectedItemIndex() * 10; + progress.current = paramControls.progressCurrentComboBox.getSelectedItemIndex() * 10; + progress.indeterminate = paramControls.progressIndeterminateButton.getToggleState(); n.progress = progress; - n.person = optionalParamsTwoView.personEditor.getText(); - n.type = Notification::Type (optionalParamsTwoView.categoryComboBox.getSelectedItemIndex()); - n.priority = Notification::Priority (optionalParamsTwoView.priorityComboBox.getSelectedItemIndex() - 2); - n.lockScreenAppearance = Notification::LockScreenAppearance (optionalParamsTwoView.lockScreenVisibilityComboBox.getSelectedItemIndex() - 1); - n.groupId = optionalParamsTwoView.groupIdEditor.getText(); - n.groupSortKey = optionalParamsTwoView.sortKeyEditor.getText(); - n.groupSummary = optionalParamsTwoView.groupSummaryButton.getToggleState(); - n.groupAlertBehaviour = Notification::GroupAlertBehaviour (optionalParamsTwoView.groupAlertBehaviourComboBox.getSelectedItemIndex()); + n.person = paramControls.personEditor.getText(); + n.type = Notification::Type (paramControls.categoryComboBox.getSelectedItemIndex()); + n.priority = Notification::Priority (paramControls.priorityComboBox.getSelectedItemIndex() - 2); + n.lockScreenAppearance = Notification::LockScreenAppearance (paramControls.lockScreenVisibilityComboBox.getSelectedItemIndex() - 1); + n.groupId = paramControls.groupIdEditor.getText(); + n.groupSortKey = paramControls.sortKeyEditor.getText(); + n.groupSummary = paramControls.groupSummaryButton.getToggleState(); + n.groupAlertBehaviour = Notification::GroupAlertBehaviour (paramControls.groupAlertBehaviourComboBox.getSelectedItemIndex()); } void MainContentComponent::fillOptionalParamsThree (PushNotifications::Notification& n) { - n.accentColour = optionalParamsThreeView.accentColourButton.findColour (TextButton::buttonColourId, false); - n.ledColour = optionalParamsThreeView.ledColourButton .findColour (TextButton::buttonColourId, false); + n.accentColour = paramControls.accentColourButton.findColour (TextButton::buttonColourId, false); + n.ledColour = paramControls.ledColourButton .findColour (TextButton::buttonColourId, false); using Notification = PushNotifications::Notification; Notification::LedBlinkPattern ledBlinkPattern; - ledBlinkPattern.msToBeOn = optionalParamsThreeView.ledMsToBeOnComboBox .getSelectedItemIndex() * 200; - ledBlinkPattern.msToBeOff = optionalParamsThreeView.ledMsToBeOffComboBox.getSelectedItemIndex() * 200; + ledBlinkPattern.msToBeOn = paramControls.ledMsToBeOnComboBox .getSelectedItemIndex() * 200; + ledBlinkPattern.msToBeOff = paramControls.ledMsToBeOffComboBox.getSelectedItemIndex() * 200; n.ledBlinkPattern = ledBlinkPattern; Array vibrationPattern; - if (optionalParamsThreeView.vibratorMsToBeOnComboBox .getSelectedItemIndex() > 0 && - optionalParamsThreeView.vibratorMsToBeOffComboBox.getSelectedItemIndex() > 0) + if (paramControls.vibratorMsToBeOnComboBox .getSelectedItemIndex() > 0 && + paramControls.vibratorMsToBeOffComboBox.getSelectedItemIndex() > 0) { - vibrationPattern.add (optionalParamsThreeView.vibratorMsToBeOffComboBox.getSelectedItemIndex() * 500); - vibrationPattern.add (optionalParamsThreeView.vibratorMsToBeOnComboBox .getSelectedItemIndex() * 500); - vibrationPattern.add (2 * optionalParamsThreeView.vibratorMsToBeOffComboBox.getSelectedItemIndex() * 500); - vibrationPattern.add (2 * optionalParamsThreeView.vibratorMsToBeOnComboBox .getSelectedItemIndex() * 500); + vibrationPattern.add (paramControls.vibratorMsToBeOffComboBox.getSelectedItemIndex() * 500); + vibrationPattern.add (paramControls.vibratorMsToBeOnComboBox .getSelectedItemIndex() * 500); + vibrationPattern.add (2 * paramControls.vibratorMsToBeOffComboBox.getSelectedItemIndex() * 500); + vibrationPattern.add (2 * paramControls.vibratorMsToBeOnComboBox .getSelectedItemIndex() * 500); } n.vibrationPattern = vibrationPattern; - n.localOnly = optionalParamsThreeView.localOnlyButton.getToggleState(); - n.ongoing = optionalParamsThreeView.ongoingButton.getToggleState(); - n.timestampVisibility = Notification::TimestampVisibility (optionalParamsThreeView.timestampVisibilityComboBox.getSelectedItemIndex()); + n.localOnly = paramControls.localOnlyButton.getToggleState(); + n.ongoing = paramControls.ongoingButton.getToggleState(); + n.timestampVisibility = Notification::TimestampVisibility (paramControls.timestampVisibilityComboBox.getSelectedItemIndex()); - if (optionalParamsThreeView.timeoutAfterComboBox.getSelectedItemIndex() > 0) + if (paramControls.timeoutAfterComboBox.getSelectedItemIndex() > 0) { - auto index = optionalParamsThreeView.timeoutAfterComboBox.getSelectedItemIndex(); + auto index = paramControls.timeoutAfterComboBox.getSelectedItemIndex(); n.timeoutAfterMs = index * 1000 + 4000; } } void MainContentComponent::setupAccentColour() { - optionalParamsThreeView.accentColourSelector = new ColourSelector(); - optionalParamsThreeView.accentColourSelector->setName ("accent colour"); - optionalParamsThreeView.accentColourSelector->setCurrentColour (optionalParamsThreeView.accentColourButton.findColour (TextButton::buttonColourId)); - optionalParamsThreeView.accentColourSelector->setColour (ColourSelector::backgroundColourId, Colours::transparentBlack); - optionalParamsThreeView.accentColourSelector->setSize (200, 200); - optionalParamsThreeView.accentColourSelector->addComponentListener (this); - optionalParamsThreeView.accentColourSelector->addChangeListener (this); + paramControls.accentColourSelector = new ColourSelector(); + paramControls.accentColourSelector->setName ("accent colour"); + paramControls.accentColourSelector->setCurrentColour (paramControls.accentColourButton.findColour (TextButton::buttonColourId)); + paramControls.accentColourSelector->setColour (ColourSelector::backgroundColourId, Colours::transparentBlack); + paramControls.accentColourSelector->setSize (200, 200); + paramControls.accentColourSelector->addComponentListener (this); + paramControls.accentColourSelector->addChangeListener (this); - CallOutBox::launchAsynchronously (optionalParamsThreeView.accentColourSelector, optionalParamsThreeView.accentColourButton.getScreenBounds(), nullptr); + CallOutBox::launchAsynchronously (paramControls.accentColourSelector, paramControls.accentColourButton.getScreenBounds(), nullptr); } void MainContentComponent::setupLedColour() { - optionalParamsThreeView.ledColourSelector = new ColourSelector(); - optionalParamsThreeView.ledColourSelector->setName ("led colour"); - optionalParamsThreeView.ledColourSelector->setCurrentColour (optionalParamsThreeView.ledColourButton.findColour (TextButton::buttonColourId)); - optionalParamsThreeView.ledColourSelector->setColour (ColourSelector::backgroundColourId, Colours::transparentBlack); - optionalParamsThreeView.ledColourSelector->setSize (200, 200); - optionalParamsThreeView.ledColourSelector->addComponentListener (this); - optionalParamsThreeView.ledColourSelector->addChangeListener (this); + paramControls.ledColourSelector = new ColourSelector(); + paramControls.ledColourSelector->setName ("led colour"); + paramControls.ledColourSelector->setCurrentColour (paramControls.ledColourButton.findColour (TextButton::buttonColourId)); + paramControls.ledColourSelector->setColour (ColourSelector::backgroundColourId, Colours::transparentBlack); + paramControls.ledColourSelector->setSize (200, 200); + paramControls.ledColourSelector->addComponentListener (this); + paramControls.ledColourSelector->addChangeListener (this); - CallOutBox::launchAsynchronously (optionalParamsThreeView.ledColourSelector, optionalParamsThreeView.accentColourButton.getScreenBounds(), nullptr); + CallOutBox::launchAsynchronously (paramControls.ledColourSelector, paramControls.accentColourButton.getScreenBounds(), nullptr); } void MainContentComponent::changeListenerCallback (ChangeBroadcaster* source) { - if (source == optionalParamsThreeView.accentColourSelector) + if (source == paramControls.accentColourSelector) { - Colour c = optionalParamsThreeView.accentColourSelector->getCurrentColour(); - optionalParamsThreeView.accentColourButton.setColour (TextButton::buttonColourId, c); + Colour c = paramControls.accentColourSelector->getCurrentColour(); + paramControls.accentColourButton.setColour (TextButton::buttonColourId, c); } - else if (source == optionalParamsThreeView.ledColourSelector) + else if (source == paramControls.ledColourSelector) { - Colour c = optionalParamsThreeView.ledColourSelector->getCurrentColour(); - optionalParamsThreeView.ledColourButton.setColour (TextButton::buttonColourId, c); + Colour c = paramControls.ledColourSelector->getCurrentColour(); + paramControls.ledColourButton.setColour (TextButton::buttonColourId, c); } } void MainContentComponent::componentBeingDeleted (Component& component) { - if (&component == optionalParamsThreeView.accentColourSelector) - optionalParamsThreeView.accentColourSelector = nullptr; - else if (&component == optionalParamsThreeView.ledColourSelector) - optionalParamsThreeView.ledColourSelector = nullptr; + if (&component == paramControls.accentColourSelector) + paramControls.accentColourSelector = nullptr; + else if (&component == paramControls.ledColourSelector) + paramControls.ledColourSelector = nullptr; } void MainContentComponent::handleNotification (bool isLocalNotification, const PushNotifications::Notification& n) @@ -576,9 +789,15 @@ Array MainContentComponent::getAndroidChannels() return { ch1, ch2, ch3 }; } -#elif JUCE_IOS -PushNotifications::Settings MainContentComponent::getIosSettings() +#elif JUCE_IOS || JUCE_MAC +PushNotifications::Settings MainContentComponent::getNotificationSettings() { + PushNotifications::Settings settings; + settings.allowAlert = true; + settings.allowBadge = true; + settings.allowSound = true; + + #if JUCE_IOS using Action = PushNotifications::Settings::Action; using Category = PushNotifications::Settings::Category; @@ -617,11 +836,8 @@ PushNotifications::Settings MainContentComponent::getIosSettings() textCategory.actions = { textAction }; textCategory.sendDismissAction = true; - PushNotifications::Settings settings; - settings.allowAlert = true; - settings.allowBadge = true; - settings.allowSound = true; settings.categories = { okCategory, okCancelCategory, textCategory }; + #endif return settings; } diff --git a/examples/PushNotificationsDemo/Source/MainComponent.h b/examples/PushNotificationsDemo/Source/MainComponent.h index 5e819de386..2212a86e40 100644 --- a/examples/PushNotificationsDemo/Source/MainComponent.h +++ b/examples/PushNotificationsDemo/Source/MainComponent.h @@ -88,230 +88,61 @@ private: void upstreamMessageSendingError (const String& messageId, const String& error) override; static Array getAndroidChannels(); - #elif JUCE_IOS - static PushNotifications::Settings getIosSettings(); + #elif JUCE_IOS || JUCE_MAC + static PushNotifications::Settings getNotificationSettings(); #endif - struct RequiredParamsView : public Component + struct RowComponent : public Component { - RequiredParamsView() + RowComponent (Label& l, Component& c, int u = 1) + : label (l), + editor (c), + rowUnits (u) { - addAndMakeVisible (identifierLabel); - addAndMakeVisible (identifierEditor); - addAndMakeVisible (titleLabel); - addAndMakeVisible (titleEditor); - addAndMakeVisible (bodyLabel); - addAndMakeVisible (bodyEditor); - #if JUCE_IOS - addAndMakeVisible (categoryLabel); - addAndMakeVisible (categoryComboBox); - - categories.add ("okCategory"); - categories.add ("okCancelCategory"); - categories.add ("textCategory"); - - for (const auto& c : categories) - categoryComboBox.addItem (c, categoryComboBox.getNumItems() + 1); - categoryComboBox.setSelectedItemIndex (0); - - #elif JUCE_ANDROID - #if __ANDROID_API__ >= 26 - addAndMakeVisible (channelIdLabel); - addAndMakeVisible (channelIdComboBox); - - for (int i = 1; i <= 3; ++i) - channelIdComboBox.addItem (String (i), i); - channelIdComboBox.setSelectedItemIndex (0); - - #endif - addAndMakeVisible (iconLabel); - addAndMakeVisible (iconComboBox); - - for (int i = 0; i < 5; ++i) - iconComboBox.addItem ("icon" + String (i + 1), i + 1); - iconComboBox.setSelectedItemIndex (0); - #endif - - // For now, to be able to dismiss mobile keyboard. - setWantsKeyboardFocus (true); + addAndMakeVisible (label); + addAndMakeVisible (editor); } void resized() override { - const int labelColumnWidth = getWidth() / 3; - #if JUCE_ANDROID && __ANDROID_API__ >= 26 - const int rowHeight = getHeight() / 8; - #else - const int rowHeight = getHeight() / 7; - #endif - - auto layoutRow = [labelColumnWidth] (Rectangle rowBounds, Component& label, Component& editor) - { - label .setBounds (rowBounds.removeFromLeft (labelColumnWidth)); - editor.setBounds (rowBounds); - }; - auto bounds = getLocalBounds(); - - layoutRow (bounds.removeFromTop (rowHeight), identifierLabel, identifierEditor); - layoutRow (bounds.removeFromTop (rowHeight), titleLabel, titleEditor); - layoutRow (bounds.removeFromTop (4 * rowHeight), bodyLabel, bodyEditor); - #if JUCE_IOS - layoutRow (bounds.removeFromTop (rowHeight), categoryLabel, categoryComboBox); - #elif JUCE_ANDROID - #if __ANDROID_API__ >= 26 - layoutRow (bounds.removeFromTop (rowHeight), channelIdLabel, channelIdComboBox); - #endif - layoutRow (bounds.removeFromTop (rowHeight), iconLabel, iconComboBox); - #endif + label .setBounds (bounds.removeFromLeft (getWidth() / 3)); + editor.setBounds (bounds); } - Label identifierLabel { "identifierLabel", "Identifier" }; - TextEditor identifierEditor; - Label titleLabel { "titleLabel", "Title" }; - TextEditor titleEditor; - Label bodyLabel { "bodyLabel", "Body" }; - TextEditor bodyEditor; - #if JUCE_IOS - StringArray categories; - Label categoryLabel { "categoryLabel", "Category" }; - ComboBox categoryComboBox; - #elif JUCE_ANDROID - Label channelIdLabel { "channelIdLabel", "Channel ID" }; - ComboBox channelIdComboBox; - Label iconLabel { "iconLabel", "Icon" }; - ComboBox iconComboBox; - #endif + + Label& label; + Component& editor; + int rowUnits; }; - struct OptionalParamsOneView : public Component + struct ParamControls { - OptionalParamsOneView() - { - addAndMakeVisible (subtitleLabel); - addAndMakeVisible (subtitleEditor); - addAndMakeVisible (badgeNumberLabel); - addAndMakeVisible (badgeNumberComboBox); - addAndMakeVisible (soundToPlayLabel); - addAndMakeVisible (soundToPlayComboBox); - addAndMakeVisible (propertiesLabel); - addAndMakeVisible (propertiesEditor); - #if JUCE_IOS - addAndMakeVisible (fireInLabel); - addAndMakeVisible (fireInComboBox); - addAndMakeVisible (repeatLabel); - addAndMakeVisible (repeatButton); + Label identifierLabel { "identifierLabel", "Identifier" }; + TextEditor identifierEditor; + Label titleLabel { "titleLabel", "Title" }; + TextEditor titleEditor; + Label bodyLabel { "bodyLabel", "Body" }; + TextEditor bodyEditor; - fireInComboBox.addItem ("Now", 1); + Label categoryLabel { "categoryLabel", "Category" }; + ComboBox categoryComboBox; + Label channelIdLabel { "channelIdLabel", "Channel ID" }; + ComboBox channelIdComboBox; + Label iconLabel { "iconLabel", "Icon" }; + ComboBox iconComboBox; - for (int i = 1; i < 11; ++i) - fireInComboBox.addItem (String (10 * i) + "seconds", i + 1); - fireInComboBox.setSelectedItemIndex (0); - - #elif JUCE_ANDROID - addAndMakeVisible (largeIconLabel); - addAndMakeVisible (largeIconComboBox); - addAndMakeVisible (badgeIconLabel); - addAndMakeVisible (badgeIconComboBox); - addAndMakeVisible (tickerTextLabel); - addAndMakeVisible (tickerTextEditor); - addAndMakeVisible (autoCancelLabel); - addAndMakeVisible (autoCancelButton); - addAndMakeVisible (alertOnlyOnceLabel); - addAndMakeVisible (alertOnlyOnceButton); - addAndMakeVisible (actionsLabel); - addAndMakeVisible (actionsComboBox); - - largeIconComboBox.addItem ("none", 1); - - for (int i = 1; i < 5; ++i) - largeIconComboBox.addItem ("icon" + String (i), i + 1); - largeIconComboBox.setSelectedItemIndex (0); - - badgeIconComboBox.addItem ("none", 1); - badgeIconComboBox.addItem ("small", 2); - badgeIconComboBox.addItem ("large", 3); - badgeIconComboBox.setSelectedItemIndex (2); - - actionsComboBox.addItem ("none", 1); - actionsComboBox.addItem ("ok-cancel", 2); - actionsComboBox.addItem ("ok-cancel-icons", 3); - actionsComboBox.addItem ("text-input", 4); - actionsComboBox.addItem ("text-input-limited_responses", 5); - actionsComboBox.setSelectedItemIndex (0); - #endif - - for (int i = 0; i < 7; ++i) - badgeNumberComboBox.addItem (String (i), i + 1); - badgeNumberComboBox.setSelectedItemIndex (0); - - #if JUCE_IOS - String prefix = "sounds/"; - String extension = ".caf"; - #else - String prefix; - String extension; - #endif - - soundToPlayComboBox.addItem ("none", 1); - soundToPlayComboBox.addItem ("default_os_sound", 2); - soundToPlayComboBox.addItem (prefix + "demonstrative" + extension, 3); - soundToPlayComboBox.addItem (prefix + "isntit" + extension, 4); - soundToPlayComboBox.addItem (prefix + "jinglebellssms" + extension, 5); - soundToPlayComboBox.addItem (prefix + "served" + extension, 6); - soundToPlayComboBox.addItem (prefix + "solemn" + extension, 7); - soundToPlayComboBox.setSelectedItemIndex (1); - - // For now, to be able to dismiss mobile keyboard. - setWantsKeyboardFocus (true); - } - - void resized() override - { - const int labelColumnWidth = getWidth() / 3; - #if JUCE_ANDROID - const int rowHeight = getHeight() / 12; - #else - const int rowHeight = getHeight() / 8; - #endif - - auto layoutRow = [labelColumnWidth] (Rectangle rowBounds, Component& label, Component& editor) - { - label .setBounds (rowBounds.removeFromLeft (labelColumnWidth)); - editor.setBounds (rowBounds); - }; - - auto bounds = getLocalBounds(); - - layoutRow (bounds.removeFromTop (rowHeight), subtitleLabel, subtitleEditor); - layoutRow (bounds.removeFromTop (rowHeight), badgeNumberLabel, badgeNumberComboBox); - layoutRow (bounds.removeFromTop (rowHeight), soundToPlayLabel, soundToPlayComboBox); - layoutRow (bounds.removeFromTop (3 * rowHeight), propertiesLabel, propertiesEditor); - #if JUCE_IOS - layoutRow (bounds.removeFromTop (rowHeight), fireInLabel, fireInComboBox); - layoutRow (bounds.removeFromTop (rowHeight), repeatLabel, repeatButton); - #elif JUCE_ANDROID - layoutRow (bounds.removeFromTop (rowHeight), largeIconLabel, largeIconComboBox); - layoutRow (bounds.removeFromTop (rowHeight), badgeIconLabel, badgeIconComboBox); - layoutRow (bounds.removeFromTop (rowHeight), tickerTextLabel, tickerTextEditor); - layoutRow (bounds.removeFromTop (rowHeight), autoCancelLabel, autoCancelButton); - layoutRow (bounds.removeFromTop (rowHeight), alertOnlyOnceLabel, alertOnlyOnceButton); - layoutRow (bounds.removeFromTop (rowHeight), actionsLabel, actionsComboBox); - #endif - } - Label subtitleLabel { "subtitleLabel", "Subtitle" }; - TextEditor subtitleEditor; - Label badgeNumberLabel { "badgeNumberLabel", "BadgeNumber" }; - ComboBox badgeNumberComboBox; - Label soundToPlayLabel { "soundToPlayLabel", "SoundToPlay" }; - ComboBox soundToPlayComboBox; - Label propertiesLabel { "propertiesLabel", "Properties" }; - TextEditor propertiesEditor; - #if JUCE_IOS + Label subtitleLabel { "subtitleLabel", "Subtitle" }; + TextEditor subtitleEditor; + Label badgeNumberLabel { "badgeNumberLabel", "BadgeNumber" }; + ComboBox badgeNumberComboBox; + Label soundToPlayLabel { "soundToPlayLabel", "SoundToPlay" }; + ComboBox soundToPlayComboBox; + Label propertiesLabel { "propertiesLabel", "Properties" }; + TextEditor propertiesEditor; Label fireInLabel { "fireInLabel", "Fire in" }; ComboBox fireInComboBox; Label repeatLabel { "repeatLabel", "Repeat" }; ToggleButton repeatButton; - #elif JUCE_ANDROID Label largeIconLabel { "largeIconLabel", "Large Icon" }; ComboBox largeIconComboBox; Label badgeIconLabel { "badgeIconLabel", "Badge Icon" }; @@ -324,106 +155,6 @@ private: ToggleButton alertOnlyOnceButton; Label actionsLabel { "actionsLabel", "Actions" }; ComboBox actionsComboBox; - #endif - }; - - struct OptionalParamsTwoView : public Component - { - OptionalParamsTwoView() - { - addAndMakeVisible (progressMaxLabel); - addAndMakeVisible (progressMaxComboBox); - addAndMakeVisible (progressCurrentLabel); - addAndMakeVisible (progressCurrentComboBox); - addAndMakeVisible (progressIndeterminateLabel); - addAndMakeVisible (progressIndeterminateButton); - addAndMakeVisible (categoryLabel); - addAndMakeVisible (categoryComboBox); - addAndMakeVisible (priorityLabel); - addAndMakeVisible (priorityComboBox); - addAndMakeVisible (personLabel); - addAndMakeVisible (personEditor); - addAndMakeVisible (lockScreenVisibilityLabel); - addAndMakeVisible (lockScreenVisibilityComboBox); - addAndMakeVisible (groupIdLabel); - addAndMakeVisible (groupIdEditor); - addAndMakeVisible (sortKeyLabel); - addAndMakeVisible (sortKeyEditor); - addAndMakeVisible (groupSummaryLabel); - addAndMakeVisible (groupSummaryButton); - addAndMakeVisible (groupAlertBehaviourLabel); - addAndMakeVisible (groupAlertBehaviourComboBox); - - for (int i = 0; i < 11; ++i) - { - progressMaxComboBox .addItem (String (i * 10) + "%", i + 1); - progressCurrentComboBox.addItem (String (i * 10) + "%", i + 1); - } - - progressMaxComboBox .setSelectedItemIndex (0); - progressCurrentComboBox.setSelectedItemIndex (0); - - categoryComboBox.addItem ("unspecified", 1); - categoryComboBox.addItem ("alarm", 2); - categoryComboBox.addItem ("call", 3); - categoryComboBox.addItem ("email", 4); - categoryComboBox.addItem ("error", 5); - categoryComboBox.addItem ("event", 6); - categoryComboBox.addItem ("message", 7); - categoryComboBox.addItem ("progress", 8); - categoryComboBox.addItem ("promo", 9); - categoryComboBox.addItem ("recommendation", 10); - categoryComboBox.addItem ("reminder", 11); - categoryComboBox.addItem ("service", 12); - categoryComboBox.addItem ("social", 13); - categoryComboBox.addItem ("status", 14); - categoryComboBox.addItem ("system", 15); - categoryComboBox.addItem ("transport", 16); - categoryComboBox.setSelectedItemIndex (0); - - for (int i = -2; i < 3; ++i) - priorityComboBox.addItem (String (i), i + 3); - priorityComboBox.setSelectedItemIndex (2); - - lockScreenVisibilityComboBox.addItem ("don't show", 1); - lockScreenVisibilityComboBox.addItem ("show partially", 2); - lockScreenVisibilityComboBox.addItem ("show completely", 3); - lockScreenVisibilityComboBox.setSelectedItemIndex (1); - - groupAlertBehaviourComboBox.addItem ("alert all", 1); - groupAlertBehaviourComboBox.addItem ("alert summary", 2); - groupAlertBehaviourComboBox.addItem ("alert children", 3); - groupAlertBehaviourComboBox.setSelectedItemIndex (0); - - // For now, to be able to dismiss mobile keyboard. - setWantsKeyboardFocus (true); - } - - void resized() override - { - const int labelColumnWidth = getWidth() / 3; - const int rowHeight = getHeight() / 11; - - auto layoutRow = [labelColumnWidth] (Rectangle rowBounds, Component& label, Component& editor) - { - label .setBounds (rowBounds.removeFromLeft (labelColumnWidth)); - editor.setBounds (rowBounds); - }; - - auto bounds = getLocalBounds(); - - layoutRow (bounds.removeFromTop (rowHeight), progressMaxLabel, progressMaxComboBox); - layoutRow (bounds.removeFromTop (rowHeight), progressCurrentLabel, progressCurrentComboBox); - layoutRow (bounds.removeFromTop (rowHeight), progressIndeterminateLabel, progressIndeterminateButton); - layoutRow (bounds.removeFromTop (rowHeight), categoryLabel, categoryComboBox); - layoutRow (bounds.removeFromTop (rowHeight), priorityLabel, priorityComboBox); - layoutRow (bounds.removeFromTop (rowHeight), personLabel, personEditor); - layoutRow (bounds.removeFromTop (rowHeight), lockScreenVisibilityLabel, lockScreenVisibilityComboBox); - layoutRow (bounds.removeFromTop (rowHeight), groupIdLabel, groupIdEditor); - layoutRow (bounds.removeFromTop (rowHeight), sortKeyLabel, sortKeyEditor); - layoutRow (bounds.removeFromTop (rowHeight), groupSummaryLabel, groupSummaryButton); - layoutRow (bounds.removeFromTop (rowHeight), groupAlertBehaviourLabel, groupAlertBehaviourComboBox); - } Label progressMaxLabel { "progressMaxLabel", "ProgressMax" }; ComboBox progressMaxComboBox; @@ -431,8 +162,8 @@ private: ComboBox progressCurrentComboBox; Label progressIndeterminateLabel { "progressIndeterminateLabel", "ProgressIndeterminate" }; ToggleButton progressIndeterminateButton; - Label categoryLabel { "categoryLabel", "Category" }; - ComboBox categoryComboBox; + Label notifCategoryLabel { "notifCategoryLabel", "Category" }; + ComboBox notifCategoryComboBox; Label priorityLabel { "priorityLabel", "Priority" }; ComboBox priorityComboBox; Label personLabel { "personLabel", "Person" }; @@ -447,84 +178,6 @@ private: ToggleButton groupSummaryButton; Label groupAlertBehaviourLabel { "groupAlertBehaviourLabel", "GroupAlertBehaviour" }; ComboBox groupAlertBehaviourComboBox; - }; - - struct OptionalParamsThreeView : public Component - { - OptionalParamsThreeView() - { - addAndMakeVisible (accentColourLabel); - addAndMakeVisible (accentColourButton); - addAndMakeVisible (ledColourLabel); - addAndMakeVisible (ledColourButton); - addAndMakeVisible (ledMsToBeOnLabel); - addAndMakeVisible (ledMsToBeOnComboBox); - addAndMakeVisible (ledMsToBeOffLabel); - addAndMakeVisible (ledMsToBeOffComboBox); - addAndMakeVisible (vibratorMsToBeOnLabel); - addAndMakeVisible (vibratorMsToBeOnComboBox); - addAndMakeVisible (vibratorMsToBeOffLabel); - addAndMakeVisible (vibratorMsToBeOffComboBox); - addAndMakeVisible (localOnlyLabel); - addAndMakeVisible (localOnlyButton); - addAndMakeVisible (ongoingLabel); - addAndMakeVisible (ongoingButton); - addAndMakeVisible (timestampVisibilityLabel); - addAndMakeVisible (timestampVisibilityComboBox); - addAndMakeVisible (timeoutAfterLabel); - addAndMakeVisible (timeoutAfterComboBox); - - timeoutAfterComboBox.addItem ("No timeout", 1); - - for (int i = 0; i < 10; ++i) - { - ledMsToBeOnComboBox .addItem (String (i * 200) + "ms", i + 1); - ledMsToBeOffComboBox .addItem (String (i * 200) + "ms", i + 1); - vibratorMsToBeOnComboBox .addItem (String (i * 500) + "ms", i + 1); - vibratorMsToBeOffComboBox.addItem (String (i * 500) + "ms", i + 1); - timeoutAfterComboBox.addItem (String (5000 + 1000 * i) + "ms", i + 2); - } - - ledMsToBeOnComboBox .setSelectedItemIndex (5); - ledMsToBeOffComboBox .setSelectedItemIndex (5); - vibratorMsToBeOnComboBox .setSelectedItemIndex (0); - vibratorMsToBeOffComboBox.setSelectedItemIndex (0); - timeoutAfterComboBox.setSelectedItemIndex (0); - - timestampVisibilityComboBox.addItem ("off", 1); - timestampVisibilityComboBox.addItem ("on", 2); - timestampVisibilityComboBox.addItem ("chronometer", 3); - timestampVisibilityComboBox.addItem ("count down", 4); - timestampVisibilityComboBox.setSelectedItemIndex (1); - - // For now, to be able to dismiss mobile keyboard. - setWantsKeyboardFocus (true); - } - - void resized() override - { - const int labelColumnWidth = getWidth() / 3; - const int rowHeight = getHeight() / 10; - - auto layoutRow = [labelColumnWidth] (Rectangle rowBounds, Component& label, Component& editor) - { - label .setBounds (rowBounds.removeFromLeft (labelColumnWidth)); - editor.setBounds (rowBounds); - }; - - auto bounds = getLocalBounds(); - - layoutRow (bounds.removeFromTop (rowHeight), accentColourLabel, accentColourButton); - layoutRow (bounds.removeFromTop (rowHeight), ledColourLabel, ledColourButton); - layoutRow (bounds.removeFromTop (rowHeight), ledMsToBeOnLabel, ledMsToBeOnComboBox); - layoutRow (bounds.removeFromTop (rowHeight), ledMsToBeOffLabel, ledMsToBeOffComboBox); - layoutRow (bounds.removeFromTop (rowHeight), vibratorMsToBeOnLabel, vibratorMsToBeOnComboBox); - layoutRow (bounds.removeFromTop (rowHeight), vibratorMsToBeOffLabel, vibratorMsToBeOffComboBox); - layoutRow (bounds.removeFromTop (rowHeight), localOnlyLabel, localOnlyButton); - layoutRow (bounds.removeFromTop (rowHeight), ongoingLabel, ongoingButton); - layoutRow (bounds.removeFromTop (rowHeight), timestampVisibilityLabel, timestampVisibilityComboBox); - layoutRow (bounds.removeFromTop (rowHeight), timeoutAfterLabel, timeoutAfterComboBox); - } Label accentColourLabel { "accentColourLabel", "AccentColour" }; TextButton accentColourButton; @@ -551,6 +204,45 @@ private: ColourSelector* ledColourSelector = nullptr; }; + void setupControls(); + void distributeControls(); + + struct ParamsView : public Component + { + ParamsView() + { + // For now, to be able to dismiss mobile keyboard. + setWantsKeyboardFocus (true); + } + + void addRowComponent (RowComponent *rc) + { + rowComponents.add (rc); + addAndMakeVisible (rc); + } + + void resized() override + { + int totalRowUnits = 0; + + for (const auto &rc : rowComponents) + totalRowUnits += rc->rowUnits; + + const int rowHeight = getHeight() / totalRowUnits; + + auto bounds = getLocalBounds(); + + for (auto &rc : rowComponents) + rc->setBounds (bounds.removeFromTop (rc->rowUnits * rowHeight)); + + auto* last = rowComponents[rowComponents.size() - 1]; + last->setBounds (last->getBounds().withHeight (getHeight() - last->getY())); + } + + private: + OwnedArray rowComponents; + }; + struct AuxActionsView : public Component { AuxActionsView() @@ -559,7 +251,7 @@ private: addAndMakeVisible (removeDeliveredNotifWithIdButton); addAndMakeVisible (deliveredNotifIdentifier); addAndMakeVisible (removeAllDeliveredNotifsButton); - #if JUCE_IOS + #if JUCE_IOS || JUCE_MAC addAndMakeVisible (getPendingNotificationsButton); addAndMakeVisible (removePendingNotifWithIdButton); addAndMakeVisible (pendingNotifIdentifier); @@ -586,7 +278,7 @@ private: removeAllDeliveredNotifsButton .setBounds (bounds.removeFromTop (rowHeight)); - #if JUCE_IOS + #if JUCE_IOS || JUCE_MAC getPendingNotificationsButton .setBounds (bounds.removeFromTop (rowHeight)); rowBounds = bounds.removeFromTop (rowHeight); @@ -601,12 +293,10 @@ private: TextButton removeDeliveredNotifWithIdButton { "Remove Delivered Notif With ID:" }; TextEditor deliveredNotifIdentifier; TextButton removeAllDeliveredNotifsButton { "Remove All Delivered Notifs" }; - #if JUCE_IOS TextButton getPendingNotificationsButton { "Get Pending Notifications" }; TextButton removePendingNotifWithIdButton { "Remove Pending Notif With ID:" }; TextEditor pendingNotifIdentifier; TextButton removeAllPendingNotifsButton { "Remove All Pending Notifs" }; - #endif }; struct RemoteView : public Component @@ -641,17 +331,52 @@ private: TextButton unsubscribeFromSportsButton { "UnsubscribeFromSports" }; }; - Label headerLabel { "headerLabel", "Push Notifications Demo" }; - RequiredParamsView requiredParamsView; - OptionalParamsOneView optionalParamsOneView; - OptionalParamsTwoView optionalParamsTwoView; - OptionalParamsThreeView optionalParamsThreeView; - AuxActionsView auxActionsView; - TabbedComponent localNotificationsTabs { TabbedButtonBar::TabsAtTop }; - RemoteView remoteView; - TabbedComponent mainTabs { TabbedButtonBar::TabsAtTop }; - TextButton sendButton { "Send!" }; - Label notAvailableYetLabel { "notAvailableYetLabel", "Push Notifications feature is not available on this platform yet!" }; + struct DemoTabbedComponent : public TabbedComponent + { + explicit DemoTabbedComponent (TabbedButtonBar::Orientation orientation) + : TabbedComponent (orientation) + { + } + + void currentTabChanged (int, const String& newCurrentTabName) override + { + if (! showedRemoteInstructions && newCurrentTabName == "Remote") + { + MainContentComponent::showRemoteInstructions(); + + showedRemoteInstructions = true; + } + + } + + private: + bool showedRemoteInstructions = false; + }; + + static void showRemoteInstructions() + { + #if JUCE_IOS || JUCE_MAC + NativeMessageBox::showMessageBoxAsync (AlertWindow::InfoIcon, + "Remote Notifications instructions", + "In order to be able to test remote notifications " + "ensure that the app is signed and that you register " + "the bundle ID for remote notifications in " + "Apple Developer Center."); + #endif + } + + Label headerLabel { "headerLabel", "Push Notifications Demo" }; + ParamControls paramControls; + ParamsView paramsOneView; + ParamsView paramsTwoView; + ParamsView paramsThreeView; + ParamsView paramsFourView; + AuxActionsView auxActionsView; + TabbedComponent localNotificationsTabs { TabbedButtonBar::TabsAtTop }; + RemoteView remoteView; + DemoTabbedComponent mainTabs { TabbedButtonBar::TabsAtTop }; + TextButton sendButton { "Send!" }; + Label notAvailableYetLabel { "notAvailableYetLabel", "Push Notifications feature is not available on this platform yet!" }; //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainContentComponent) diff --git a/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Xcode.h b/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Xcode.h index f55b51c2be..9a6bdbe2b6 100644 --- a/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Xcode.h +++ b/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Xcode.h @@ -249,9 +249,6 @@ public: props.add (new BooleanPropertyComponent (getBackgroundBleValue(), "Bluetooth MIDI background capability", "Enabled"), "Enable this to grant your app the capability to connect to Bluetooth LE devices when in background mode."); - props.add (new BooleanPropertyComponent (getPushNotificationsValue(), "Push Notifications capability", "Enabled"), - "Enable this to grant your app the capability to receive push notifications."); - props.add (new BooleanPropertyComponent (getAppGroupsEnabledValue(), "App groups capability", "Enabled"), "Enable this to grant your app the capability to share resources between apps using the same app group ID."); @@ -259,6 +256,9 @@ public: "Enable this to grant your app the capability to use native file load/save browser windows on iOS."); } + props.add (new BooleanPropertyComponent (getPushNotificationsValue(), "Push Notifications capability", "Enabled"), + "Enable this to grant your app the capability to receive push notifications."); + props.add (new TextPropertyComponent (getPListToMergeValue(), "Custom PList", 8192, true), "You can paste the contents of an XML PList file in here, and the settings that it contains will override any " "settings that the Projucer creates. BEWARE! When doing this, be careful to remove from the XML any " @@ -860,7 +860,7 @@ public: && type == Target::StandalonePlugIn && owner.getProject().shouldEnableIAA()) ? 1 : 0; - auto pushNotificationsEnabled = (owner.iOS && owner.isPushNotificationsEnabled()) ? 1 : 0; + auto pushNotificationsEnabled = owner.isPushNotificationsEnabled() ? 1 : 0; auto sandboxEnabled = (type == Target::AudioUnitv3PlugIn ? 1 : 0); attributes << "SystemCapabilities = {"; @@ -1146,7 +1146,7 @@ public: if (owner.isInAppPurchasesEnabled()) defines.set ("JUCE_IN_APP_PURCHASES", "1"); - if (owner.iOS && owner.isPushNotificationsEnabled()) + if (owner.isPushNotificationsEnabled()) defines.set ("JUCE_PUSH_NOTIFICATIONS", "1"); defines = mergePreprocessorDefs (defines, owner.getAllPreprocessorDefs (config, type)); @@ -2552,8 +2552,10 @@ private: } else { - if (isiOS() && isPushNotificationsEnabled()) - entitlements.set ("aps-environment", "development"); + if (isPushNotificationsEnabled()) + entitlements.set (isiOS() ? "aps-environment" + : "com.apple.developer.aps-environment", + "development"); } if (isAppGroupsEnabled()) diff --git a/modules/juce_core/native/juce_osx_ObjCHelpers.h b/modules/juce_core/native/juce_osx_ObjCHelpers.h index 0702946c29..483ceddb91 100644 --- a/modules/juce_core/native/juce_osx_ObjCHelpers.h +++ b/modules/juce_core/native/juce_osx_ObjCHelpers.h @@ -67,6 +67,132 @@ static inline NSArray* createNSArrayFromStringArray (const StringArray& strings) return [array autorelease]; } +static NSArray* varArrayToNSArray (const var& varToParse); + +static NSDictionary* varObjectToNSDictionary (const var& varToParse) +{ + auto* dictionary = [NSMutableDictionary dictionary]; + + if (varToParse.isObject()) + { + auto* dynamicObject = varToParse.getDynamicObject(); + + auto& properties = dynamicObject->getProperties(); + + for (int i = 0; i < properties.size(); ++i) + { + auto* keyString = juceStringToNS (properties.getName (i).toString()); + + const var& valueVar = properties.getValueAt (i); + + if (valueVar.isObject()) + { + auto* valueDictionary = varObjectToNSDictionary (valueVar); + + [dictionary setObject: valueDictionary forKey: keyString]; + } + else if (valueVar.isArray()) + { + auto* valueArray = varArrayToNSArray (valueVar); + + [dictionary setObject: valueArray forKey: keyString]; + } + else + { + auto* valueString = juceStringToNS (valueVar.toString()); + + [dictionary setObject: valueString forKey: keyString]; + } + } + } + + return dictionary; +} + +static NSArray* varArrayToNSArray (const var& varToParse) +{ + jassert (varToParse.isArray()); + + if (! varToParse.isArray()) + return nil; + + const auto* varArray = varToParse.getArray(); + + auto* array = [NSMutableArray arrayWithCapacity: (NSUInteger) varArray->size()]; + + for (const auto& aVar : *varArray) + { + if (aVar.isObject()) + { + auto* valueDictionary = varObjectToNSDictionary (aVar); + + [array addObject: valueDictionary]; + } + else if (aVar.isArray()) + { + auto* valueArray = varArrayToNSArray (aVar); + + [array addObject: valueArray]; + } + else + { + auto* valueString = juceStringToNS (aVar.toString()); + + [array addObject: valueString]; + } + } + + return array; +} + +static var nsArrayToVar (NSArray* array); + +static var nsDictionaryToVar (NSDictionary* dictionary) +{ + DynamicObject::Ptr dynamicObject = new DynamicObject(); + + for (NSString* key in dictionary) + { + const auto keyString = nsStringToJuce (key); + + id value = dictionary[key]; + + if ([value isKindOfClass: [NSString class]]) + dynamicObject->setProperty (keyString, nsStringToJuce ((NSString*) value)); + else if ([value isKindOfClass: [NSNumber class]]) + dynamicObject->setProperty (keyString, nsStringToJuce ([(NSNumber*) value stringValue])); + else if ([value isKindOfClass: [NSDictionary class]]) + dynamicObject->setProperty (keyString, nsDictionaryToVar ((NSDictionary*) value)); + else if ([value isKindOfClass: [NSArray class]]) + dynamicObject->setProperty (keyString, nsArrayToVar ((NSArray*) value)); + else + jassertfalse; // Unsupported yet, add here! + } + + return var (dynamicObject.get()); +} + +static var nsArrayToVar (NSArray* array) +{ + Array resultArray; + + for (id value in array) + { + if ([value isKindOfClass: [NSString class]]) + resultArray.add (var (nsStringToJuce ((NSString*) value))); + else if ([value isKindOfClass: [NSNumber class]]) + resultArray.add (var (nsStringToJuce ([(NSNumber*) value stringValue]))); + else if ([value isKindOfClass: [NSDictionary class]]) + resultArray.add (nsDictionaryToVar ((NSDictionary*) value)); + else if ([value isKindOfClass: [NSArray class]]) + resultArray.add (nsArrayToVar ((NSArray*) value)); + else + jassertfalse; // Unsupported yet, add here! + } + + return var (resultArray); +} + #if JUCE_MAC template static NSRect makeNSRect (const RectangleType& r) noexcept diff --git a/modules/juce_events/native/juce_mac_MessageManager.mm b/modules/juce_events/native/juce_mac_MessageManager.mm index 27456e63a9..d6f1eae7dd 100644 --- a/modules/juce_events/native/juce_mac_MessageManager.mm +++ b/modules/juce_events/native/juce_mac_MessageManager.mm @@ -102,7 +102,7 @@ private: { AppDelegateClass() : ObjCClass ("JUCEAppDelegate_") { - addMethod (@selector (applicationWillFinishLaunching:), applicationWillFinishLaunching, "v@:@@"); + addMethod (@selector (applicationWillFinishLaunching:), applicationWillFinishLaunching, "v@:@"); addMethod (@selector (getUrl:withReplyEvent:), getUrl_withReplyEvent, "v@:@@"); addMethod (@selector (applicationShouldTerminate:), applicationShouldTerminate, "I@:@"); addMethod (@selector (applicationWillTerminate:), applicationWillTerminate, "v@:@"); @@ -116,11 +116,22 @@ private: addMethod (@selector (mainMenuTrackingEnded:), mainMenuTrackingEnded, "v@:@"); addMethod (@selector (dummyMethod), dummyMethod, "v@:"); + #if JUCE_PUSH_NOTIFICATIONS + //============================================================================== + addIvar*> ("pushNotificationsDelegate"); + + addMethod (@selector (applicationDidFinishLaunching:), applicationDidFinishLaunching, "v@:@"); + addMethod (@selector (setPushNotificationsDelegate:), setPushNotificationsDelegate, "v@:@"); + addMethod (@selector (application:didRegisterForRemoteNotificationsWithDeviceToken:), registeredForRemoteNotifications, "v@:@@"); + addMethod (@selector (application:didFailToRegisterForRemoteNotificationsWithError:), failedToRegisterForRemoteNotifications, "v@:@@"); + addMethod (@selector (application:didReceiveRemoteNotification:), didReceiveRemoteNotification, "v@:@@"); + #endif + registerClass(); } private: - static void applicationWillFinishLaunching (id self, SEL, NSApplication*, NSNotification*) + static void applicationWillFinishLaunching (id self, SEL, NSNotification*) { [[NSAppleEventManager sharedAppleEventManager] setEventHandler: self andSelector: @selector (getUrl:withReplyEvent:) @@ -128,6 +139,19 @@ private: andEventID: kAEGetURL]; } + #if JUCE_PUSH_NOTIFICATIONS + static void applicationDidFinishLaunching (id self, SEL, NSNotification* notification) + { + if (notification.userInfo != nil) + { + NSUserNotification* userNotification = [notification.userInfo objectForKey: nsStringLiteral ("NSApplicationLaunchUserNotificationKey")]; + + if (userNotification != nil && userNotification.userInfo != nil) + didReceiveRemoteNotification (self, nil, [NSApplication sharedApplication], userNotification.userInfo); + } + } + #endif + static NSApplicationTerminateReply applicationShouldTerminate (id /*self*/, SEL, NSApplication*) { if (auto* app = JUCEApplicationBase::getInstance()) @@ -216,6 +240,73 @@ private: return s; } + + #if JUCE_PUSH_NOTIFICATIONS + //============================================================================== + static void setPushNotificationsDelegate (id self, SEL, NSObject* delegate) + { + object_setInstanceVariable (self, "pushNotificationsDelegate", delegate); + } + + static NSObject* getPushNotificationsDelegate (id self) + { + return getIvar*> (self, "pushNotificationsDelegate"); + } + + static void registeredForRemoteNotifications (id self, SEL, NSApplication* application, NSData* deviceToken) + { + auto* delegate = getPushNotificationsDelegate (self); + + SEL selector = NSSelectorFromString (@"application:didRegisterForRemoteNotificationsWithDeviceToken:"); + + if (delegate != nil && [delegate respondsToSelector: selector]) + { + NSInvocation* invocation = [NSInvocation invocationWithMethodSignature: [delegate methodSignatureForSelector: selector]]; + [invocation setSelector: selector]; + [invocation setTarget: delegate]; + [invocation setArgument: &application atIndex:2]; + [invocation setArgument: &deviceToken atIndex:3]; + + [invocation invoke]; + } + } + + static void failedToRegisterForRemoteNotifications (id self, SEL, NSApplication* application, NSError* error) + { + auto* delegate = getPushNotificationsDelegate (self); + + SEL selector = NSSelectorFromString (@"application:didFailToRegisterForRemoteNotificationsWithError:"); + + if (delegate != nil && [delegate respondsToSelector: selector]) + { + NSInvocation* invocation = [NSInvocation invocationWithMethodSignature: [delegate methodSignatureForSelector: selector]]; + [invocation setSelector: selector]; + [invocation setTarget: delegate]; + [invocation setArgument: &application atIndex:2]; + [invocation setArgument: &error atIndex:3]; + + [invocation invoke]; + } + } + + static void didReceiveRemoteNotification (id self, SEL, NSApplication* application, NSDictionary* userInfo) + { + auto* delegate = getPushNotificationsDelegate (self); + + SEL selector = NSSelectorFromString (@"application:didReceiveRemoteNotification:"); + + if (delegate != nil && [delegate respondsToSelector: selector]) + { + NSInvocation* invocation = [NSInvocation invocationWithMethodSignature: [delegate methodSignatureForSelector: selector]]; + [invocation setSelector: selector]; + [invocation setTarget: delegate]; + [invocation setArgument: &application atIndex:2]; + [invocation setArgument: &userInfo atIndex:3]; + + [invocation invoke]; + } + } + #endif }; }; diff --git a/modules/juce_gui_extra/juce_gui_extra.cpp b/modules/juce_gui_extra/juce_gui_extra.cpp index f8693b5634..49b2d6b422 100644 --- a/modules/juce_gui_extra/juce_gui_extra.cpp +++ b/modules/juce_gui_extra/juce_gui_extra.cpp @@ -55,6 +55,12 @@ #import #import + #if JUCE_PUSH_NOTIFICATIONS + #import + + #include "native/juce_mac_PushNotifications.cpp" + #endif + //============================================================================== #elif JUCE_IOS #if JUCE_PUSH_NOTIFICATIONS diff --git a/modules/juce_gui_extra/misc/juce_PushNotifications.cpp b/modules/juce_gui_extra/misc/juce_PushNotifications.cpp index e0c36e2921..62c4de567c 100644 --- a/modules/juce_gui_extra/misc/juce_PushNotifications.cpp +++ b/modules/juce_gui_extra/misc/juce_PushNotifications.cpp @@ -28,7 +28,7 @@ namespace juce { //============================================================================== -#if ! JUCE_ANDROID && ! JUCE_IOS +#if ! JUCE_ANDROID && ! JUCE_IOS && ! JUCE_MAC bool PushNotifications::Notification::isValid() const noexcept { return true; } #endif @@ -49,7 +49,7 @@ void PushNotifications::removeListener (Listener* l) { listeners.remove (l); } void PushNotifications::requestPermissionsWithSettings (const PushNotifications::Settings& settings) { - #if JUCE_PUSH_NOTIFICATIONS && JUCE_IOS + #if JUCE_PUSH_NOTIFICATIONS && (JUCE_IOS || JUCE_MAC) pimpl->requestPermissionsWithSettings (settings); #else ignoreUnused (settings); @@ -59,7 +59,7 @@ void PushNotifications::requestPermissionsWithSettings (const PushNotifications: void PushNotifications::requestSettingsUsed() { - #if JUCE_PUSH_NOTIFICATIONS && JUCE_IOS + #if JUCE_PUSH_NOTIFICATIONS && (JUCE_IOS || JUCE_MAC) pimpl->requestSettingsUsed(); #else listeners.call (&PushNotifications::Listener::notificationSettingsReceived, {}); diff --git a/modules/juce_gui_extra/misc/juce_PushNotifications.h b/modules/juce_gui_extra/misc/juce_PushNotifications.h index 94ee5ccffc..617a54d967 100644 --- a/modules/juce_gui_extra/misc/juce_PushNotifications.h +++ b/modules/juce_gui_extra/misc/juce_PushNotifications.h @@ -124,6 +124,14 @@ public: URL soundToPlay; /**< Optional: empty when the notification should be silent. When the name is set to "default_os_sound", then a default sound will be used. + For a custom sound on OSX, set the URL to the name of a sound file (preferably without + an extension) and place the sound file directly in bundle's "Resources" directory (you + can use "Xcode Resource" tickbox in Projucer to achieve that), i.e. it cannot be in a + subdirectory of "Resources" like "Resources/sound". Alternatively, if a sound file + cannot be found in bundle's "Resources" directory, the OS may look for the sound in the + following paths: "~/Library/Sounds", "/Library/Sounds", "/Network/Library/Sounds", + "/System/Library/Sounds". + For a custom sound on iOS, set the URL to a relative path within your bundle, including file extension. For instance, if your bundle contains "sounds" folder with "my_sound.caf" file, then the URL should be "sounds/my_sound.caf". @@ -312,7 +320,11 @@ public: //========================================================================== /** Describes settings we want to use for current device. Note that at the - moment this is only used on iOS. + moment this is only used on iOS and partially on OSX. + + On OSX only allow* flags are used and they control remote notifications only. + To control sound, alert and badge settings for local notifications on OSX, + use Notifications settings in System Preferences. To setup push notifications for current device, provide permissions required, as well as register categories of notifications you want to support. Each @@ -411,9 +423,13 @@ public: on user's subsequent changes in OS settings, the actual current settings may be different (e.g. user might have later decided to disable sounds). - Note that settings are currently only used on iOS. When calling on other platforms, Settings - with no categories and all allow* flags set to true will be received in - Listener::notificationSettingsReceived(). + Note that settings are currently only used on iOS and partially on OSX. + + On OSX, only allow* flags are used and they refer to remote notifications only. For + local notifications, refer to System Preferences. + + When calling this function on other platforms, Settings with no categories and all allow* + flags set to true will be received in Listener::notificationSettingsReceived(). */ void requestSettingsUsed(); @@ -486,7 +502,7 @@ public: //========================================================================== /** Checks whether notifications are enabled for given application. - On iOS this will always return true, use requestSettingsUsed() instead. + On iOS and OSX this will always return true, use requestSettingsUsed() instead. */ bool areNotificationsEnabled() const; diff --git a/modules/juce_gui_extra/native/juce_ios_PushNotifications.cpp b/modules/juce_gui_extra/native/juce_ios_PushNotifications.cpp index 59612650f6..74fdd65ff3 100644 --- a/modules/juce_gui_extra/native/juce_ios_PushNotifications.cpp +++ b/modules/juce_gui_extra/native/juce_ios_PushNotifications.cpp @@ -34,84 +34,6 @@ template <> struct ContainerDeletePolicygetProperties(); - - for (int i = 0; i < properties.size(); ++i) - { - NSString* keyString = juceStringToNS (properties.getName (i).toString()); - - const var& valueVar = properties.getValueAt (i); - - if (valueVar.isObject()) - { - NSDictionary* valueDictionary = varObjectToNSDictionary (valueVar); - - [dictionary setObject: valueDictionary forKey: keyString]; - } - else if (valueVar.isArray()) - { - NSArray* valueArray = varArrayToNSArray (valueVar); - - [dictionary setObject: valueArray forKey: keyString]; - } - else - { - NSString* valueString = juceStringToNS (valueVar.toString()); - - [dictionary setObject: valueString forKey: keyString]; - } - } - } - - return dictionary; - } - - NSArray* varArrayToNSArray (const var& varToParse) - { - jassert (varToParse.isArray()); - - if (! varToParse.isArray()) - return nil; - - const auto* varArray = varToParse.getArray(); - - NSMutableArray* array = [NSMutableArray arrayWithCapacity: (NSUInteger) varArray->size()]; - - for (const auto& aVar : *varArray) - { - if (aVar.isObject()) - { - NSDictionary* valueDictionary = varObjectToNSDictionary (aVar); - - [array addObject: valueDictionary]; - } - else if (aVar.isArray()) - { - NSArray* valueArray = varArrayToNSArray (aVar); - - [array addObject: valueArray]; - } - else - { - NSString* valueString = juceStringToNS (aVar.toString()); - - [array addObject: valueString]; - } - } - - return array; - } - using Action = PushNotifications::Settings::Action; using Category = PushNotifications::Settings::Category; @@ -210,7 +132,7 @@ namespace PushNotificationsDelegateDetails auto triggerTime = Time::getCurrentTime() + RelativeTime (n.triggerIntervalSec); notification.fireDate = [NSDate dateWithTimeIntervalSince1970: triggerTime.toMilliseconds() / 1000.]; - notification.userInfo = PushNotificationsDelegateDetails::varObjectToNSDictionary (n.properties); + notification.userInfo = varObjectToNSDictionary (n.properties); auto soundToPlayString = n.soundToPlay.toString (true); @@ -242,7 +164,7 @@ namespace PushNotificationsDelegateDetails else if (soundToPlayString.isNotEmpty()) content.sound = [UNNotificationSound soundNamed: juceStringToNS (soundToPlayString)]; - NSMutableDictionary* propsDict = (NSMutableDictionary*) PushNotificationsDelegateDetails::varObjectToNSDictionary (n.properties); + NSMutableDictionary* propsDict = (NSMutableDictionary*) varObjectToNSDictionary (n.properties); [propsDict setObject: juceStringToNS (soundToPlayString) forKey: nsStringLiteral ("com.juce.soundName")]; content.userInfo = propsDict; @@ -268,54 +190,6 @@ namespace PushNotificationsDelegateDetails } #endif - var nsArrayToVar (NSArray* array); - - var nsDictionaryToVar (NSDictionary* dictionary) - { - DynamicObject::Ptr dynamicObject = new DynamicObject(); - - for (NSString* key in dictionary) - { - const auto keyString = nsStringToJuce (key); - - id value = dictionary[key]; - - if ([value isKindOfClass: [NSString class]]) - dynamicObject->setProperty (keyString, nsStringToJuce ((NSString*) value)); - else if ([value isKindOfClass: [NSNumber class]]) - dynamicObject->setProperty (keyString, nsStringToJuce ([(NSNumber*) value stringValue])); - else if ([value isKindOfClass: [NSDictionary class]]) - dynamicObject->setProperty (keyString, nsDictionaryToVar ((NSDictionary*) value)); - else if ([value isKindOfClass: [NSArray class]]) - dynamicObject->setProperty (keyString, nsArrayToVar ((NSArray*) value)); - else - jassertfalse; // Unsupported yet, add here! - } - - return var (dynamicObject); - } - - var nsArrayToVar (NSArray* array) - { - Array resultArray; - - for (id value in array) - { - if ([value isKindOfClass: [NSString class]]) - resultArray.add (var (nsStringToJuce ((NSString*) value))); - else if ([value isKindOfClass: [NSNumber class]]) - resultArray.add (var (nsStringToJuce ([(NSNumber*) value stringValue]))); - else if ([value isKindOfClass: [NSDictionary class]]) - resultArray.add (nsDictionaryToVar ((NSDictionary*) value)); - else if ([value isKindOfClass: [NSArray class]]) - resultArray.add (nsArrayToVar ((NSArray*) value)); - else - jassertfalse; // Unsupported yet, add here! - } - - return var (resultArray); - } - String getUserResponseFromNSDictionary (NSDictionary* dictionary) { if (dictionary == nil || dictionary.count == 0) @@ -397,7 +271,7 @@ namespace PushNotificationsDelegateDetails n.category = nsStringToJuce (r.content.categoryIdentifier); n.badgeNumber = r.content.badge.intValue; - auto userInfoVar = PushNotificationsDelegateDetails::nsDictionaryToVar (r.content.userInfo); + auto userInfoVar = nsDictionaryToVar (r.content.userInfo); if (auto* object = userInfoVar.getDynamicObject()) { @@ -755,8 +629,6 @@ struct PushNotifications::Pimpl : private PushNotificationsDelegate #endif [[UIApplication sharedApplication] registerForRemoteNotifications]; - - initialised = true; } void requestSettingsUsed() @@ -980,6 +852,8 @@ struct PushNotifications::Pimpl : private PushNotificationsDelegate deviceToken = nsStringToJuce (deviceTokenString); + initialised = true; + owner.listeners.call (&PushNotifications::Listener::deviceTokenRefreshed, deviceToken); } diff --git a/modules/juce_gui_extra/native/juce_mac_PushNotifications.cpp b/modules/juce_gui_extra/native/juce_mac_PushNotifications.cpp new file mode 100644 index 0000000000..47a9a1df9f --- /dev/null +++ b/modules/juce_gui_extra/native/juce_mac_PushNotifications.cpp @@ -0,0 +1,557 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2017 - ROLI Ltd. + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 5 End-User License + Agreement and JUCE 5 Privacy Policy (both updated and effective as of the + 27th April 2017). + + End User License Agreement: www.juce.com/juce-5-licence + Privacy Policy: www.juce.com/juce-5-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +template <> struct ContainerDeletePolicy> { static void destroy (NSObject* o) { [o release]; } }; + +namespace PushNotificationsDelegateDetailsOsx +{ + using Action = PushNotifications::Notification::Action; + + //============================================================================== + NSUserNotification* juceNotificationToNSUserNotification (const PushNotifications::Notification& n, + bool isEarlierThanMavericks, + bool isEarlierThanYosemite) + { + auto* notification = [[NSUserNotification alloc] init]; + + notification.title = juceStringToNS (n.title); + notification.subtitle = juceStringToNS (n.subtitle); + notification.informativeText = juceStringToNS (n.body); + notification.userInfo = varObjectToNSDictionary (n.properties); + + auto triggerTime = Time::getCurrentTime() + RelativeTime (n.triggerIntervalSec); + notification.deliveryDate = [NSDate dateWithTimeIntervalSince1970: triggerTime.toMilliseconds() / 1000.]; + + if (n.repeat && n.triggerIntervalSec >= 60) + { + auto* dateComponents = [[NSDateComponents alloc] init]; + auto intervalSec = NSInteger (n.triggerIntervalSec); + dateComponents.second = intervalSec; + dateComponents.nanosecond = NSInteger ((n.triggerIntervalSec - intervalSec) * 1000000000); + + notification.deliveryRepeatInterval = dateComponents; + + [dateComponents autorelease]; + } + + auto soundToPlayString = n.soundToPlay.toString (true); + + if (soundToPlayString == "default_os_sound") + { + notification.soundName = NSUserNotificationDefaultSoundName; + } + else if (soundToPlayString.isNotEmpty()) + { + auto* soundName = juceStringToNS (soundToPlayString.fromLastOccurrenceOf ("/", false, false) + .upToLastOccurrenceOf (".", false, false)); + + notification.soundName = soundName; + } + + notification.hasActionButton = n.actions.size() > 0; + + if (n.actions.size() > 0) + notification.actionButtonTitle = juceStringToNS (n.actions.getReference (0).title); + + if (! isEarlierThanMavericks) + { + notification.identifier = juceStringToNS (n.identifier); + + if (n.actions.size() > 0) + { + notification.hasReplyButton = n.actions.getReference (0).style == Action::text; + notification.responsePlaceholder = juceStringToNS (n.actions.getReference (0).textInputPlaceholder); + } + + auto* imageDirectory = n.icon.contains ("/") + ? juceStringToNS (n.icon.upToLastOccurrenceOf ("/", false, true)) + : [NSString string]; + + auto* imageName = juceStringToNS (n.icon.fromLastOccurrenceOf ("/", false, false) + .upToLastOccurrenceOf (".", false, false)); + auto* imageExtension = juceStringToNS (n.icon.fromLastOccurrenceOf (".", false, false)); + + NSString* imagePath = nil; + + if ([imageDirectory length] == NSUInteger (0)) + { + imagePath = [[NSBundle mainBundle] pathForResource: imageName + ofType: imageExtension]; + } + else + { + imagePath = [[NSBundle mainBundle] pathForResource: imageName + ofType: imageExtension + inDirectory: imageDirectory]; + } + + notification.contentImage = [[NSImage alloc] initWithContentsOfFile: imagePath]; + + if (! isEarlierThanYosemite) + { + if (n.actions.size() > 1) + { + auto* additionalActions = [NSMutableArray arrayWithCapacity: (NSUInteger) n.actions.size() - 1]; + + for (int a = 1; a < n.actions.size(); ++a) + [additionalActions addObject: [NSUserNotificationAction actionWithIdentifier: juceStringToNS (n.actions[a].identifier) + title: juceStringToNS (n.actions[a].title)]]; + + notification.additionalActions = additionalActions; + } + } + } + + [notification autorelease]; + + return notification; + } + + //============================================================================== + PushNotifications::Notification nsUserNotificationToJuceNotification (NSUserNotification* n, + bool isEarlierThanMavericks, + bool isEarlierThanYosemite) + { + PushNotifications::Notification notif; + + notif.title = nsStringToJuce (n.title); + notif.subtitle = nsStringToJuce (n.subtitle); + notif.body = nsStringToJuce (n.informativeText); + + notif.repeat = n.deliveryRepeatInterval != nil; + + if (n.deliveryRepeatInterval != nil) + { + notif.triggerIntervalSec = n.deliveryRepeatInterval.second + (n.deliveryRepeatInterval.nanosecond / 1000000000.); + } + else + { + NSDate* dateNow = [NSDate date]; + notif.triggerIntervalSec = [dateNow timeIntervalSinceDate: n.deliveryDate]; + } + + notif.soundToPlay = URL (nsStringToJuce (n.soundName)); + notif.properties = nsDictionaryToVar (n.userInfo); + + if (! isEarlierThanMavericks) + { + notif.identifier = nsStringToJuce (n.identifier); + + if (n.contentImage != nil) + notif.icon = nsStringToJuce ([n.contentImage name]); + } + + Array actions; + + if (n.actionButtonTitle != nil) + { + Action action; + action.title = nsStringToJuce (n.actionButtonTitle); + + if (! isEarlierThanMavericks) + { + if (n.hasReplyButton) + action.style = Action::text; + + if (n.responsePlaceholder != nil) + action.textInputPlaceholder = nsStringToJuce (n.responsePlaceholder); + } + + actions.add (action); + } + + if (! isEarlierThanYosemite) + { + if (n.additionalActions != nil) + { + for (NSUserNotificationAction* a in n.additionalActions) + { + Action action; + action.identifier = nsStringToJuce (a.identifier); + action.title = nsStringToJuce (a.title); + + actions.add (action); + } + } + } + + return notif; + } + + //============================================================================== + var getNotificationPropertiesFromDictionaryVar (const var& dictionaryVar) + { + auto* dictionaryVarObject = dictionaryVar.getDynamicObject(); + + if (dictionaryVarObject == nullptr) + return {}; + + const auto& properties = dictionaryVarObject->getProperties(); + + DynamicObject::Ptr propsVarObject = new DynamicObject(); + + for (int i = 0; i < properties.size(); ++i) + { + auto propertyName = properties.getName (i).toString(); + + if (propertyName == "aps") + continue; + + propsVarObject->setProperty (propertyName, properties.getValueAt (i)); + } + + return var (propsVarObject.get()); + } + + PushNotifications::Notification nsDictionaryToJuceNotification (NSDictionary* dictionary) + { + const var dictionaryVar = nsDictionaryToVar (dictionary); + + const var apsVar = dictionaryVar.getProperty ("aps", {}); + + if (! apsVar.isObject()) + return {}; + + var alertVar = apsVar.getProperty ("alert", {}); + + const var titleVar = alertVar.getProperty ("title", {}); + const var bodyVar = alertVar.isObject() ? alertVar.getProperty ("body", {}) : alertVar; + + const var categoryVar = apsVar.getProperty ("category", {}); + const var soundVar = apsVar.getProperty ("sound", {}); + const var badgeVar = apsVar.getProperty ("badge", {}); + const var threadIdVar = apsVar.getProperty ("thread-id", {}); + + PushNotifications::Notification notification; + + notification.title = titleVar .toString(); + notification.body = bodyVar .toString(); + notification.groupId = threadIdVar.toString(); + notification.category = categoryVar.toString(); + notification.soundToPlay = URL (soundVar.toString()); + notification.badgeNumber = (int) badgeVar; + notification.properties = getNotificationPropertiesFromDictionaryVar (dictionaryVar); + + return notification; + } +} + +//============================================================================== +struct PushNotificationsDelegate +{ + PushNotificationsDelegate() : delegate ([getClass().createInstance() init]) + { + Class::setThis (delegate, this); + + id appDelegate = [[NSApplication sharedApplication] delegate]; + + SEL selector = NSSelectorFromString (@"setPushNotificationsDelegate:"); + + if ([appDelegate respondsToSelector: selector]) + [appDelegate performSelector: selector withObject: delegate]; + + [NSUserNotificationCenter defaultUserNotificationCenter].delegate = delegate; + } + + virtual ~PushNotificationsDelegate() + { + [NSUserNotificationCenter defaultUserNotificationCenter].delegate = nil; + } + + virtual void registeredForRemoteNotifications (NSData* deviceToken) = 0; + + virtual void failedToRegisterForRemoteNotifications (NSError* error) = 0; + + virtual void didReceiveRemoteNotification (NSDictionary* userInfo) = 0; + + virtual void didDeliverNotification (NSUserNotification* notification) = 0; + + virtual void didActivateNotification (NSUserNotification* notification) = 0; + + virtual bool shouldPresentNotification (NSUserNotification* notification) = 0; + +protected: + ScopedPointer> delegate; + +private: + struct Class : public ObjCClass> + { + Class() : ObjCClass> ("JucePushNotificationsDelegate_") + { + addIvar ("self"); + + addMethod (@selector (application:didRegisterForRemoteNotificationsWithDeviceToken:), registeredForRemoteNotifications, "v@:@@"); + addMethod (@selector (application:didFailToRegisterForRemoteNotificationsWithError:), failedToRegisterForRemoteNotifications, "v@:@@"); + addMethod (@selector (application:didReceiveRemoteNotification:), didReceiveRemoteNotification, "v@:@@"); + addMethod (@selector (userNotificationCenter:didDeliverNotification:), didDeliverNotification, "v@:@@"); + addMethod (@selector (userNotificationCenter:didActivateNotification:), didActivateNotification, "v@:@@"); + addMethod (@selector (userNotificationCenter:shouldPresentNotification:), shouldPresentNotification, "B@:@@"); + + registerClass(); + } + + //============================================================================== + static PushNotificationsDelegate& getThis (id self) { return *getIvar (self, "self"); } + static void setThis (id self, PushNotificationsDelegate* d) { object_setInstanceVariable (self, "self", d); } + + //============================================================================== + static void registeredForRemoteNotifications (id self, SEL, NSApplication*, + NSData* deviceToken) { getThis (self).registeredForRemoteNotifications (deviceToken); } + + static void failedToRegisterForRemoteNotifications (id self, SEL, NSApplication*, + NSError* error) { getThis (self).failedToRegisterForRemoteNotifications (error); } + + static void didReceiveRemoteNotification (id self, SEL, NSApplication*, + NSDictionary* userInfo) { getThis (self).didReceiveRemoteNotification (userInfo); } + + static void didDeliverNotification (id self, SEL, NSUserNotificationCenter*, + NSUserNotification* notification) { getThis (self).didDeliverNotification (notification); } + + static void didActivateNotification (id self, SEL, NSUserNotificationCenter*, + NSUserNotification* notification) { getThis (self).didActivateNotification (notification); } + + static bool shouldPresentNotification (id self, SEL, NSUserNotificationCenter*, + NSUserNotification* notification) { return getThis (self).shouldPresentNotification (notification); } + }; + + //============================================================================== + static Class& getClass() + { + static Class c; + return c; + } +}; + +//============================================================================== +bool PushNotifications::Notification::isValid() const noexcept { return true; } + +//============================================================================== +struct PushNotifications::Pimpl : private PushNotificationsDelegate +{ + Pimpl (PushNotifications& p) + : owner (p) + { + } + + void requestPermissionsWithSettings (const PushNotifications::Settings& settingsToUse) + { + if (isEarlierThanLion) + return; + + settings = settingsToUse; + + NSRemoteNotificationType types = NSUInteger ((bool) settings.allowBadge); + + if (isAtLeastMountainLion) + types |= ((bool) settings.allowSound << 1 | (bool) settings.allowAlert << 2); + + [[NSApplication sharedApplication] registerForRemoteNotificationTypes: types]; + } + + void requestSettingsUsed() + { + if (isEarlierThanLion) + { + // no settings available + owner.listeners.call (&PushNotifications::Listener::notificationSettingsReceived, {}); + return; + } + + settings.allowBadge = [NSApplication sharedApplication].enabledRemoteNotificationTypes & NSRemoteNotificationTypeBadge; + + if (isAtLeastMountainLion) + { + settings.allowSound = [NSApplication sharedApplication].enabledRemoteNotificationTypes & NSRemoteNotificationTypeSound; + settings.allowAlert = [NSApplication sharedApplication].enabledRemoteNotificationTypes & NSRemoteNotificationTypeAlert; + } + + owner.listeners.call (&PushNotifications::Listener::notificationSettingsReceived, settings); + } + + bool areNotificationsEnabled() const { return true; } + + void sendLocalNotification (const Notification& n) + { + auto* notification = PushNotificationsDelegateDetailsOsx::juceNotificationToNSUserNotification (n, isEarlierThanMavericks, isEarlierThanYosemite); + + [[NSUserNotificationCenter defaultUserNotificationCenter] scheduleNotification: notification]; + } + + void getDeliveredNotifications() const + { + Array notifs; + + for (NSUserNotification* n in [NSUserNotificationCenter defaultUserNotificationCenter].deliveredNotifications) + notifs.add (PushNotificationsDelegateDetailsOsx::nsUserNotificationToJuceNotification (n, isEarlierThanMavericks, isEarlierThanYosemite)); + + owner.listeners.call (&Listener::deliveredNotificationsListReceived, notifs); + } + + void removeAllDeliveredNotifications() + { + [[NSUserNotificationCenter defaultUserNotificationCenter] removeAllDeliveredNotifications]; + } + + void removeDeliveredNotification (const String& identifier) + { + PushNotifications::Notification n; + n.identifier = identifier; + + auto nsNotification = PushNotificationsDelegateDetailsOsx::juceNotificationToNSUserNotification (n, isEarlierThanMavericks, isEarlierThanYosemite); + + [[NSUserNotificationCenter defaultUserNotificationCenter] removeDeliveredNotification: nsNotification]; + } + + void setupChannels (const Array& groups, const Array& channels) + { + ignoreUnused (groups, channels); + } + + void getPendingLocalNotifications() const + { + Array notifs; + + for (NSUserNotification* n in [NSUserNotificationCenter defaultUserNotificationCenter].scheduledNotifications) + notifs.add (PushNotificationsDelegateDetailsOsx::nsUserNotificationToJuceNotification (n, isEarlierThanMavericks, isEarlierThanYosemite)); + + owner.listeners.call (&PushNotifications::Listener::pendingLocalNotificationsListReceived, notifs); + } + + void removePendingLocalNotification (const String& identifier) + { + PushNotifications::Notification n; + n.identifier = identifier; + + auto nsNotification = PushNotificationsDelegateDetailsOsx::juceNotificationToNSUserNotification (n, isEarlierThanMavericks, isEarlierThanYosemite); + + [[NSUserNotificationCenter defaultUserNotificationCenter] removeScheduledNotification: nsNotification]; + } + + void removeAllPendingLocalNotifications() + { + for (NSUserNotification* n in [NSUserNotificationCenter defaultUserNotificationCenter].scheduledNotifications) + [[NSUserNotificationCenter defaultUserNotificationCenter] removeScheduledNotification: n]; + } + + String getDeviceToken() + { + // You need to call requestPermissionsWithSettings() first. + jassert (initialised); + + return deviceToken; + } + + //============================================================================== + //PushNotificationsDelegate + void registeredForRemoteNotifications (NSData* deviceTokenToUse) override + { + auto* deviceTokenString = [[[[deviceTokenToUse description] + stringByReplacingOccurrencesOfString: nsStringLiteral ("<") withString: nsStringLiteral ("")] + stringByReplacingOccurrencesOfString: nsStringLiteral (">") withString: nsStringLiteral ("")] + stringByReplacingOccurrencesOfString: nsStringLiteral (" ") withString: nsStringLiteral ("")]; + + deviceToken = nsStringToJuce (deviceTokenString); + + initialised = true; + + owner.listeners.call (&PushNotifications::Listener::deviceTokenRefreshed, deviceToken); + } + + void failedToRegisterForRemoteNotifications (NSError* error) override + { + ignoreUnused (error); + + deviceToken.clear(); + } + + void didReceiveRemoteNotification (NSDictionary* userInfo) override + { + auto n = PushNotificationsDelegateDetailsOsx::nsDictionaryToJuceNotification (userInfo); + + owner.listeners.call (&PushNotifications::Listener::handleNotification, true, n); + } + + void didDeliverNotification (NSUserNotification* notification) override + { + ignoreUnused (notification); + } + + void didActivateNotification (NSUserNotification* notification) override + { + auto n = PushNotificationsDelegateDetailsOsx::nsUserNotificationToJuceNotification (notification, isEarlierThanMavericks, isEarlierThanYosemite); + + if (notification.activationType == NSUserNotificationActivationTypeContentsClicked) + { + owner.listeners.call (&PushNotifications::Listener::handleNotification, notification.remote, n); + } + else + { + auto actionIdentifier = (! isEarlierThanYosemite && notification.additionalActivationAction != nil) + ? nsStringToJuce (notification.additionalActivationAction.identifier) + : nsStringToJuce (notification.actionButtonTitle); + + auto reply = notification.activationType == NSUserNotificationActivationTypeReplied + ? nsStringToJuce ([notification.response string]) + : String(); + + owner.listeners.call (&PushNotifications::Listener::handleNotificationAction, notification.remote, n, actionIdentifier, reply); + } + } + + bool shouldPresentNotification (NSUserNotification* notification) override { return true; } + + void subscribeToTopic (const String& topic) { ignoreUnused (topic); } + void unsubscribeFromTopic (const String& topic) { ignoreUnused (topic); } + + void sendUpstreamMessage (const String& serverSenderId, + const String& collapseKey, + const String& messageId, + const String& messageType, + int timeToLive, + const StringPairArray& additionalData) + { + ignoreUnused (serverSenderId, collapseKey, messageId, messageType); + ignoreUnused (timeToLive, additionalData); + } + +private: + PushNotifications& owner; + + const bool isEarlierThanLion = std::floor (NSFoundationVersionNumber) < std::floor (NSFoundationVersionNumber10_7); + const bool isAtLeastMountainLion = std::floor (NSFoundationVersionNumber) >= NSFoundationVersionNumber10_7; + const bool isEarlierThanMavericks = std::floor (NSFoundationVersionNumber) < NSFoundationVersionNumber10_9; + const bool isEarlierThanYosemite = std::floor (NSFoundationVersionNumber) <= NSFoundationVersionNumber10_9; + + bool initialised = false; + String deviceToken; + + PushNotifications::Settings settings; +}; + +} // namespace juce