Repository: jchambers/pushy (java)

Issue:
ios15 feature: relavance score
Please add support for relevance-score in push payload

Pull request description:
Add support for relevance scores in payloads
This adds support for the new `relevance-score` payload field introduced in iOS 15 and closes #903.

Patch:
diff --git a/pushy/src/main/java/com/eatthepath/pushy/apns/util/ApnsPayloadBuilder.java b/pushy/src/main/java/com/eatthepath/pushy/apns/util/ApnsPayloadBuilder.java
index b18da073..632fa678 100644
--- a/pushy/src/main/java/com/eatthepath/pushy/apns/util/ApnsPayloadBuilder.java
+++ b/pushy/src/main/java/com/eatthepath/pushy/apns/util/ApnsPayloadBuilder.java
@@ -74,6 +74,8 @@ public abstract class ApnsPayloadBuilder {
 
     private InterruptionLevel interruptionLevel = null;
 
+    private Double relevanceScore = null;
+
     private String[] urlArguments = null;
 
     private final HashMap<String, Object> customProperties = new HashMap<>();
@@ -93,6 +95,7 @@ public abstract class ApnsPayloadBuilder {
     private static final String SUMMARY_ARGUMENT_COUNT_KEY = "summary-arg-count";
     private static final String URL_ARGS_KEY = "url-args";
     private static final String INTERRUPTION_LEVEL_KEY = "interruption-level";
+    private static final String RELEVANCE_SCORE_KEY = "relevance-score";
 
     private static final String ALERT_TITLE_KEY = "title";
     private static final String ALERT_TITLE_LOC_KEY = "title-loc-key";
@@ -599,13 +602,38 @@ public abstract class ApnsPayloadBuilder {
      * @see <a href="https://developer.apple.com/design/human-interface-guidelines/ios/system-capabilities/notifications/">Human
      * Interface Guidelines: Notifications</a>
      *
-     * @since 0.15
+     * @since 0.15.0
      */
     public ApnsPayloadBuilder setInterruptionLevel(final InterruptionLevel interruptionLevel) {
         this.interruptionLevel = interruptionLevel;
         return this;
     }
 
+    /**
+     * <p>Sets the relevance score for this notification. By default, no relevance score is included.</p>
+     *
+     * <p>The relevance score is:</p>
+     *
+     * <blockquote>…a number between 0 and 1, that the system uses to sort the notifications from your app. The highest
+     * score gets featured in the notification summary.</blockquote>
+     *
+     * @param relevanceScore a relevance score between 0 and 1, inclusive, or {@code null} if no relevance score should
+     * be included in the notification
+     *
+     * @return a reference to this payload builder
+     *
+     * @since 0.15.0
+     */
+    public ApnsPayloadBuilder setRelevanceScore(final Double relevanceScore) {
+        if (relevanceScore != null && (relevanceScore < 0 || relevanceScore > 1 || relevanceScore.isNaN())) {
+            throw new IllegalArgumentException("Relevance score must be a number between 0 and 1, inclusive, but was actually " + relevanceScore);
+        }
+
+        this.relevanceScore = relevanceScore;
+
+        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
@@ -734,6 +762,10 @@ public abstract class ApnsPayloadBuilder {
                 aps.put(INTERRUPTION_LEVEL_KEY, this.interruptionLevel.getValue());
             }
 
+            if (this.relevanceScore != null) {
+                aps.put(RELEVANCE_SCORE_KEY, this.relevanceScore);
+            }
+
             final Map<String, Object> alert = new HashMap<>();
             {
                 if (this.alertBody != null) {


Test patch:
diff --git a/pushy/src/test/java/com/eatthepath/pushy/apns/util/ApnsPayloadBuilderTest.java b/pushy/src/test/java/com/eatthepath/pushy/apns/util/ApnsPayloadBuilderTest.java
index 2ae99159..d1e0964c 100644
--- a/pushy/src/test/java/com/eatthepath/pushy/apns/util/ApnsPayloadBuilderTest.java
+++ b/pushy/src/test/java/com/eatthepath/pushy/apns/util/ApnsPayloadBuilderTest.java
@@ -28,6 +28,7 @@ import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.Arguments;
 import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.ValueSource;
 
 import java.text.ParseException;
 import java.util.ArrayList;
@@ -483,6 +484,32 @@ public abstract class ApnsPayloadBuilderTest {
         assertEquals("time-sensitive", aps.get("interruption-level"));
     }
 
+    @Test
+    void testSetRelevanceScore() {
+        {
+            final double relevanceScore = 0.7;
+            this.builder.setRelevanceScore(relevanceScore);
+
+            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.build());
+
+            assertEquals(relevanceScore, ((Number) aps.get("relevance-score")).doubleValue());
+        }
+
+        {
+            this.builder.setRelevanceScore(null);
+
+            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.build());
+
+            assertFalse(aps.containsKey("relevance-score"));
+        }
+    }
+
+    @ParameterizedTest
+    @ValueSource(doubles = {-7, 1.1, Double.NaN, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY})
+    void testSetRelevanceScoreIllegalValue(final double illegalRelevanceScore) {
+        assertThrows(IllegalArgumentException.class, () -> this.builder.setRelevanceScore(illegalRelevanceScore));
+    }
+
     @Test
     void testSetSummaryArgumentCountNonPositive() {
         assertThrows(IllegalArgumentException.class, () -> this.builder.setSummaryArgumentCount(0));