Repository: firebase/firebase-admin-java
Pull request #285 (2019-07-02 11:49:19)

Add support for analytics_label to be specified

the-real-mrcs: Thanks for reviewing!

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 71672db..ab91dd8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,7 @@
 # Unreleased
 
+- [added] Added `FcmOptions`, `ApnsFcmOptions` and `AndroidFcmOptions` to the
+  `FirebaseMessaging` API, which all provides a `setAnalyticsLabel()` method.
 - [changed] Upgraded the Cloud Firestore client to 1.9.0.
 - [changed] Upgraded the Cloud Storage client to 1.79.0.
 - [changed] Upgraded the Google API client to 1.30.1.
diff --git a/src/main/java/com/google/firebase/messaging/AndroidConfig.java b/src/main/java/com/google/firebase/messaging/AndroidConfig.java
index 065376a..dc0a455 100644
--- a/src/main/java/com/google/firebase/messaging/AndroidConfig.java
+++ b/src/main/java/com/google/firebase/messaging/AndroidConfig.java
@@ -49,6 +49,9 @@ public class AndroidConfig {
   @Key("notification")
   private final AndroidNotification notification;
 
+  @Key("fcm_options")
+  private final AndroidFcmOptions fcmOptions;
+
   private AndroidConfig(Builder builder) {
     this.collapseKey = builder.collapseKey;
     if (builder.priority != null) {
@@ -71,6 +74,7 @@ public class AndroidConfig {
     this.restrictedPackageName = builder.restrictedPackageName;
     this.data = builder.data.isEmpty() ? null : ImmutableMap.copyOf(builder.data);
     this.notification = builder.notification;
+    this.fcmOptions = builder.fcmOptions;
   }
 
   /**
@@ -98,6 +102,7 @@ public class AndroidConfig {
     private String restrictedPackageName;
     private final Map<String, String> data = new HashMap<>();
     private AndroidNotification notification;
+    private AndroidFcmOptions fcmOptions;
 
     private Builder() {}
 
@@ -187,6 +192,15 @@ public class AndroidConfig {
       return this;
     }
 
+    /**
+     * Sets the {@link AndroidFcmOptions}, which will override values set in the {@link FcmOptions}
+     * for Android messages.
+     */
+    public Builder setFcmOptions(AndroidFcmOptions androidFcmOptions) {
+      this.fcmOptions = androidFcmOptions;
+      return this;
+    }
+
     /**
      * Creates a new {@link AndroidConfig} instance from the parameters set on this builder.
      *
diff --git a/src/main/java/com/google/firebase/messaging/AndroidFcmOptions.java b/src/main/java/com/google/firebase/messaging/AndroidFcmOptions.java
new file mode 100644
index 0000000..cbe5dff
--- /dev/null
+++ b/src/main/java/com/google/firebase/messaging/AndroidFcmOptions.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.firebase.messaging;
+
+import com.google.api.client.util.Key;
+
+/**
+ * Represents the Android-specific FCM options that can be included in an {@link AndroidConfig}.
+ * Instances of this class are thread-safe and immutable.
+ */
+public final class AndroidFcmOptions {
+
+  @Key("analytics_label")
+  private final String analyticsLabel;
+
+  private AndroidFcmOptions(Builder builder) {
+    FcmOptionsUtil.checkAnalyticsLabel(builder.analyticsLabel);
+    this.analyticsLabel = builder.analyticsLabel;
+  }
+
+  /**
+   * Creates a new {@link AndroidFcmOptions} with the specified analytics label.
+   *
+   * @param analyticsLabel An analytics label
+   * @return An AndroidFcmOptions with the analytics label set to the supplied value.
+   */
+  public static AndroidFcmOptions withAnalyticsLabel(String analyticsLabel) {
+    return builder().setAnalyticsLabel(analyticsLabel).build();
+  }
+
+  /**
+   * Creates a new {@link AndroidFcmOptions.Builder}.
+   *
+   * @return A {@link AndroidFcmOptions.Builder} instance.
+   */
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder {
+
+    private String analyticsLabel;
+
+    private Builder() {}
+
+    /**
+     * @param analyticsLabel A string representing the analytics label used for Android messages.
+     * @return This builder
+     */
+    public Builder setAnalyticsLabel(String analyticsLabel) {
+      this.analyticsLabel = analyticsLabel;
+      return this;
+    }
+
+    /**
+     * Creates a new {@link AndroidFcmOptions} instance from the parameters set on this builder.
+     *
+     * @return A new {@link AndroidFcmOptions} instance.
+     * @throws IllegalArgumentException If any of the parameters set on the builder are invalid.
+     */
+    public AndroidFcmOptions build() {
+      return new AndroidFcmOptions(this);
+    }
+  }
+}
diff --git a/src/main/java/com/google/firebase/messaging/ApnsConfig.java b/src/main/java/com/google/firebase/messaging/ApnsConfig.java
index 5e503c0..0e5de86 100644
--- a/src/main/java/com/google/firebase/messaging/ApnsConfig.java
+++ b/src/main/java/com/google/firebase/messaging/ApnsConfig.java
@@ -38,6 +38,9 @@ public class ApnsConfig {
   @Key("payload")
   private final Map<String, Object> payload;
 
+  @Key("fcm_options")
+  private final ApnsFcmOptions fcmOptions;
+
   private ApnsConfig(Builder builder) {
     checkArgument(builder.aps != null, "aps must be specified");
     checkArgument(!builder.customData.containsKey("aps"),
@@ -47,6 +50,7 @@ public class ApnsConfig {
         .putAll(builder.customData)
         .put("aps", builder.aps.getFields())
         .build();
+    this.fcmOptions = builder.fcmOptions;
   }
 
   /**
@@ -63,6 +67,7 @@ public class ApnsConfig {
     private final Map<String, String> headers = new HashMap<>();
     private final Map<String, Object> customData = new HashMap<>();
     private Aps aps;
+    private ApnsFcmOptions fcmOptions;
 
     private Builder() {}
 
@@ -123,6 +128,15 @@ public class ApnsConfig {
       return this;
     }
 
+    /**
+     * Sets the {@link ApnsFcmOptions}, which will override values set in the {@link FcmOptions} for
+     * APNS messages.
+     */
+    public Builder setFcmOptions(ApnsFcmOptions apnsFcmOptions) {
+      this.fcmOptions = apnsFcmOptions;
+      return this;
+    }
+
     /**
      * Creates a new {@link ApnsConfig} instance from the parameters set on this builder.
      *
diff --git a/src/main/java/com/google/firebase/messaging/ApnsFcmOptions.java b/src/main/java/com/google/firebase/messaging/ApnsFcmOptions.java
new file mode 100644
index 0000000..a49095c
--- /dev/null
+++ b/src/main/java/com/google/firebase/messaging/ApnsFcmOptions.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.firebase.messaging;
+
+import com.google.api.client.util.Key;
+
+/**
+ * Represents the APNS-specific FCM options that can be included in an {@link ApnsConfig}. Instances
+ * of this class are thread-safe and immutable.
+ */
+public final class ApnsFcmOptions {
+
+  @Key("analytics_label")
+  private final String analyticsLabel;
+
+  private ApnsFcmOptions(Builder builder) {
+    FcmOptionsUtil.checkAnalyticsLabel(builder.analyticsLabel);
+    this.analyticsLabel = builder.analyticsLabel;
+  }
+
+  /**
+   * Creates a new {@link ApnsFcmOptions} with the specified analytics label.
+   *
+   * @param analyticsLabel An analytics label
+   * @return An ApnsFcmOptions with the analytics label set to the supplied value.
+   */
+  public static ApnsFcmOptions withAnalyticsLabel(String analyticsLabel) {
+    return builder().setAnalyticsLabel(analyticsLabel).build();
+  }
+
+  /**
+   * Creates a new {@link ApnsFcmOptions.Builder}.
+   *
+   * @return An {@link ApnsFcmOptions.Builder} instance.
+   */
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder {
+
+    private String analyticsLabel;
+
+    private Builder() {}
+
+    /**
+     * @param analyticsLabel A string representing the analytics label used for APNS messages.
+     * @return This builder
+     */
+    public Builder setAnalyticsLabel(String analyticsLabel) {
+      this.analyticsLabel = analyticsLabel;
+      return this;
+    }
+
+    /**
+     * Creates a new {@link ApnsFcmOptions} instance from the parameters set on this builder.
+     *
+     * @return A new {@link ApnsFcmOptions} instance.
+     * @throws IllegalArgumentException If any of the parameters set on the builder are invalid.
+     */
+    public ApnsFcmOptions build() {
+      return new ApnsFcmOptions(this);
+    }
+  }
+}
diff --git a/src/main/java/com/google/firebase/messaging/FcmOptions.java b/src/main/java/com/google/firebase/messaging/FcmOptions.java
new file mode 100644
index 0000000..996deb8
--- /dev/null
+++ b/src/main/java/com/google/firebase/messaging/FcmOptions.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.firebase.messaging;
+
+import com.google.api.client.util.Key;
+
+/**
+ * Represents the platform-independent FCM options that can be included in a {@link Message}.
+ * Instances of this class are thread-safe and immutable.
+ */
+public final class FcmOptions {
+
+  @Key("analytics_label")
+  private final String analyticsLabel;
+
+  private FcmOptions(Builder builder) {
+    FcmOptionsUtil.checkAnalyticsLabel(builder.analyticsLabel);
+    this.analyticsLabel = builder.analyticsLabel;
+  }
+
+  /**
+   * Creates a new {@link FcmOptions} with the specified analytics label.
+   *
+   * @param analyticsLabel An analytics label
+   * @return An FcmOptions with the analytics label set to the supplied value.
+   */
+  public static FcmOptions withAnalyticsLabel(String analyticsLabel) {
+    return new Builder().setAnalyticsLabel(analyticsLabel).build();
+  }
+
+  /**
+   * Creates a new {@link FcmOptions.Builder}.
+   *
+   * @return An {@link FcmOptions.Builder} instance.
+   */
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder {
+
+    private String analyticsLabel;
+
+    private Builder() {}
+
+    /**
+     * @param analyticsLabel A string representing the analytics label used for messages where no
+     *     platform-specific analytics label has been specified.
+     * @return This builder
+     */
+    public Builder setAnalyticsLabel(String analyticsLabel) {
+      this.analyticsLabel = analyticsLabel;
+      return this;
+    }
+
+    /**
+     * Creates a new {@link FcmOptions} instance from the parameters set on this builder.
+     *
+     * @return A new {@link FcmOptions} instance.
+     * @throws IllegalArgumentException If any of the parameters set on the builder are invalid.
+     */
+    public FcmOptions build() {
+      return new FcmOptions(this);
+    }
+  }
+}
diff --git a/src/main/java/com/google/firebase/messaging/FcmOptionsUtil.java b/src/main/java/com/google/firebase/messaging/FcmOptionsUtil.java
new file mode 100644
index 0000000..b16b2b8
--- /dev/null
+++ b/src/main/java/com/google/firebase/messaging/FcmOptionsUtil.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.firebase.messaging;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import java.util.regex.Pattern;
+
+final class FcmOptionsUtil {
+
+  /**
+   * Pattern matching a valid analytics labels.
+   */
+  private static final Pattern ANALYTICS_LABEL_REGEX = Pattern.compile("^[a-zA-Z0-9-_.~%]{0,50}$");
+
+  /**
+   * Returns false if the supplied {@code analyticsLabel} has a disallowed format.
+   */
+  private static boolean isValid(String analyticsLabel) {
+    return ANALYTICS_LABEL_REGEX.matcher(analyticsLabel).matches();
+  }
+
+  /**
+   * Validates the format of the supplied label.
+   *
+   * @throws IllegalArgumentException If the label is non-null and has a disallowed format.
+   */
+  static void checkAnalyticsLabel(String analyticsLabel) {
+    checkArgument(
+        analyticsLabel == null || isValid(analyticsLabel),
+        "Analytics label must have format matching'^[a-zA-Z0-9-_.~%]{1,50}$");
+  }
+}
diff --git a/src/main/java/com/google/firebase/messaging/Message.java b/src/main/java/com/google/firebase/messaging/Message.java
index 89a4a5c..c69abae 100644
--- a/src/main/java/com/google/firebase/messaging/Message.java
+++ b/src/main/java/com/google/firebase/messaging/Message.java
@@ -63,6 +63,9 @@ public class Message {
   @Key("condition")
   private final String condition;
 
+  @Key("fcm_options")
+  private final FcmOptions fcmOptions;
+
   private Message(Builder builder) {
     this.data = builder.data.isEmpty() ? null : ImmutableMap.copyOf(builder.data);
     this.notification = builder.notification;
@@ -78,6 +81,7 @@ public class Message {
     this.token = builder.token;
     this.topic = stripPrefix(builder.topic);
     this.condition = builder.condition;
+    this.fcmOptions = builder.fcmOptions;
   }
 
   @VisibleForTesting
@@ -120,6 +124,11 @@ public class Message {
     return condition;
   }
 
+  @VisibleForTesting
+  FcmOptions getFcmOptions() {
+    return fcmOptions;
+  }
+
   Map<String, Object> wrapForTransport(boolean dryRun) {
     ImmutableMap.Builder<String, Object> payload = ImmutableMap.<String, Object>builder()
         .put("message", this);
@@ -160,6 +169,7 @@ public class Message {
     private String token;
     private String topic;
     private String condition;
+    private FcmOptions fcmOptions;
 
     private Builder() {}
 
@@ -266,6 +276,15 @@ public class Message {
       return this;
     }
 
+    /**
+     * Sets the {@link FcmOptions}, which can be overridden by the platform-specific {@code
+     * fcm_options} fields.
+     */
+    public Builder setFcmOptions(FcmOptions fcmOptions) {
+      this.fcmOptions = fcmOptions;
+      return this;
+    }
+
     /**
      * Creates a new {@link Message} instance from the parameters set on this builder.
      *


Test patch:
diff --git a/src/test/java/com/google/firebase/messaging/MessageTest.java b/src/test/java/com/google/firebase/messaging/MessageTest.java
index 788127d..5f555fe 100644
--- a/src/test/java/com/google/firebase/messaging/MessageTest.java
+++ b/src/test/java/com/google/firebase/messaging/MessageTest.java
@@ -583,6 +583,101 @@ public class MessageTest {
     assertSame(message, wrappedMessage.get("message"));
   }
 
+  @Test
+  public void testMessageWithAllFcmOptions() throws IOException {
+    Message messageUsingShorthand = Message.builder()
+        .setTopic("foo")
+        .setFcmOptions(FcmOptions.withAnalyticsLabel("message-label"))
+        .setAndroidConfig(AndroidConfig.builder()
+            .setFcmOptions(AndroidFcmOptions.withAnalyticsLabel("android-label")).build())
+        .setApnsConfig(
+            ApnsConfig.builder().setAps(Aps.builder().build())
+                .setFcmOptions(ApnsFcmOptions.withAnalyticsLabel("apns-label"))
+                .build()).build();
+    Message messageUsingBuilder = Message.builder()
+        .setTopic("foo")
+        .setFcmOptions(FcmOptions.builder().setAnalyticsLabel("message-label").build())
+        .setAndroidConfig(AndroidConfig.builder()
+            .setFcmOptions(AndroidFcmOptions.builder().setAnalyticsLabel("android-label").build())
+            .build())
+        .setApnsConfig(
+            ApnsConfig.builder().setAps(Aps.builder().build())
+                .setFcmOptions(ApnsFcmOptions.builder().setAnalyticsLabel("apns-label").build())
+                .build()).build();
+
+    ImmutableMap<String, ImmutableMap<String, String>> androidConfig =
+        ImmutableMap.of("fcm_options", ImmutableMap.of("analytics_label", "android-label"));
+    ImmutableMap<String, Object> apnsConfig =
+        ImmutableMap.<String, Object>builder()
+            .put("fcm_options", ImmutableMap.of("analytics_label", "apns-label"))
+            .put("payload", ImmutableMap.of("aps", ImmutableMap.of()))
+            .build();
+    ImmutableMap<String, Object> expected =
+        ImmutableMap.<String, Object>builder()
+            .put("topic", "foo")
+            .put("fcm_options", ImmutableMap.of("analytics_label", "message-label"))
+            .put("android", androidConfig)
+            .put("apns", apnsConfig)
+            .build();
+    assertJsonEquals(expected, messageUsingBuilder);
+    assertJsonEquals(expected, messageUsingShorthand);
+  }
+
+  @Test
+  public void createMessageWithDefaultFcmOptions() throws IOException {
+    Message message = Message.builder()
+        .setTopic("foo")
+        .setFcmOptions(FcmOptions.builder().build())
+        .setAndroidConfig(
+            AndroidConfig.builder().setFcmOptions(AndroidFcmOptions.builder().build()).build())
+        .setApnsConfig(
+            ApnsConfig.builder()
+                .setAps(Aps.builder().build())
+                .setFcmOptions(ApnsFcmOptions.builder().build())
+                .build())
+        .build();
+
+    ImmutableMap<String, Object> apnsConfig =
+        ImmutableMap.<String, Object>builder()
+            .put("fcm_options", ImmutableMap.of())
+            .put("payload", ImmutableMap.of("aps", ImmutableMap.of()))
+            .build();
+    ImmutableMap<String, Object> expected =
+        ImmutableMap.<String, Object>builder()
+            .put("topic", "foo")
+            .put("fcm_options", ImmutableMap.of())
+            .put("android", ImmutableMap.of("fcm_options", ImmutableMap.of()))
+            .put("apns", apnsConfig)
+            .build();
+    assertJsonEquals(expected, message);
+  }
+
+  @Test
+  public void testIncorrectAnalyticsLabelFormat() {
+    try {
+      FcmOptions.builder().setAnalyticsLabel("!").build();
+      fail("No error thrown when using bad analytics label format.");
+    } catch (IllegalArgumentException expected) {
+      //expected
+    }
+
+    try {
+      FcmOptions.builder()
+          .setAnalyticsLabel("THIS_IS_LONGER_THAN_50_CHARACTERS_WHICH_IS_NOT_ALLOWED")
+          .build();
+      fail("No error thrown when using bad analytics label format.");
+    } catch (IllegalArgumentException expected) {
+      //expected
+    }
+
+    try {
+      FcmOptions.builder().setAnalyticsLabel("   ").build();
+      fail("No error thrown when using bad analytics label format.");
+    } catch (IllegalArgumentException expected) {
+      //expected
+    }
+  }
+
   private static void assertJsonEquals(
       Map expected, Object actual) throws IOException {
     assertEquals(expected, toMap(actual));