Repository: relayrides/pushy (java)

Issue:
Add fields for Safari push notifications
Sending a Safari push notification requires a "url-args" field in the "aps" object. It also allows an optional "action" field in the "alert" object. There is no apparent way to add either of these fields with ApnsPayloadBuilder.

[See documentation here](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/NotificationProgrammingGuideForWebsites/PushNotifications/PushNotifications.html#//apple_ref/doc/uid/TP40013225-CH3-SW12). Sample payload looks like:

```
{
    "aps": {
        "alert": {
            "title": "Flight A998 Now Boarding",
            "body": "Boarding has begun for Flight A998.",
            "action": "View"
        },
        "url-args": ["boarding", "A998"]
    }
}
```


Pull request description:
Fields for Safari notifications (closes #373)
As requested in #373, this adds fields for Safari push notifications to the payload builder.

Patch:
diff --git a/pushy/src/main/java/com/relayrides/pushy/apns/util/ApnsPayloadBuilder.java b/pushy/src/main/java/com/relayrides/pushy/apns/util/ApnsPayloadBuilder.java
index 5c84d867..82738f23 100644
--- a/pushy/src/main/java/com/relayrides/pushy/apns/util/ApnsPayloadBuilder.java
+++ b/pushy/src/main/java/com/relayrides/pushy/apns/util/ApnsPayloadBuilder.java
@@ -24,6 +24,7 @@ import java.io.CharArrayWriter;
 import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 import com.google.gson.Gson;
@@ -52,12 +53,14 @@ public class ApnsPayloadBuilder {
     private String[] localizedAlertSubtitleArguments = null;
     private String launchImageFileName = null;
     private boolean showActionButton = true;
+    private String actionButtonLabel = null;
     private String localizedActionButtonKey = null;
     private Integer badgeNumber = null;
     private String soundFileName = null;
     private String categoryName = null;
     private boolean contentAvailable = false;
     private boolean mutableContent = false;
+    private String[] urlArguments = null;
 
     private boolean preferStringRepresentationForAlerts = false;
 
@@ -70,6 +73,7 @@ public class ApnsPayloadBuilder {
     private static final String CATEGORY_KEY = "category";
     private static final String CONTENT_AVAILABLE_KEY = "content-available";
     private static final String MUTABLE_CONTENT_KEY = "mutable-content";
+    private static final String URL_ARGS_KEY = "url-args";
 
     private static final String ALERT_TITLE_KEY = "title";
     private static final String ALERT_TITLE_LOC_KEY = "title-loc-key";
@@ -80,6 +84,7 @@ public class ApnsPayloadBuilder {
     private static final String ALERT_BODY_KEY = "body";
     private static final String ALERT_LOC_KEY = "loc-key";
     private static final String ALERT_ARGS_KEY = "loc-args";
+    private static final String ACTION_KEY = "action";
     private static final String ACTION_LOC_KEY = "action-loc-key";
     private static final String LAUNCH_IMAGE_KEY = "launch-image";
 
@@ -285,17 +290,43 @@ public class ApnsPayloadBuilder {
         return this;
     }
 
+    /**
+     * <p>Sets the literal text of the action button to be shown for the push notification <em>for Safari push
+     * notifications only</em>. Clears any previously-set localized action key. By default, the OS-default label will be
+     * used for the action button.</p>
+     *
+     * @param action the literal label to be shown on the action button for this notification
+     *
+     * @return a reference to this payload builder
+     *
+     * @see ApnsPayloadBuilder#setLocalizedActionButtonKey(String)
+     * @see ApnsPayloadBuilder#setShowActionButton(boolean)
+     *
+     * @since 0.8.2
+     */
+    public ApnsPayloadBuilder setActionButtonLabel(final String action) {
+        this.actionButtonLabel = action;
+        this.localizedActionButtonKey = null;
+
+        return this;
+    }
+
     /**
      * <p>Sets the key of a string in the receiving app's localized string list to be used as the label of the
-     * &quot;action&quot; button if the push notification is displayed as an alert. By default, the OS-default label
-     * will be used for the action button.</p>
+     * &quot;action&quot; button if the push notification is displayed as an alert. Clears any previously-set literal
+     * action button label. By default, the OS-default label will be used for the action button.</p>
      *
      * @param localizedActionButtonKey a key to a string in the receiving app's localized string list
      *
      * @return a reference to this payload builder
+     *
+     * @see ApnsPayloadBuilder#setActionButtonLabel(String)
+     * @see ApnsPayloadBuilder#setShowActionButton(boolean)
      */
     public ApnsPayloadBuilder setLocalizedActionButtonKey(final String localizedActionButtonKey) {
         this.localizedActionButtonKey = localizedActionButtonKey;
+        this.actionButtonLabel = null;
+
         return this;
     }
 
@@ -392,6 +423,59 @@ public class ApnsPayloadBuilder {
         return this;
     }
 
+    /**
+     * <p>Sets the list of arguments to populate placeholders in the {@code urlFormatString} associated with a Safari
+     * push notification. Has no effect for non-Safari notifications. According to the Notification Programming Guide
+     * for Websites:</p>
+     *
+     * <blockquote>The {@code url-args} key must be included [for Safari push notifications]. The number of elements in
+     * the array must match the number of placeholders in the {@code urlFormatString} value and the order of the
+     * placeholders in the URL format string determines the order of the values supplied by the {@code url-args} array.
+     * The number of placeholders may be zero, in which case the array should be empty. However, it is common practice
+     * to always include at least one argument so that the user is directed to a web page specific to the notification
+     * received.</blockquote>
+     *
+     * @param arguments the arguments with which to populate URL placeholders, which may be an empty list; if
+     * {@code null}, the {@code url-args} key is ommitted from the payload entirely
+     *
+     * @return a reference to this payload builder
+     *
+     * @see <a href="https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/NotificationProgrammingGuideForWebsites/PushNotifications/PushNotifications.html#//apple_ref/doc/uid/TP40013225-CH3-SW1">
+     *      Notification Programming Guide for Websites - Configuring Safari Push Notifications</a>
+     *
+     * @since 0.8.2
+     */
+    public ApnsPayloadBuilder setUrlArguments(final List<String> arguments) {
+        return this.setUrlArguments(arguments != null ? arguments.toArray(new String[0]) : null);
+    }
+
+    /**
+     * <p>Sets the list of arguments to populate placeholders in the {@code urlFormatString} associated with a Safari
+     * push notification. Has no effect for non-Safari notifications. According to the Notification Programming Guide
+     * for Websites:</p>
+     *
+     * <blockquote>The {@code url-args} key must be included [for Safari push notifications]. The number of elements in
+     * the array must match the number of placeholders in the {@code urlFormatString} value and the order of the
+     * placeholders in the URL format string determines the order of the values supplied by the {@code url-args} array.
+     * The number of placeholders may be zero, in which case the array should be empty. However, it is common practice
+     * to always include at least one argument so that the user is directed to a web page specific to the notification
+     * received.</blockquote>
+     *
+     * @param arguments the arguments with which to populate URL placeholders, which may be an empty array; if
+     * {@code null}, the {@code url-args} key is ommitted from the payload entirely
+     *
+     * @return a reference to this payload builder
+     *
+     * @see <a href="https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/NotificationProgrammingGuideForWebsites/PushNotifications/PushNotifications.html#//apple_ref/doc/uid/TP40013225-CH3-SW1">
+     *      Notification Programming Guide for Websites - Configuring Safari Push Notifications</a>
+     *
+     * @since 0.8.2
+     */
+    public ApnsPayloadBuilder setUrlArguments(final String... arguments) {
+        this.urlArguments = arguments;
+        return this;
+    }
+
     /**
      * <p>Adds a custom property to the payload. According to Apple's documentation:</p>
      *
@@ -477,6 +561,10 @@ public class ApnsPayloadBuilder {
                     if (this.localizedActionButtonKey != null) {
                         alert.put(ACTION_LOC_KEY, this.localizedActionButtonKey);
                     }
+
+                    if (this.actionButtonLabel != null) {
+                        alert.put(ACTION_KEY, this.actionButtonLabel);
+                    }
                 } else {
                     // To hide the action button, the key needs to be present, but the value needs to be null
                     alert.put(ACTION_LOC_KEY, null);
@@ -517,6 +605,10 @@ public class ApnsPayloadBuilder {
                 aps.put(ALERT_KEY, alert);
             }
 
+            if (this.urlArguments != null) {
+                aps.put(URL_ARGS_KEY, this.urlArguments);
+            }
+
             payload.put(APS_KEY, aps);
         }
 


Test patch:
diff --git a/pushy/src/test/java/com/relayrides/pushy/apns/util/ApnsPayloadBuilderTest.java b/pushy/src/test/java/com/relayrides/pushy/apns/util/ApnsPayloadBuilderTest.java
index 920ac6f5..12ed91d6 100644
--- a/pushy/src/test/java/com/relayrides/pushy/apns/util/ApnsPayloadBuilderTest.java
+++ b/pushy/src/test/java/com/relayrides/pushy/apns/util/ApnsPayloadBuilderTest.java
@@ -28,6 +28,7 @@ import static org.junit.Assert.assertTrue;
 import java.lang.reflect.Type;
 import java.nio.charset.CharsetEncoder;
 import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 
@@ -309,9 +310,27 @@ public class ApnsPayloadBuilderTest {
         }
     }
 
+    @Test
+    public void testSetActionButtonLabel() {
+        final String actionButtonLabel = "Bam! Pow!";
+
+        this.builder.setLocalizedActionButtonKey("action.key");
+        this.builder.setActionButtonLabel(actionButtonLabel);
+
+        final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.buildWithDefaultMaximumLength());
+
+        @SuppressWarnings("unchecked")
+        final Map<String, Object> alert = (Map<String, Object>) aps.get("alert");
+
+        assertEquals(actionButtonLabel, alert.get("action"));
+        assertNull(alert.get("action-loc-key"));
+    }
+
     @Test
     public void testSetLocalizedActionButtonKey() {
         final String actionButtonKey = "action.key";
+
+        this.builder.setActionButtonLabel("Literal action button label");
         this.builder.setLocalizedActionButtonKey(actionButtonKey);
 
         final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.buildWithDefaultMaximumLength());
@@ -320,6 +339,7 @@ public class ApnsPayloadBuilderTest {
         final Map<String, Object> alert = (Map<String, Object>) aps.get("alert");
 
         assertEquals(actionButtonKey, alert.get("action-loc-key"));
+        assertNull(alert.get("action"));
     }
 
     @Test
@@ -396,6 +416,73 @@ public class ApnsPayloadBuilderTest {
         assertEquals(customValue, payload.get(customKey));
     }
 
+    @Test
+    public void testSetUrlArgumentsList() {
+        {
+            final List<String> arguments = new ArrayList<>();
+            arguments.add("first");
+            arguments.add("second");
+            arguments.add("third");
+
+            this.builder.setUrlArguments(arguments);
+
+            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.buildWithDefaultMaximumLength());
+
+            assertEquals(arguments, aps.get("url-args"));
+        }
+
+        {
+            final List<String> arguments = new ArrayList<>();
+            this.builder.setUrlArguments(arguments);
+
+            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.buildWithDefaultMaximumLength());
+
+            assertEquals(arguments, aps.get("url-args"));
+        }
+
+        {
+            this.builder.setUrlArguments((List<String>) null);
+
+            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.buildWithDefaultMaximumLength());
+
+            assertNull(aps.get("url-args"));
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    @Test
+    public void testSetUrlArgumentsArray() {
+        {
+            this.builder.setUrlArguments("first", "second", "third");
+
+            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.buildWithDefaultMaximumLength());
+
+            final List<String> argumentsFromPayload = (List<String>) aps.get("url-args");
+
+            assertEquals(3, argumentsFromPayload.size());
+            assertEquals("first", argumentsFromPayload.get(0));
+            assertEquals("second", argumentsFromPayload.get(1));
+            assertEquals("third", argumentsFromPayload.get(2));
+        }
+
+        {
+            final String[] arguments = new String[0];
+            this.builder.setUrlArguments(arguments);
+
+            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.buildWithDefaultMaximumLength());
+
+            assertTrue(((List<String>) aps.get("url-args")).isEmpty());
+        }
+
+        {
+            this.builder.setUrlArguments((String[]) null);
+
+            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.buildWithDefaultMaximumLength());
+
+            assertNull(aps.get("url-args"));
+        }
+    }
+
     @Test
     public void testBuildWithMaximumLength() {
         final String reallyLongAlertMessage = "All non-glanded recruited mercenaries now engaging in training " +