From 7249ab761a0de26a8b06e95461baf2b8fc3db25b Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Mon, 25 Dec 2017 15:20:13 -0800 Subject: [PATCH 01/32] Adding experimental FCM API --- .../firebase/messaging/AndroidConfig.java | 114 +++++++++++ .../messaging/AndroidNotification.java | 169 ++++++++++++++++ .../firebase/messaging/FirebaseMessaging.java | 125 ++++++++++++ .../google/firebase/messaging/Message.java | 103 ++++++++++ .../firebase/messaging/Notification.java | 18 ++ .../messaging/FirebaseMessagingIT.java | 25 +++ .../messaging/FirebaseMessagingTest.java | 96 +++++++++ .../firebase/messaging/MessageTest.java | 188 ++++++++++++++++++ 8 files changed, 838 insertions(+) create mode 100644 src/main/java/com/google/firebase/messaging/AndroidConfig.java create mode 100644 src/main/java/com/google/firebase/messaging/AndroidNotification.java create mode 100644 src/main/java/com/google/firebase/messaging/FirebaseMessaging.java create mode 100644 src/main/java/com/google/firebase/messaging/Message.java create mode 100644 src/main/java/com/google/firebase/messaging/Notification.java create mode 100644 src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java create mode 100644 src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java create mode 100644 src/test/java/com/google/firebase/messaging/MessageTest.java diff --git a/src/main/java/com/google/firebase/messaging/AndroidConfig.java b/src/main/java/com/google/firebase/messaging/AndroidConfig.java new file mode 100644 index 000000000..32d2dcdad --- /dev/null +++ b/src/main/java/com/google/firebase/messaging/AndroidConfig.java @@ -0,0 +1,114 @@ +package com.google.firebase.messaging; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.api.client.util.Key; +import com.google.common.collect.ImmutableMap; +import java.util.HashMap; +import java.util.Map; + +public class AndroidConfig { + + @Key("collapse_key") + private final String collapseKey; + + @Key("priority") + private final String priority; + + @Key("ttl") + private final String ttl; + + @Key("restricted_package_name") + private final String restrictedPackageName; + + @Key("data") + private final Map data; + + @Key("notification") + private final AndroidNotification notification; + + private AndroidConfig(Builder builder) { + this.collapseKey = builder.collapseKey; + if (builder.priority != null) { + this.priority = builder.priority.name(); + } else { + this.priority = null; + } + if (builder.ttl != null) { + checkArgument(builder.ttl.endsWith("s"), "ttl must end with 's'"); + String numeric = builder.ttl.substring(0, builder.ttl.length() - 1); + checkArgument(numeric.matches("[0-9.\\-]*"), "malformed ttl string"); + try { + checkArgument(Double.parseDouble(numeric) >= 0, "ttl must not be negative"); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("invalid ttl value", e); + } + } + this.ttl = builder.ttl; + this.restrictedPackageName = builder.restrictedPackageName; + this.data = builder.data.isEmpty() ? null : ImmutableMap.copyOf(builder.data); + this.notification = builder.notification; + } + + public enum Priority { + high, + normal, + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private String collapseKey; + private Priority priority; + private String ttl; + private String restrictedPackageName; + private final Map data = new HashMap<>(); + private AndroidNotification notification; + + private Builder() { + + } + + public Builder setCollapseKey(String collapseKey) { + this.collapseKey = collapseKey; + return this; + } + + public Builder setPriority(Priority priority) { + this.priority = priority; + return this; + } + + public Builder setTtl(String ttl) { + this.ttl = ttl; + return this; + } + + public Builder setRestrictedPackageName(String restrictedPackageName) { + this.restrictedPackageName = restrictedPackageName; + return this; + } + + public Builder putData(String key, String value) { + this.data.put(key, value); + return this; + } + + public Builder putAllData(Map data) { + this.data.putAll(data); + return this; + } + + public Builder setNotification(AndroidNotification notification) { + this.notification = notification; + return this; + } + + public AndroidConfig build() { + return new AndroidConfig(this); + } + } +} diff --git a/src/main/java/com/google/firebase/messaging/AndroidNotification.java b/src/main/java/com/google/firebase/messaging/AndroidNotification.java new file mode 100644 index 000000000..71794b176 --- /dev/null +++ b/src/main/java/com/google/firebase/messaging/AndroidNotification.java @@ -0,0 +1,169 @@ +package com.google.firebase.messaging; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.api.client.util.Key; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import java.util.ArrayList; +import java.util.List; + +public class AndroidNotification { + + @Key("title") + private final String title; + + @Key("body") + private final String body; + + @Key("icon") + private final String icon; + + @Key("color") + private final String color; + + @Key("sound") + private final String sound; + + @Key("tag") + private final String tag; + + @Key("click_action") + private final String clickAction; + + @Key("body_loc_key") + private final String bodyLocKey; + + @Key("body_loc_args") + private final List bodyLocArgs; + + @Key("title_loc_key") + private final String titleLocKey; + + @Key("title_loc_args") + private final List titleLocArgs; + + private AndroidNotification(Builder builder) { + this.title = builder.title; + this.body = builder.body; + this.icon = builder.icon; + if (builder.color != null) { + checkArgument(builder.color.matches("^#[0-9a-fA-F]{6}$"), + "color must be in the form #RRGGBB"); + } + this.color = builder.color; + this.sound = builder.sound; + this.tag = builder.tag; + this.clickAction = builder.clickAction; + this.bodyLocKey = builder.bodyLocKey; + if (!builder.bodyLocArgs.isEmpty()) { + checkArgument(!Strings.isNullOrEmpty(builder.bodyLocKey), + "bodyLocKey is required when specifying bodyLocArgs"); + this.bodyLocArgs = ImmutableList.copyOf(builder.bodyLocArgs); + } else { + this.bodyLocArgs = null; + } + + this.titleLocKey = builder.titleLocKey; + if (!builder.titleLocArgs.isEmpty()) { + checkArgument(!Strings.isNullOrEmpty(builder.titleLocKey), + "titleLocKey is required when specifying titleLocArgs"); + this.titleLocArgs = ImmutableList.copyOf(builder.titleLocArgs); + } else { + this.titleLocArgs = null; + } + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private String title; + private String body; + private String icon; + private String color; + private String sound; + private String tag; + private String clickAction; + private String bodyLocKey; + private List bodyLocArgs = new ArrayList<>(); + private String titleLocKey; + private List titleLocArgs = new ArrayList<>(); + + private Builder() { + } + + public Builder setTitle(String title) { + this.title = title; + return this; + } + + public Builder setBody(String body) { + this.body = body; + return this; + } + + public Builder setIcon(String icon) { + this.icon = icon; + return this; + } + + public Builder setColor(String color) { + this.color = color; + return this; + } + + public Builder setSound(String sound) { + this.sound = sound; + return this; + } + + public Builder setTag(String tag) { + this.tag = tag; + return this; + } + + public Builder setClickAction(String clickAction) { + this.clickAction = clickAction; + return this; + } + + public Builder setBodyLocKey(String bodyLocKey) { + this.bodyLocKey = bodyLocKey; + return this; + } + + public Builder addBodyLocArg(String arg) { + this.bodyLocArgs.add(arg); + return this; + } + + public Builder addAllBodyLocArgs(List args) { + this.bodyLocArgs.addAll(args); + return this; + } + + public Builder setTitleLocKey(String titleLocKey) { + this.titleLocKey = titleLocKey; + return this; + } + + public Builder addTitleLocArg(String arg) { + this.titleLocArgs.add(arg); + return this; + } + + public Builder addAllTitleLocArgs(List args) { + this.titleLocArgs.addAll(args); + return this; + } + + public AndroidNotification build() { + return new AndroidNotification(this); + } + + } + +} diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java new file mode 100644 index 000000000..fa970c038 --- /dev/null +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java @@ -0,0 +1,125 @@ +package com.google.firebase.messaging; + + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpRequestFactory; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.HttpResponseInterceptor; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.http.json.JsonHttpContent; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.JsonObjectParser; +import com.google.api.core.ApiFuture; +import com.google.auth.http.HttpCredentialsAdapter; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import com.google.firebase.FirebaseApp; +import com.google.firebase.ImplFirebaseTrampolines; +import com.google.firebase.iid.FirebaseInstanceId; +import com.google.firebase.internal.FirebaseService; +import com.google.firebase.internal.TaskToApiFuture; +import com.google.firebase.tasks.Task; +import java.util.Map; +import java.util.concurrent.Callable; + +public class FirebaseMessaging { + + private static final String FCM_URL = "https://fcm.googleapis.com/v1/projects/%s/messages:send"; + + private final FirebaseApp app; + private final HttpRequestFactory requestFactory; + private final JsonFactory jsonFactory; + private final String url; + + private HttpResponseInterceptor interceptor; + + private FirebaseMessaging(FirebaseApp app) { + HttpTransport httpTransport = app.getOptions().getHttpTransport(); + GoogleCredentials credentials = ImplFirebaseTrampolines.getCredentials(app); + this.app = app; + this.requestFactory = httpTransport.createRequestFactory( + new HttpCredentialsAdapter(credentials)); + this.jsonFactory = app.getOptions().getJsonFactory(); + String projectId = ImplFirebaseTrampolines.getProjectId(app); + checkArgument(!Strings.isNullOrEmpty(projectId), + "Project ID is required to access messaging service. Use a service account credential or " + + "set the project ID explicitly via FirebaseOptions. Alternatively you can also " + + "set the project ID via the GCLOUD_PROJECT environment variable."); + this.url = String.format(FCM_URL, projectId); + } + + public static FirebaseMessaging getInstance() { + return getInstance(FirebaseApp.getInstance()); + } + + /** + * Gets the {@link FirebaseInstanceId} instance for the specified {@link FirebaseApp}. + * + * @return The {@link FirebaseInstanceId} instance for the specified {@link FirebaseApp}. + */ + public static synchronized FirebaseMessaging getInstance(FirebaseApp app) { + FirebaseMessagingService service = ImplFirebaseTrampolines.getService(app, SERVICE_ID, + FirebaseMessagingService.class); + if (service == null) { + service = ImplFirebaseTrampolines.addService(app, new FirebaseMessagingService(app)); + } + return service.getInstance(); + } + + public ApiFuture sendAsync(Message message) { + return new TaskToApiFuture<>(send(message)); + } + + private Task send(final Message message) { + checkNotNull(message, "message must not be null"); + return ImplFirebaseTrampolines.submitCallable(app, new Callable() { + @Override + public String call() throws Exception { + HttpRequest request = requestFactory.buildPostRequest( + new GenericUrl(url), new JsonHttpContent(jsonFactory, + ImmutableMap.of("message", message))); + request.setParser(new JsonObjectParser(jsonFactory)); + request.setResponseInterceptor(interceptor); + HttpResponse response = null; + try { + response = request.execute(); + Map map = response.parseAs(Map.class); + return (String) map.get("name"); + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + if (response != null) { + response.disconnect(); + } + } + } + }); + } + + @VisibleForTesting + void setInterceptor(HttpResponseInterceptor interceptor) { + this.interceptor = interceptor; + } + + private static final String SERVICE_ID = FirebaseMessaging.class.getName(); + + private static class FirebaseMessagingService extends FirebaseService { + + FirebaseMessagingService(FirebaseApp app) { + super(SERVICE_ID, new FirebaseMessaging(app)); + } + + @Override + public void destroy() { + // NOTE: We don't explicitly tear down anything here, but public methods of StorageClient + // will now fail because calls to getOptions() and getToken() will hit FirebaseApp, + // which will throw once the app is deleted. + } + } +} diff --git a/src/main/java/com/google/firebase/messaging/Message.java b/src/main/java/com/google/firebase/messaging/Message.java new file mode 100644 index 000000000..6eb8da7b8 --- /dev/null +++ b/src/main/java/com/google/firebase/messaging/Message.java @@ -0,0 +1,103 @@ +package com.google.firebase.messaging; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.api.client.util.Key; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import com.google.common.primitives.Booleans; +import java.util.HashMap; +import java.util.Map; + +public class Message { + + @Key("data") + private Map data; + + @Key("notification") + private Notification notification; + + @Key("android") + private AndroidConfig androidConfig; + + @Key("token") + private String token; + + @Key("topic") + private String topic; + + @Key("condition") + private String condition; + + private Message(Builder builder) { + this.data = builder.data.isEmpty() ? null : ImmutableMap.copyOf(builder.data); + this.notification = builder.notification; + this.androidConfig = builder.androidConfig; + int count = Booleans.countTrue( + !Strings.isNullOrEmpty(builder.token), + !Strings.isNullOrEmpty(builder.topic), + !Strings.isNullOrEmpty(builder.condition) + ); + checkArgument(count == 1, "Exactly one of token, topic or condition must be specified"); + this.token = builder.token; + this.topic = builder.topic; + this.condition = builder.condition; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private final Map data = new HashMap<>(); + private Notification notification; + private AndroidConfig androidConfig; + private String token; + private String topic; + private String condition; + + private Builder() { + + } + + public Builder setNotification(Notification notification) { + this.notification = notification; + return this; + } + + public Builder setAndroidConfig(AndroidConfig androidConfig) { + this.androidConfig = androidConfig; + return this; + } + + public Builder setToken(String token) { + this.token = token; + return this; + } + + public Builder setTopic(String topic) { + this.topic = topic; + return this; + } + + public Builder setCondition(String condition) { + this.condition = condition; + return this; + } + + public Builder putData(String key, String value) { + this.data.put(key, value); + return this; + } + + public Builder putAllData(Map map) { + this.data.putAll(map); + return this; + } + + public Message build() { + return new Message(this); + } + } + +} diff --git a/src/main/java/com/google/firebase/messaging/Notification.java b/src/main/java/com/google/firebase/messaging/Notification.java new file mode 100644 index 000000000..5613a7994 --- /dev/null +++ b/src/main/java/com/google/firebase/messaging/Notification.java @@ -0,0 +1,18 @@ +package com.google.firebase.messaging; + +import com.google.api.client.util.Key; + +public class Notification { + + @Key("title") + private final String title; + + @Key("body") + private final String body; + + public Notification(String title, String body) { + this.title = title; + this.body = body; + } + +} diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java new file mode 100644 index 000000000..49c680214 --- /dev/null +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java @@ -0,0 +1,25 @@ +package com.google.firebase.messaging; + +import com.google.firebase.testing.IntegrationTestUtils; +import org.junit.BeforeClass; +import org.junit.Test; + +public class FirebaseMessagingIT { + + @BeforeClass + public static void setUpClass() throws Exception { + IntegrationTestUtils.ensureDefaultApp(); + } + + @Test + public void testSend() throws Exception { + FirebaseMessaging messaging = FirebaseMessaging.getInstance(); + + Message message = Message.builder() + .setNotification(new Notification("Title", "Body")) + .setTopic("foo-bar") + .build(); + String resp = messaging.sendAsync(message).get(); + System.out.println(resp); + } +} diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java new file mode 100644 index 000000000..763d1a840 --- /dev/null +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java @@ -0,0 +1,96 @@ +package com.google.firebase.messaging; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.fail; + +import com.google.api.client.http.HttpRequest; +import com.google.api.client.testing.http.MockHttpTransport; +import com.google.api.client.testing.http.MockLowLevelHttpResponse; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.TestOnlyImplFirebaseTrampolines; +import com.google.firebase.auth.MockGoogleCredentials; +import com.google.firebase.testing.TestResponseInterceptor; +import java.io.ByteArrayOutputStream; +import org.junit.After; +import org.junit.Test; + +public class FirebaseMessagingTest { + + @After + public void tearDown() { + TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + } + + @Test + public void testNoProjectId() { + FirebaseOptions options = new FirebaseOptions.Builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .build(); + FirebaseApp.initializeApp(options); + try { + FirebaseMessaging.getInstance(); + fail("No error thrown for missing project ID"); + } catch (IllegalArgumentException expected) { + // expected + } + } + + @Test + public void testNullMessage() throws Exception { + FirebaseOptions options = new FirebaseOptions.Builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setProjectId("test-project") + .build(); + FirebaseApp.initializeApp(options); + + FirebaseMessaging messaging = FirebaseMessaging.getInstance(); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + messaging.setInterceptor(interceptor); + try { + messaging.sendAsync(null); + fail("No error thrown for null message"); + } catch (NullPointerException expected) { + // expected + } + + assertNull(interceptor.getResponse()); + } + + @Test + public void testSend() throws Exception { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setContent("{\"name\": \"mock-name\"}"); + MockHttpTransport transport = new MockHttpTransport.Builder() + .setLowLevelHttpResponse(response) + .build(); + FirebaseOptions options = new FirebaseOptions.Builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setProjectId("test-project") + .setHttpTransport(transport) + .build(); + FirebaseApp app = FirebaseApp.initializeApp(options); + + FirebaseMessaging messaging = FirebaseMessaging.getInstance(); + assertSame(messaging, FirebaseMessaging.getInstance(app)); + + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + messaging.setInterceptor(interceptor); + String resp = messaging.sendAsync(Message.builder().setTopic("test-topic").build()).get(); + assertEquals("mock-name", resp); + + assertNotNull(interceptor.getResponse()); + HttpRequest request = interceptor.getResponse().getRequest(); + assertEquals("POST", request.getRequestMethod()); + String url = "https://fcm.googleapis.com/v1/projects/test-project/messages:send"; + assertEquals(url, request.getUrl().toString()); + assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + request.getContent().writeTo(out); + assertEquals("{\"message\":{\"topic\":\"test-topic\"}}", out.toString()); + } +} diff --git a/src/test/java/com/google/firebase/messaging/MessageTest.java b/src/test/java/com/google/firebase/messaging/MessageTest.java new file mode 100644 index 000000000..7bdb4adb6 --- /dev/null +++ b/src/test/java/com/google/firebase/messaging/MessageTest.java @@ -0,0 +1,188 @@ +package com.google.firebase.messaging; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import com.google.api.client.googleapis.util.Utils; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.JsonParser; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.Test; + +public class MessageTest { + + @Test(expected = IllegalArgumentException.class) + public void testMessageWithoutTarget() throws IOException { + Message.builder().build(); + } + + @Test + public void testEmptyMessage() throws IOException { + assertJsonEquals(ImmutableMap.of("topic", "test-topic"), + Message.builder().setTopic("test-topic").build()); + assertJsonEquals(ImmutableMap.of("condition", "'foo' in topics"), + Message.builder().setCondition("'foo' in topics").build()); + assertJsonEquals(ImmutableMap.of("token", "test-token"), + Message.builder().setToken("test-token").build()); + } + + @Test + public void testDataMessage() throws IOException { + Message message = Message.builder() + .putData("k1", "v1") + .putData("k2", "v2") + .putAllData(ImmutableMap.of("k3", "v3", "k4", "v4")) + .setTopic("test-topic") + .build(); + Map data = ImmutableMap.of("k1", "v1", "k2", "v2", "k3", "v3", "k4", "v4"); + assertJsonEquals(ImmutableMap.of("topic", "test-topic", "data", data), message); + } + + @Test + public void testNotificationMessage() throws IOException { + Message message = Message.builder() + .setNotification(new Notification("title", "body")) + .setTopic("test-topic") + .build(); + Map data = ImmutableMap.of("title", "title", "body", "body"); + assertJsonEquals(ImmutableMap.of("topic", "test-topic", "notification", data), message); + } + + @Test + public void testEmptyAndroidMessage() throws IOException { + Message message = Message.builder() + .setAndroidConfig(AndroidConfig.builder().build()) + .setTopic("test-topic") + .build(); + Map data = ImmutableMap.of(); + assertJsonEquals(ImmutableMap.of("topic", "test-topic", "android", data), message); + } + + @Test + public void testAndroidMessageWithoutNotification() throws IOException { + Message message = Message.builder() + .setAndroidConfig(AndroidConfig.builder() + .setCollapseKey("test-key") + .setPriority(AndroidConfig.Priority.high) + .setTtl("10s") + .setRestrictedPackageName("test-pkg-name") + .putData("k1", "v1") + .putAllData(ImmutableMap.of("k2", "v2", "k3", "v3")) + .build()) + .setTopic("test-topic") + .build(); + Map data = ImmutableMap.of( + "collapse_key", "test-key", + "priority", "high", + "ttl", "10s", + "restricted_package_name", "test-pkg-name", + "data", ImmutableMap.of("k1", "v1", "k2", "v2", "k3", "v3") + ); + assertJsonEquals(ImmutableMap.of("topic", "test-topic", "android", data), message); + } + + @Test + public void testAndroidMessageWithNotification() throws IOException { + Message message = Message.builder() + .setAndroidConfig(AndroidConfig.builder() + .setCollapseKey("test-key") + .setPriority(AndroidConfig.Priority.high) + .setTtl("10.001s") + .setRestrictedPackageName("test-pkg-name") + .setNotification(AndroidNotification.builder() + .setTitle("android-title") + .setBody("android-body") + .setIcon("android-icon") + .setSound("android-sound") + .setColor("#112233") + .setTag("android-tag") + .setTitleLocKey("title-loc") + .addTitleLocArg("title-arg1") + .addAllTitleLocArgs(ImmutableList.of("title-arg2", "title-arg3")) + .setBodyLocKey("body-loc") + .addBodyLocArg("body-arg1") + .addAllBodyLocArgs(ImmutableList.of("body-arg2", "body-arg3")) + .build()) + .build()) + .setTopic("test-topic") + .build(); + Map notification = ImmutableMap.builder() + .put("title", "android-title") + .put("body", "android-body") + .put("icon", "android-icon") + .put("sound", "android-sound") + .put("color", "#112233") + .put("tag", "android-tag") + .put("title_loc_key", "title-loc") + .put("title_loc_args", ImmutableList.of("title-arg1", "title-arg2", "title-arg3")) + .put("body_loc_key", "body-loc") + .put("body_loc_args", ImmutableList.of("body-arg1", "body-arg2", "body-arg3")) + .build(); + Map data = ImmutableMap.of( + "collapse_key", "test-key", + "priority", "high", + "ttl", "10.001s", + "restricted_package_name", "test-pkg-name", + "notification", notification + ); + assertJsonEquals(ImmutableMap.of("topic", "test-topic", "android", data), message); + } + + @Test + public void testInvalidAndroidConfig() throws IOException { + List configBuilders = ImmutableList.of( + AndroidConfig.builder().setTtl(""), + AndroidConfig.builder().setTtl("s"), + AndroidConfig.builder().setTtl("10"), + AndroidConfig.builder().setTtl("10e1s"), + AndroidConfig.builder().setTtl("1.2.3s"), + AndroidConfig.builder().setTtl("10 s"), + AndroidConfig.builder().setTtl("-10s") + ); + for (int i = 0; i < configBuilders.size(); i++) { + try { + configBuilders.get(i).build(); + fail("No error thrown for invalid config: " + i); + } catch (IllegalArgumentException expected) { + // expected + } + } + + List notificationBuilders = ImmutableList.of( + AndroidNotification.builder().setColor(""), + AndroidNotification.builder().setColor("foo"), + AndroidNotification.builder().setColor("123"), + AndroidNotification.builder().setColor("#AABBCK"), + AndroidNotification.builder().addBodyLocArg("foo"), + AndroidNotification.builder().addTitleLocArg("foo") + ); + for (int i = 0; i < notificationBuilders.size(); i++) { + try { + notificationBuilders.get(i).build(); + fail("No error thrown for invalid notification: " + i); + } catch (IllegalArgumentException expected) { + // expected + } + } + } + + private static void assertJsonEquals( + Map expected, Object actual) throws IOException { + assertEquals(expected, toMap(actual)); + } + + private static Map toMap(Object object) throws IOException { + JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); + String json = jsonFactory.toString(object); + JsonParser parser = jsonFactory.createJsonParser(json); + Map map = new HashMap<>(); + parser.parse(map); + return map; + } + +} From 12f7da831d1e5036a26d81368d43a636d6a41f2f Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Wed, 27 Dec 2017 16:46:05 -0800 Subject: [PATCH 02/32] Added Webpush notification support --- .../google/firebase/messaging/Message.java | 23 ++++-- .../firebase/messaging/WebpushConfig.java | 64 ++++++++++++++++ .../messaging/WebpushNotification.java | 25 ++++++ .../messaging/FirebaseMessagingIT.java | 4 + .../firebase/messaging/MessageTest.java | 76 +++++++++++++++++++ 5 files changed, 186 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/google/firebase/messaging/WebpushConfig.java create mode 100644 src/main/java/com/google/firebase/messaging/WebpushNotification.java diff --git a/src/main/java/com/google/firebase/messaging/Message.java b/src/main/java/com/google/firebase/messaging/Message.java index 6eb8da7b8..cec1bcf10 100644 --- a/src/main/java/com/google/firebase/messaging/Message.java +++ b/src/main/java/com/google/firebase/messaging/Message.java @@ -12,27 +12,31 @@ public class Message { @Key("data") - private Map data; + private final Map data; @Key("notification") - private Notification notification; + private final Notification notification; @Key("android") - private AndroidConfig androidConfig; + private final AndroidConfig androidConfig; + + @Key("webpush") + private final WebpushConfig webpushConfig; @Key("token") - private String token; + private final String token; @Key("topic") - private String topic; + private final String topic; @Key("condition") - private String condition; + private final String condition; private Message(Builder builder) { this.data = builder.data.isEmpty() ? null : ImmutableMap.copyOf(builder.data); this.notification = builder.notification; this.androidConfig = builder.androidConfig; + this.webpushConfig = builder.webpushConfig; int count = Booleans.countTrue( !Strings.isNullOrEmpty(builder.token), !Strings.isNullOrEmpty(builder.topic), @@ -49,9 +53,11 @@ public static Builder builder() { } public static class Builder { + private final Map data = new HashMap<>(); private Notification notification; private AndroidConfig androidConfig; + private WebpushConfig webpushConfig; private String token; private String topic; private String condition; @@ -70,6 +76,11 @@ public Builder setAndroidConfig(AndroidConfig androidConfig) { return this; } + public Builder setWebpushConfig(WebpushConfig webpushConfig) { + this.webpushConfig = webpushConfig; + return this; + } + public Builder setToken(String token) { this.token = token; return this; diff --git a/src/main/java/com/google/firebase/messaging/WebpushConfig.java b/src/main/java/com/google/firebase/messaging/WebpushConfig.java new file mode 100644 index 000000000..6d6d7e408 --- /dev/null +++ b/src/main/java/com/google/firebase/messaging/WebpushConfig.java @@ -0,0 +1,64 @@ +package com.google.firebase.messaging; + +import com.google.api.client.util.Key; +import com.google.common.collect.ImmutableMap; +import java.util.HashMap; +import java.util.Map; + +public class WebpushConfig { + + @Key("headers") + private final Map headers; + + @Key("data") + private final Map data; + + @Key("notification") + private final WebpushNotification notification; + + private WebpushConfig(Builder builder) { + this.headers = builder.headers.isEmpty() ? null : ImmutableMap.copyOf(builder.headers); + this.data = builder.data.isEmpty() ? null : ImmutableMap.copyOf(builder.data); + this.notification = builder.notification; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private final Map headers = new HashMap<>(); + private final Map data = new HashMap<>(); + private WebpushNotification notification; + + public Builder putHeader(String key, String value) { + headers.put(key, value); + return this; + } + + public Builder putAllHeaders(Map map) { + headers.putAll(map); + return this; + } + + public Builder putData(String key, String value) { + data.put(key, value); + return this; + } + + public Builder putAllData(Map map) { + data.putAll(map); + return this; + } + + public Builder setNotification(WebpushNotification notification) { + this.notification = notification; + return this; + } + + public WebpushConfig build() { + return new WebpushConfig(this); + } + } +} diff --git a/src/main/java/com/google/firebase/messaging/WebpushNotification.java b/src/main/java/com/google/firebase/messaging/WebpushNotification.java new file mode 100644 index 000000000..c1ff4197f --- /dev/null +++ b/src/main/java/com/google/firebase/messaging/WebpushNotification.java @@ -0,0 +1,25 @@ +package com.google.firebase.messaging; + +import com.google.api.client.util.Key; + +public class WebpushNotification { + + @Key("title") + private final String title; + + @Key("body") + private final String body; + + @Key("icon") + private final String icon; + + public WebpushNotification(String title, String body) { + this(title, body, null); + } + + public WebpushNotification(String title, String body, String icon) { + this.title = title; + this.body = body; + this.icon = icon; + } +} diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java index 49c680214..e5244d997 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java @@ -17,6 +17,10 @@ public void testSend() throws Exception { Message message = Message.builder() .setNotification(new Notification("Title", "Body")) + .setAndroidConfig(AndroidConfig.builder() + .setRestrictedPackageName("com.demoapps.hkj") + .build()) + .setWebpushConfig(WebpushConfig.builder().putHeader("X-Custom-Val", "Foo").build()) .setTopic("foo-bar") .build(); String resp = messaging.sendAsync(message).get(); diff --git a/src/test/java/com/google/firebase/messaging/MessageTest.java b/src/test/java/com/google/firebase/messaging/MessageTest.java index 7bdb4adb6..dc51c0909 100644 --- a/src/test/java/com/google/firebase/messaging/MessageTest.java +++ b/src/test/java/com/google/firebase/messaging/MessageTest.java @@ -171,6 +171,82 @@ public void testInvalidAndroidConfig() throws IOException { } } + @Test + public void testEmptyWebpushMessage() throws IOException { + Message message = Message.builder() + .setWebpushConfig(WebpushConfig.builder().build()) + .setTopic("test-topic") + .build(); + Map data = ImmutableMap.of(); + assertJsonEquals(ImmutableMap.of("topic", "test-topic", "webpush", data), message); + } + + @Test + public void testWebpushMessageWithoutNotification() throws IOException { + Message message = Message.builder() + .setWebpushConfig(WebpushConfig.builder() + .putHeader("k1", "v1") + .putAllHeaders(ImmutableMap.of("k2", "v2", "k3", "v3")) + .putData("k1", "v1") + .putAllData(ImmutableMap.of("k2", "v2", "k3", "v3")) + .build()) + .setTopic("test-topic") + .build(); + Map data = ImmutableMap.of( + "headers", ImmutableMap.of("k1", "v1", "k2", "v2", "k3", "v3"), + "data", ImmutableMap.of("k1", "v1", "k2", "v2", "k3", "v3") + ); + assertJsonEquals(ImmutableMap.of("topic", "test-topic", "webpush", data), message); + } + + @Test + public void testWebpushMessageWithNotification() throws IOException { + Message message = Message.builder() + .setWebpushConfig(WebpushConfig.builder() + .putHeader("k1", "v1") + .putAllHeaders(ImmutableMap.of("k2", "v2", "k3", "v3")) + .putData("k1", "v1") + .putAllData(ImmutableMap.of("k2", "v2", "k3", "v3")) + .setNotification(new WebpushNotification( + "webpush-title", "webpush-body", "webpush-icon")) + .build()) + .setTopic("test-topic") + .build(); + Map notification = ImmutableMap.builder() + .put("title", "webpush-title") + .put("body", "webpush-body") + .put("icon", "webpush-icon") + .build(); + Map data = ImmutableMap.of( + "headers", ImmutableMap.of("k1", "v1", "k2", "v2", "k3", "v3"), + "data", ImmutableMap.of("k1", "v1", "k2", "v2", "k3", "v3"), + "notification", notification + ); + assertJsonEquals(ImmutableMap.of("topic", "test-topic", "webpush", data), message); + + // Test notification without icon + message = Message.builder() + .setWebpushConfig(WebpushConfig.builder() + .putHeader("k1", "v1") + .putAllHeaders(ImmutableMap.of("k2", "v2", "k3", "v3")) + .putData("k1", "v1") + .putAllData(ImmutableMap.of("k2", "v2", "k3", "v3")) + .setNotification(new WebpushNotification("webpush-title", "webpush-body")) + .build()) + .setTopic("test-topic") + .build(); + notification = ImmutableMap.builder() + .put("title", "webpush-title") + .put("body", "webpush-body") + .build(); + data = ImmutableMap.of( + "headers", ImmutableMap.of("k1", "v1", "k2", "v2", "k3", "v3"), + "data", ImmutableMap.of("k1", "v1", "k2", "v2", "k3", "v3"), + "notification", notification + ); + assertJsonEquals(ImmutableMap.of("topic", "test-topic", "webpush", data), message); + } + private static void assertJsonEquals( Map expected, Object actual) throws IOException { assertEquals(expected, toMap(actual)); From f095734cdbdc35eb3c5b486a3438cfa4f01c97f8 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Wed, 27 Dec 2017 17:13:20 -0800 Subject: [PATCH 03/32] Added APNS support and tests --- .../google/firebase/messaging/ApnsConfig.java | 50 ++++++++ .../firebase/messaging/FirebaseMessaging.java | 2 +- .../messaging/FirebaseMessagingException.java | 10 ++ .../google/firebase/messaging/Message.java | 10 ++ .../messaging/FirebaseMessagingTest.java | 47 +++++++- .../firebase/messaging/MessageTest.java | 108 ++++++++++++++++++ 6 files changed, 224 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/google/firebase/messaging/ApnsConfig.java create mode 100644 src/main/java/com/google/firebase/messaging/FirebaseMessagingException.java diff --git a/src/main/java/com/google/firebase/messaging/ApnsConfig.java b/src/main/java/com/google/firebase/messaging/ApnsConfig.java new file mode 100644 index 000000000..21cfd78be --- /dev/null +++ b/src/main/java/com/google/firebase/messaging/ApnsConfig.java @@ -0,0 +1,50 @@ +package com.google.firebase.messaging; + +import com.google.api.client.util.Key; +import com.google.common.collect.ImmutableMap; +import java.util.HashMap; +import java.util.Map; + +public class ApnsConfig { + + @Key("headers") + private final Map headers; + + @Key("payload") + private final Map payload; + + private ApnsConfig(Builder builder) { + this.headers = builder.headers.isEmpty() ? null : ImmutableMap.copyOf(builder.headers); + this.payload = builder.payload == null || builder.payload.isEmpty() + ? null : ImmutableMap.copyOf(builder.payload); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private final Map headers = new HashMap<>(); + private Map payload; + + public Builder putHeader(String key, String value) { + headers.put(key, value); + return this; + } + + public Builder putAllHeaders(Map map) { + headers.putAll(map); + return this; + } + + public Builder setPayload(Map payload) { + this.payload = payload; + return this; + } + + public ApnsConfig build() { + return new ApnsConfig(this); + } + } +} diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java index fa970c038..75a50c21e 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java @@ -92,7 +92,7 @@ public String call() throws Exception { Map map = response.parseAs(Map.class); return (String) map.get("name"); } catch (Exception e) { - throw new RuntimeException(e); + throw new FirebaseMessagingException("Error while calling FCM service", e); } finally { if (response != null) { response.disconnect(); diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessagingException.java b/src/main/java/com/google/firebase/messaging/FirebaseMessagingException.java new file mode 100644 index 000000000..3d7ab0cbf --- /dev/null +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessagingException.java @@ -0,0 +1,10 @@ +package com.google.firebase.messaging; + +import com.google.firebase.FirebaseException; + +public class FirebaseMessagingException extends FirebaseException { + + FirebaseMessagingException(String detailMessage, Throwable cause) { + super(detailMessage, cause); + } +} diff --git a/src/main/java/com/google/firebase/messaging/Message.java b/src/main/java/com/google/firebase/messaging/Message.java index cec1bcf10..c2d61b8ef 100644 --- a/src/main/java/com/google/firebase/messaging/Message.java +++ b/src/main/java/com/google/firebase/messaging/Message.java @@ -23,6 +23,9 @@ public class Message { @Key("webpush") private final WebpushConfig webpushConfig; + @Key("apns") + private final ApnsConfig apnsConfig; + @Key("token") private final String token; @@ -37,6 +40,7 @@ private Message(Builder builder) { this.notification = builder.notification; this.androidConfig = builder.androidConfig; this.webpushConfig = builder.webpushConfig; + this.apnsConfig = builder.apnsConfig; int count = Booleans.countTrue( !Strings.isNullOrEmpty(builder.token), !Strings.isNullOrEmpty(builder.topic), @@ -58,6 +62,7 @@ public static class Builder { private Notification notification; private AndroidConfig androidConfig; private WebpushConfig webpushConfig; + private ApnsConfig apnsConfig; private String token; private String topic; private String condition; @@ -81,6 +86,11 @@ public Builder setWebpushConfig(WebpushConfig webpushConfig) { return this; } + public Builder setApnsConfig(ApnsConfig apnsConfig) { + this.apnsConfig = apnsConfig; + return this; + } + public Builder setToken(String token) { this.token = token; return this; diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java index 763d1a840..2f097d295 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java @@ -4,22 +4,29 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import com.google.api.client.http.HttpRequest; import com.google.api.client.testing.http.MockHttpTransport; import com.google.api.client.testing.http.MockLowLevelHttpResponse; +import com.google.common.collect.ImmutableList; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.auth.MockGoogleCredentials; import com.google.firebase.testing.TestResponseInterceptor; import java.io.ByteArrayOutputStream; +import java.util.List; +import java.util.concurrent.ExecutionException; import org.junit.After; import org.junit.Test; public class FirebaseMessagingTest { + private static final String TEST_URL = + "https://fcm.googleapis.com/v1/projects/test-project/messages:send"; + @After public void tearDown() { TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); @@ -85,12 +92,48 @@ public void testSend() throws Exception { assertNotNull(interceptor.getResponse()); HttpRequest request = interceptor.getResponse().getRequest(); assertEquals("POST", request.getRequestMethod()); - String url = "https://fcm.googleapis.com/v1/projects/test-project/messages:send"; - assertEquals(url, request.getUrl().toString()); + assertEquals(TEST_URL, request.getUrl().toString()); assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); ByteArrayOutputStream out = new ByteArrayOutputStream(); request.getContent().writeTo(out); assertEquals("{\"message\":{\"topic\":\"test-topic\"}}", out.toString()); } + + @Test + public void testSendError() throws Exception { + List errors = ImmutableList.of(401, 404, 500); + + for (int statusCode : errors) { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setStatusCode(statusCode) + .setContent("test error"); + MockHttpTransport transport = new MockHttpTransport.Builder() + .setLowLevelHttpResponse(response) + .build(); + FirebaseOptions options = new FirebaseOptions.Builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setProjectId("test-project") + .setHttpTransport(transport) + .build(); + final FirebaseApp app = FirebaseApp.initializeApp(options); + + FirebaseMessaging messaging = FirebaseMessaging.getInstance(); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + messaging.setInterceptor(interceptor); + try { + messaging.sendAsync(Message.builder().setTopic("test-topic").build()).get(); + fail("No error thrown for HTTP error"); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseMessagingException); + } + + assertNotNull(interceptor.getResponse()); + HttpRequest request = interceptor.getResponse().getRequest(); + assertEquals("POST", request.getRequestMethod()); + assertEquals(TEST_URL, request.getUrl().toString()); + assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); + app.delete(); + } + } } diff --git a/src/test/java/com/google/firebase/messaging/MessageTest.java b/src/test/java/com/google/firebase/messaging/MessageTest.java index dc51c0909..401e0cf02 100644 --- a/src/test/java/com/google/firebase/messaging/MessageTest.java +++ b/src/test/java/com/google/firebase/messaging/MessageTest.java @@ -101,6 +101,7 @@ public void testAndroidMessageWithNotification() throws IOException { .setSound("android-sound") .setColor("#112233") .setTag("android-tag") + .setClickAction("android-click") .setTitleLocKey("title-loc") .addTitleLocArg("title-arg1") .addAllTitleLocArgs(ImmutableList.of("title-arg2", "title-arg3")) @@ -118,6 +119,7 @@ public void testAndroidMessageWithNotification() throws IOException { .put("sound", "android-sound") .put("color", "#112233") .put("tag", "android-tag") + .put("click_action", "android-click") .put("title_loc_key", "title-loc") .put("title_loc_args", ImmutableList.of("title-arg1", "title-arg2", "title-arg3")) .put("body_loc_key", "body-loc") @@ -133,6 +135,45 @@ public void testAndroidMessageWithNotification() throws IOException { assertJsonEquals(ImmutableMap.of("topic", "test-topic", "android", data), message); } + @Test + public void testAndroidMessageWithoutLocalization() throws IOException { + Message message = Message.builder() + .setAndroidConfig(AndroidConfig.builder() + .setCollapseKey("test-key") + .setPriority(AndroidConfig.Priority.high) + .setTtl("10.001s") + .setRestrictedPackageName("test-pkg-name") + .setNotification(AndroidNotification.builder() + .setTitle("android-title") + .setBody("android-body") + .setIcon("android-icon") + .setSound("android-sound") + .setColor("#112233") + .setTag("android-tag") + .setClickAction("android-click") + .build()) + .build()) + .setTopic("test-topic") + .build(); + Map notification = ImmutableMap.builder() + .put("title", "android-title") + .put("body", "android-body") + .put("icon", "android-icon") + .put("sound", "android-sound") + .put("color", "#112233") + .put("tag", "android-tag") + .put("click_action", "android-click") + .build(); + Map data = ImmutableMap.of( + "collapse_key", "test-key", + "priority", "high", + "ttl", "10.001s", + "restricted_package_name", "test-pkg-name", + "notification", notification + ); + assertJsonEquals(ImmutableMap.of("topic", "test-topic", "android", data), message); + } + @Test public void testInvalidAndroidConfig() throws IOException { List configBuilders = ImmutableList.of( @@ -247,6 +288,73 @@ public void testWebpushMessageWithNotification() throws IOException { assertJsonEquals(ImmutableMap.of("topic", "test-topic", "webpush", data), message); } + @Test + public void testEmptyApnsMessage() throws IOException { + Message message = Message.builder() + .setApnsConfig(ApnsConfig.builder().build()) + .setTopic("test-topic") + .build(); + Map data = ImmutableMap.of(); + assertJsonEquals(ImmutableMap.of("topic", "test-topic", "apns", data), message); + } + + @Test + public void testApnsMessageWithoutPayload() throws IOException { + Message message = Message.builder() + .setApnsConfig(ApnsConfig.builder() + .putHeader("k1", "v1") + .putAllHeaders(ImmutableMap.of("k2", "v2", "k3", "v3")) + .build()) + .setTopic("test-topic") + .build(); + Map data = ImmutableMap.of( + "headers", ImmutableMap.of("k1", "v1", "k2", "v2", "k3", "v3") + ); + assertJsonEquals(ImmutableMap.of("topic", "test-topic", "apns", data), message); + + message = Message.builder() + .setApnsConfig(ApnsConfig.builder() + .putHeader("k1", "v1") + .putAllHeaders(ImmutableMap.of("k2", "v2", "k3", "v3")) + .setPayload(null) + .build()) + .setTopic("test-topic") + .build(); + assertJsonEquals(ImmutableMap.of("topic", "test-topic", "apns", data), message); + + message = Message.builder() + .setApnsConfig(ApnsConfig.builder() + .putHeader("k1", "v1") + .putAllHeaders(ImmutableMap.of("k2", "v2", "k3", "v3")) + .setPayload(ImmutableMap.of()) + .build()) + .setTopic("test-topic") + .build(); + assertJsonEquals(ImmutableMap.of("topic", "test-topic", "apns", data), message); + } + + @Test + public void testApnsMessageWithPayload() throws IOException { + Map payload = ImmutableMap.builder() + .put("k1", "v1") + .put("k2", true) + .put("k3", ImmutableMap.of("k4", "v4")) + .build(); + Message message = Message.builder() + .setApnsConfig(ApnsConfig.builder() + .putHeader("k1", "v1") + .putAllHeaders(ImmutableMap.of("k2", "v2", "k3", "v3")) + .setPayload(payload) + .build()) + .setTopic("test-topic") + .build(); + Map data = ImmutableMap.of( + "headers", ImmutableMap.of("k1", "v1", "k2", "v2", "k3", "v3"), + "payload", payload + ); + assertJsonEquals(ImmutableMap.of("topic", "test-topic", "apns", data), message); + } + private static void assertJsonEquals( Map expected, Object actual) throws IOException { assertEquals(expected, toMap(actual)); From c0377332df2580fd7033f1e8ec00d1e26e5eaa50 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Wed, 27 Dec 2017 19:49:21 -0800 Subject: [PATCH 04/32] Added subscribe method --- .../firebase/messaging/FirebaseMessaging.java | 108 ++++++++++-- .../messaging/TopicManagementResult.java | 22 +++ .../messaging/FirebaseMessagingIT.java | 16 ++ .../messaging/FirebaseMessagingTest.java | 157 +++++++++++++++--- 4 files changed, 261 insertions(+), 42 deletions(-) create mode 100644 src/main/java/com/google/firebase/messaging/TopicManagementResult.java diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java index 75a50c21e..389cb8916 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java @@ -1,6 +1,5 @@ package com.google.firebase.messaging; - import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; @@ -18,6 +17,7 @@ import com.google.auth.oauth2.GoogleCredentials; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.firebase.FirebaseApp; import com.google.firebase.ImplFirebaseTrampolines; @@ -25,6 +25,8 @@ import com.google.firebase.internal.FirebaseService; import com.google.firebase.internal.TaskToApiFuture; import com.google.firebase.tasks.Task; +import java.io.IOException; +import java.util.List; import java.util.Map; import java.util.concurrent.Callable; @@ -32,6 +34,9 @@ public class FirebaseMessaging { private static final String FCM_URL = "https://fcm.googleapis.com/v1/projects/%s/messages:send"; + private static final String IID_HOST = "https://iid.googleapis.com"; + private static final String IID_SUBSCRIBE_PATH = "iid/v1:batchAdd"; + private final FirebaseApp app; private final HttpRequestFactory requestFactory; private final JsonFactory jsonFactory; @@ -80,28 +85,101 @@ private Task send(final Message message) { checkNotNull(message, "message must not be null"); return ImplFirebaseTrampolines.submitCallable(app, new Callable() { @Override - public String call() throws Exception { - HttpRequest request = requestFactory.buildPostRequest( - new GenericUrl(url), new JsonHttpContent(jsonFactory, - ImmutableMap.of("message", message))); - request.setParser(new JsonObjectParser(jsonFactory)); - request.setResponseInterceptor(interceptor); - HttpResponse response = null; + public String call() throws FirebaseMessagingException { try { - response = request.execute(); - Map map = response.parseAs(Map.class); - return (String) map.get("name"); + return makeSendRequest(message); } catch (Exception e) { throw new FirebaseMessagingException("Error while calling FCM service", e); - } finally { - if (response != null) { - response.disconnect(); - } } } }); } + public ApiFuture> subscribeToTopicAsync( + List registrationTokens, String topic) { + return new TaskToApiFuture<>(subscribeToTopic(registrationTokens, topic)); + } + + private Task> subscribeToTopic( + final List registrationTokens, final String topic) { + checkRegistrationTokens(registrationTokens); + checkTopic(topic); + + return ImplFirebaseTrampolines.submitCallable(app, new Callable>() { + @Override + public List call() throws FirebaseMessagingException { + try { + return makeTopicManagementRequest(registrationTokens, topic, IID_SUBSCRIBE_PATH); + } catch (IOException e) { + throw new FirebaseMessagingException("Error while calling IID service", e); + } + } + }); + } + + private String makeSendRequest(Message message) throws IOException { + HttpRequest request = requestFactory.buildPostRequest( + new GenericUrl(url), new JsonHttpContent(jsonFactory, ImmutableMap.of("message", message))); + request.setParser(new JsonObjectParser(jsonFactory)); + request.setResponseInterceptor(interceptor); + HttpResponse response = null; + try { + response = request.execute(); + Map map = response.parseAs(Map.class); + return (String) map.get("name"); + } finally { + if (response != null) { + response.disconnect(); + } + } + } + + private List makeTopicManagementRequest( + List registrationTokens, String topic, String path) throws IOException { + Map payload = ImmutableMap.of( + "to", topic, + "registration_tokens", registrationTokens + ); + + final String url = String.format("%s/%s", IID_HOST, path); + HttpRequest request = requestFactory.buildPostRequest( + new GenericUrl(url), new JsonHttpContent(jsonFactory, payload)); + request.getHeaders().set("access_token_auth", "true"); + request.setParser(new JsonObjectParser(jsonFactory)); + request.setResponseInterceptor(interceptor); + HttpResponse response = null; + try { + response = request.execute(); + Map parsed = response.parseAs(Map.class); + List> results = (List>) parsed.get("results"); + ImmutableList.Builder builder = ImmutableList.builder(); + for (Map map : results) { + builder.add(new TopicManagementResult(map)); + } + return builder.build(); + } finally { + if (response != null) { + response.disconnect(); + } + } + } + + private static void checkRegistrationTokens(List registrationTokens) { + checkArgument(registrationTokens != null && !registrationTokens.isEmpty(), + "registrationTokens list must not be null or empty"); + checkArgument(registrationTokens.size() <= 1000, + "registrationTokens list must not contain more than 1000 elements"); + for (String token : registrationTokens) { + checkArgument(!Strings.isNullOrEmpty(token), + "registration tokens list must not contain null or empty strings"); + } + } + + private static void checkTopic(String topic) { + checkArgument(!Strings.isNullOrEmpty(topic), "topic must not be null or empty"); + checkArgument(topic.matches("^(/topics/)?(private/)?[a-zA-Z0-9-_.~%]+$"), "invalid topic name"); + } + @VisibleForTesting void setInterceptor(HttpResponseInterceptor interceptor) { this.interceptor = interceptor; diff --git a/src/main/java/com/google/firebase/messaging/TopicManagementResult.java b/src/main/java/com/google/firebase/messaging/TopicManagementResult.java new file mode 100644 index 000000000..d7e5bfffc --- /dev/null +++ b/src/main/java/com/google/firebase/messaging/TopicManagementResult.java @@ -0,0 +1,22 @@ +package com.google.firebase.messaging; + +import java.util.Map; + +public class TopicManagementResult { + + private final boolean success; + private final String reason; + + TopicManagementResult(Map response) { + this.success = response.isEmpty(); + this.reason = (String) response.get("error"); + } + + public boolean isSuccess() { + return success; + } + + public String getReason() { + return reason; + } +} diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java index e5244d997..092ef88b7 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java @@ -1,11 +1,19 @@ package com.google.firebase.messaging; +import static org.junit.Assert.assertEquals; + +import com.google.common.collect.ImmutableList; import com.google.firebase.testing.IntegrationTestUtils; +import java.util.List; import org.junit.BeforeClass; import org.junit.Test; public class FirebaseMessagingIT { + private static final String TEST_REGISTRATION_TOKEN = + "fGw0qy4TGgk:APA91bGtWGjuhp4WRhHXgbabIYp1jxEKI08ofj_v1bKhWAGJQ4e3arRCWzeTfHaLz83mBnDh0a" + + "PWB1AykXAVUUGl2h1wT4XI6XazWpvY7RBUSYfoxtqSWGIm2nvWh2BOP1YG501SsRoE"; + @BeforeClass public static void setUpClass() throws Exception { IntegrationTestUtils.ensureDefaultApp(); @@ -26,4 +34,12 @@ public void testSend() throws Exception { String resp = messaging.sendAsync(message).get(); System.out.println(resp); } + + @Test + public void testSubscribe() throws Exception { + FirebaseMessaging messaging = FirebaseMessaging.getInstance(); + List results = messaging.subscribeToTopicAsync( + ImmutableList.of(TEST_REGISTRATION_TOKEN), "/topics/mock-topic").get(); + assertEquals(1, results.size()); + } } diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java index 2f097d295..5ff85ccb5 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java @@ -1,6 +1,7 @@ package com.google.firebase.messaging; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; @@ -8,6 +9,7 @@ import static org.junit.Assert.fail; import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpResponseException; import com.google.api.client.testing.http.MockHttpTransport; import com.google.api.client.testing.http.MockLowLevelHttpResponse; import com.google.common.collect.ImmutableList; @@ -24,8 +26,10 @@ public class FirebaseMessagingTest { - private static final String TEST_URL = + private static final String TEST_FCM_URL = "https://fcm.googleapis.com/v1/projects/test-project/messages:send"; + private static final String TEST_IID_URL = + "https://iid.googleapis.com/iid/v1:batchAdd"; @After public void tearDown() { @@ -71,69 +75,168 @@ public void testNullMessage() throws Exception { public void testSend() throws Exception { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() .setContent("{\"name\": \"mock-name\"}"); - MockHttpTransport transport = new MockHttpTransport.Builder() - .setLowLevelHttpResponse(response) - .build(); + FirebaseMessaging messaging = initMessaging(response); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + messaging.setInterceptor(interceptor); + String resp = messaging.sendAsync(Message.builder().setTopic("test-topic").build()).get(); + assertEquals("mock-name", resp); + + assertNotNull(interceptor.getResponse()); + HttpRequest request = interceptor.getResponse().getRequest(); + assertEquals("POST", request.getRequestMethod()); + assertEquals(TEST_FCM_URL, request.getUrl().toString()); + assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + request.getContent().writeTo(out); + assertEquals("{\"message\":{\"topic\":\"test-topic\"}}", out.toString()); + } + + @Test + public void testSendError() throws Exception { + List errors = ImmutableList.of(401, 404, 500); + + for (int statusCode : errors) { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setStatusCode(statusCode) + .setContent("test error"); + FirebaseMessaging messaging = initMessaging(response); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + messaging.setInterceptor(interceptor); + try { + messaging.sendAsync(Message.builder().setTopic("test-topic").build()).get(); + fail("No error thrown for HTTP error"); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseMessagingException); + assertTrue(e.getCause().getCause() instanceof HttpResponseException); + } + + assertNotNull(interceptor.getResponse()); + HttpRequest request = interceptor.getResponse().getRequest(); + assertEquals("POST", request.getRequestMethod()); + assertEquals(TEST_FCM_URL, request.getUrl().toString()); + assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); + FirebaseApp.getInstance().delete(); + } + } + + @Test + public void testInvalidSubscribe() { FirebaseOptions options = new FirebaseOptions.Builder() .setCredentials(new MockGoogleCredentials("test-token")) .setProjectId("test-project") - .setHttpTransport(transport) .build(); - FirebaseApp app = FirebaseApp.initializeApp(options); - + FirebaseApp.initializeApp(options); FirebaseMessaging messaging = FirebaseMessaging.getInstance(); - assertSame(messaging, FirebaseMessaging.getInstance(app)); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + messaging.setInterceptor(interceptor); + ImmutableList.Builder tooManyIds = ImmutableList.builder(); + for (int i = 0; i < 1001; i++) { + tooManyIds.add("id" + i); + } + List invalidArgs = ImmutableList.of( + new TopicMgtArgs(null, null), + new TopicMgtArgs(null, "test-topic"), + new TopicMgtArgs(ImmutableList.of(), "test-topic"), + new TopicMgtArgs(ImmutableList.of(""), "test-topic"), + new TopicMgtArgs(tooManyIds.build(), "test-topic"), + new TopicMgtArgs(ImmutableList.of(""), null), + new TopicMgtArgs(ImmutableList.of("id"), ""), + new TopicMgtArgs(ImmutableList.of("id"), "foo*") + ); + for (TopicMgtArgs args : invalidArgs) { + try { + messaging.subscribeToTopicAsync(args.registrationTokens, args.topic); + fail("No error thrown for invalid args"); + } catch (IllegalArgumentException expected) { + // expected + } + } + + assertNull(interceptor.getResponse()); + } + + @Test + public void testSubscribe() throws Exception { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setContent("{\"results\": [{}, {\"error\": \"error_reason\"}]}"); + FirebaseMessaging messaging = initMessaging(response); TestResponseInterceptor interceptor = new TestResponseInterceptor(); messaging.setInterceptor(interceptor); - String resp = messaging.sendAsync(Message.builder().setTopic("test-topic").build()).get(); - assertEquals("mock-name", resp); + + List result = messaging.subscribeToTopicAsync( + ImmutableList.of("id1", "id2"), "test-topic").get(); + assertEquals(2, result.size()); + assertTrue(result.get(0).isSuccess()); + assertNull(result.get(0).getReason()); + assertFalse(result.get(1).isSuccess()); + assertEquals("error_reason", result.get(1).getReason()); assertNotNull(interceptor.getResponse()); HttpRequest request = interceptor.getResponse().getRequest(); assertEquals("POST", request.getRequestMethod()); - assertEquals(TEST_URL, request.getUrl().toString()); + assertEquals(TEST_IID_URL, request.getUrl().toString()); assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); + assertEquals("true", request.getHeaders().get("access_token_auth")); ByteArrayOutputStream out = new ByteArrayOutputStream(); request.getContent().writeTo(out); - assertEquals("{\"message\":{\"topic\":\"test-topic\"}}", out.toString()); + assertEquals("{\"to\":\"test-topic\",\"registration_tokens\":[\"id1\",\"id2\"]}", + out.toString()); } @Test - public void testSendError() throws Exception { + public void testSubscribeError() throws Exception { List errors = ImmutableList.of(401, 404, 500); for (int statusCode : errors) { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() .setStatusCode(statusCode) .setContent("test error"); - MockHttpTransport transport = new MockHttpTransport.Builder() - .setLowLevelHttpResponse(response) - .build(); - FirebaseOptions options = new FirebaseOptions.Builder() - .setCredentials(new MockGoogleCredentials("test-token")) - .setProjectId("test-project") - .setHttpTransport(transport) - .build(); - final FirebaseApp app = FirebaseApp.initializeApp(options); - - FirebaseMessaging messaging = FirebaseMessaging.getInstance(); + FirebaseMessaging messaging = initMessaging(response); TestResponseInterceptor interceptor = new TestResponseInterceptor(); messaging.setInterceptor(interceptor); try { - messaging.sendAsync(Message.builder().setTopic("test-topic").build()).get(); + messaging.subscribeToTopicAsync(ImmutableList.of("id1", "id2"), "test-topic").get(); fail("No error thrown for HTTP error"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseMessagingException); + assertTrue(e.getCause().getCause() instanceof HttpResponseException); } assertNotNull(interceptor.getResponse()); HttpRequest request = interceptor.getResponse().getRequest(); assertEquals("POST", request.getRequestMethod()); - assertEquals(TEST_URL, request.getUrl().toString()); + assertEquals(TEST_IID_URL, request.getUrl().toString()); assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); - app.delete(); + FirebaseApp.getInstance().delete(); + } + } + + private static FirebaseMessaging initMessaging(MockLowLevelHttpResponse mockResponse) { + MockHttpTransport transport = new MockHttpTransport.Builder() + .setLowLevelHttpResponse(mockResponse) + .build(); + FirebaseOptions options = new FirebaseOptions.Builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setProjectId("test-project") + .setHttpTransport(transport) + .build(); + FirebaseApp app = FirebaseApp.initializeApp(options); + + FirebaseMessaging messaging = FirebaseMessaging.getInstance(); + assertSame(messaging, FirebaseMessaging.getInstance(app)); + return messaging; + } + + private static class TopicMgtArgs { + private final List registrationTokens; + private final String topic; + + TopicMgtArgs(List registrationTokens, String topic) { + this.registrationTokens = registrationTokens; + this.topic = topic; } } } From da0f5d1bda66ce782aafa9e4f1f8f50d2af89f7c Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Wed, 27 Dec 2017 20:39:21 -0800 Subject: [PATCH 05/32] Added TopicManagementResponse type --- .../firebase/messaging/FirebaseMessaging.java | 20 +++---- .../messaging/TopicManagementResponse.java | 60 +++++++++++++++++++ .../messaging/TopicManagementResult.java | 22 ------- .../messaging/FirebaseMessagingIT.java | 4 +- .../messaging/FirebaseMessagingTest.java | 13 ++-- 5 files changed, 77 insertions(+), 42 deletions(-) create mode 100644 src/main/java/com/google/firebase/messaging/TopicManagementResponse.java delete mode 100644 src/main/java/com/google/firebase/messaging/TopicManagementResult.java diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java index 389cb8916..5e2d89644 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java @@ -17,7 +17,6 @@ import com.google.auth.oauth2.GoogleCredentials; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; -import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.firebase.FirebaseApp; import com.google.firebase.ImplFirebaseTrampolines; @@ -95,19 +94,19 @@ public String call() throws FirebaseMessagingException { }); } - public ApiFuture> subscribeToTopicAsync( + public ApiFuture subscribeToTopicAsync( List registrationTokens, String topic) { return new TaskToApiFuture<>(subscribeToTopic(registrationTokens, topic)); } - private Task> subscribeToTopic( + private Task subscribeToTopic( final List registrationTokens, final String topic) { checkRegistrationTokens(registrationTokens); checkTopic(topic); - return ImplFirebaseTrampolines.submitCallable(app, new Callable>() { + return ImplFirebaseTrampolines.submitCallable(app, new Callable() { @Override - public List call() throws FirebaseMessagingException { + public TopicManagementResponse call() throws FirebaseMessagingException { try { return makeTopicManagementRequest(registrationTokens, topic, IID_SUBSCRIBE_PATH); } catch (IOException e) { @@ -134,7 +133,7 @@ private String makeSendRequest(Message message) throws IOException { } } - private List makeTopicManagementRequest( + private TopicManagementResponse makeTopicManagementRequest( List registrationTokens, String topic, String path) throws IOException { Map payload = ImmutableMap.of( "to", topic, @@ -151,12 +150,11 @@ private List makeTopicManagementRequest( try { response = request.execute(); Map parsed = response.parseAs(Map.class); - List> results = (List>) parsed.get("results"); - ImmutableList.Builder builder = ImmutableList.builder(); - for (Map map : results) { - builder.add(new TopicManagementResult(map)); + List results = (List) parsed.get("results"); + if (results == null || results.isEmpty()) { + throw new IOException("Unexpected topic management response"); } - return builder.build(); + return new TopicManagementResponse(results); } finally { if (response != null) { response.disconnect(); diff --git a/src/main/java/com/google/firebase/messaging/TopicManagementResponse.java b/src/main/java/com/google/firebase/messaging/TopicManagementResponse.java new file mode 100644 index 000000000..62a81539d --- /dev/null +++ b/src/main/java/com/google/firebase/messaging/TopicManagementResponse.java @@ -0,0 +1,60 @@ +package com.google.firebase.messaging; + +import com.google.common.collect.ImmutableList; +import java.util.List; +import java.util.Map; + +public class TopicManagementResponse { + + private final int successCount; + private final int failureCount; + private final List errors; + + TopicManagementResponse(List results) { + int successCount = 0; + int failureCount = 0; + ImmutableList.Builder errors = ImmutableList.builder(); + for (int i = 0; i < results.size(); i++) { + Map result = results.get(i); + if (result.isEmpty()) { + successCount++; + } else { + failureCount++; + errors.add(new Error(i, (String) result.get("error"))); + } + } + this.successCount = successCount; + this.failureCount = failureCount; + this.errors = errors.build(); + } + + public int getSuccessCount() { + return successCount; + } + + public int getFailureCount() { + return failureCount; + } + + public List getErrors() { + return errors; + } + + public static class Error { + private final int index; + private final String reason; + + private Error(int index, String reason) { + this.index = index; + this.reason = reason; + } + + public int getIndex() { + return index; + } + + public String getReason() { + return reason; + } + } +} diff --git a/src/main/java/com/google/firebase/messaging/TopicManagementResult.java b/src/main/java/com/google/firebase/messaging/TopicManagementResult.java deleted file mode 100644 index d7e5bfffc..000000000 --- a/src/main/java/com/google/firebase/messaging/TopicManagementResult.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.google.firebase.messaging; - -import java.util.Map; - -public class TopicManagementResult { - - private final boolean success; - private final String reason; - - TopicManagementResult(Map response) { - this.success = response.isEmpty(); - this.reason = (String) response.get("error"); - } - - public boolean isSuccess() { - return success; - } - - public String getReason() { - return reason; - } -} diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java index 092ef88b7..0d0db794e 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java @@ -38,8 +38,8 @@ public void testSend() throws Exception { @Test public void testSubscribe() throws Exception { FirebaseMessaging messaging = FirebaseMessaging.getInstance(); - List results = messaging.subscribeToTopicAsync( + TopicManagementResponse results = messaging.subscribeToTopicAsync( ImmutableList.of(TEST_REGISTRATION_TOKEN), "/topics/mock-topic").get(); - assertEquals(1, results.size()); + assertEquals(1, results.getSuccessCount() + results.getFailureCount()); } } diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java index 5ff85ccb5..232e5075e 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java @@ -1,7 +1,6 @@ package com.google.firebase.messaging; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; @@ -165,13 +164,13 @@ public void testSubscribe() throws Exception { TestResponseInterceptor interceptor = new TestResponseInterceptor(); messaging.setInterceptor(interceptor); - List result = messaging.subscribeToTopicAsync( + TopicManagementResponse result = messaging.subscribeToTopicAsync( ImmutableList.of("id1", "id2"), "test-topic").get(); - assertEquals(2, result.size()); - assertTrue(result.get(0).isSuccess()); - assertNull(result.get(0).getReason()); - assertFalse(result.get(1).isSuccess()); - assertEquals("error_reason", result.get(1).getReason()); + assertEquals(1, result.getSuccessCount()); + assertEquals(1, result.getFailureCount()); + assertEquals(1, result.getErrors().size()); + assertEquals(1, result.getErrors().get(0).getIndex()); + assertEquals("error_reason", result.getErrors().get(0).getReason()); assertNotNull(interceptor.getResponse()); HttpRequest request = interceptor.getResponse().getRequest(); From 565fd01d55ccaf602fdcfc249abe4a90d58f8a90 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Thu, 28 Dec 2017 15:53:38 -0800 Subject: [PATCH 06/32] Added unsub method --- .../firebase/messaging/FirebaseMessaging.java | 30 ++++ .../messaging/FirebaseMessagingIT.java | 8 ++ .../messaging/FirebaseMessagingTest.java | 130 ++++++++++++++---- 3 files changed, 144 insertions(+), 24 deletions(-) diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java index 5e2d89644..68b52cf82 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java @@ -10,8 +10,10 @@ import com.google.api.client.http.HttpResponseInterceptor; import com.google.api.client.http.HttpTransport; import com.google.api.client.http.json.JsonHttpContent; +import com.google.api.client.json.GenericJson; import com.google.api.client.json.JsonFactory; import com.google.api.client.json.JsonObjectParser; +import com.google.api.client.util.Key; import com.google.api.core.ApiFuture; import com.google.auth.http.HttpCredentialsAdapter; import com.google.auth.oauth2.GoogleCredentials; @@ -35,6 +37,7 @@ public class FirebaseMessaging { private static final String IID_HOST = "https://iid.googleapis.com"; private static final String IID_SUBSCRIBE_PATH = "iid/v1:batchAdd"; + private static final String IID_UNSUBSCRIBE_PATH = "iid/v1:batchRemove"; private final FirebaseApp app; private final HttpRequestFactory requestFactory; @@ -116,6 +119,28 @@ public TopicManagementResponse call() throws FirebaseMessagingException { }); } + public ApiFuture unsubscribeFromTopicAsync( + List registrationTokens, String topic) { + return new TaskToApiFuture<>(unsubscribeFromTopic(registrationTokens, topic)); + } + + private Task unsubscribeFromTopic( + final List registrationTokens, final String topic) { + checkRegistrationTokens(registrationTokens); + checkTopic(topic); + + return ImplFirebaseTrampolines.submitCallable(app, new Callable() { + @Override + public TopicManagementResponse call() throws FirebaseMessagingException { + try { + return makeTopicManagementRequest(registrationTokens, topic, IID_UNSUBSCRIBE_PATH); + } catch (IOException e) { + throw new FirebaseMessagingException("Error while calling IID service", e); + } + } + }); + } + private String makeSendRequest(Message message) throws IOException { HttpRequest request = requestFactory.buildPostRequest( new GenericUrl(url), new JsonHttpContent(jsonFactory, ImmutableMap.of("message", message))); @@ -198,4 +223,9 @@ public void destroy() { // which will throw once the app is deleted. } } + + public static class TopicMgtOutput { + @Key("results") + private List> results; + } } diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java index 0d0db794e..5588f6229 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java @@ -42,4 +42,12 @@ public void testSubscribe() throws Exception { ImmutableList.of(TEST_REGISTRATION_TOKEN), "/topics/mock-topic").get(); assertEquals(1, results.getSuccessCount() + results.getFailureCount()); } + + @Test + public void testUnsubscribe() throws Exception { + FirebaseMessaging messaging = FirebaseMessaging.getInstance(); + TopicManagementResponse results = messaging.unsubscribeFromTopicAsync( + ImmutableList.of(TEST_REGISTRATION_TOKEN), "/topics/mock-topic").get(); + assertEquals(1, results.getSuccessCount() + results.getFailureCount()); + } } diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java index 232e5075e..a84fb09b4 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java @@ -27,8 +27,30 @@ public class FirebaseMessagingTest { private static final String TEST_FCM_URL = "https://fcm.googleapis.com/v1/projects/test-project/messages:send"; - private static final String TEST_IID_URL = + private static final String TEST_IID_SUBSCRIBE_URL = "https://iid.googleapis.com/iid/v1:batchAdd"; + private static final String TEST_IID_UNSUBSCRIBE_URL = + "https://iid.googleapis.com/iid/v1:batchRemove"; + private static final List HTTP_ERRORS = ImmutableList.of(401, 404, 500); + + private static final ImmutableList.Builder tooManyIds = ImmutableList.builder(); + + static { + for (int i = 0; i < 1001; i++) { + tooManyIds.add("id" + i); + } + } + + private static final List INVALID_TOPIC_MGT_ARGS = ImmutableList.of( + new TopicMgtArgs(null, null), + new TopicMgtArgs(null, "test-topic"), + new TopicMgtArgs(ImmutableList.of(), "test-topic"), + new TopicMgtArgs(ImmutableList.of(""), "test-topic"), + new TopicMgtArgs(tooManyIds.build(), "test-topic"), + new TopicMgtArgs(ImmutableList.of(""), null), + new TopicMgtArgs(ImmutableList.of("id"), ""), + new TopicMgtArgs(ImmutableList.of("id"), "foo*") + ); @After public void tearDown() { @@ -93,9 +115,7 @@ public void testSend() throws Exception { @Test public void testSendError() throws Exception { - List errors = ImmutableList.of(401, 404, 500); - - for (int statusCode : errors) { + for (int statusCode : HTTP_ERRORS) { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() .setStatusCode(statusCode) .setContent("test error"); @@ -130,21 +150,7 @@ public void testInvalidSubscribe() { TestResponseInterceptor interceptor = new TestResponseInterceptor(); messaging.setInterceptor(interceptor); - ImmutableList.Builder tooManyIds = ImmutableList.builder(); - for (int i = 0; i < 1001; i++) { - tooManyIds.add("id" + i); - } - List invalidArgs = ImmutableList.of( - new TopicMgtArgs(null, null), - new TopicMgtArgs(null, "test-topic"), - new TopicMgtArgs(ImmutableList.of(), "test-topic"), - new TopicMgtArgs(ImmutableList.of(""), "test-topic"), - new TopicMgtArgs(tooManyIds.build(), "test-topic"), - new TopicMgtArgs(ImmutableList.of(""), null), - new TopicMgtArgs(ImmutableList.of("id"), ""), - new TopicMgtArgs(ImmutableList.of("id"), "foo*") - ); - for (TopicMgtArgs args : invalidArgs) { + for (TopicMgtArgs args : INVALID_TOPIC_MGT_ARGS) { try { messaging.subscribeToTopicAsync(args.registrationTokens, args.topic); fail("No error thrown for invalid args"); @@ -175,7 +181,7 @@ public void testSubscribe() throws Exception { assertNotNull(interceptor.getResponse()); HttpRequest request = interceptor.getResponse().getRequest(); assertEquals("POST", request.getRequestMethod()); - assertEquals(TEST_IID_URL, request.getUrl().toString()); + assertEquals(TEST_IID_SUBSCRIBE_URL, request.getUrl().toString()); assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); assertEquals("true", request.getHeaders().get("access_token_auth")); @@ -187,9 +193,7 @@ public void testSubscribe() throws Exception { @Test public void testSubscribeError() throws Exception { - List errors = ImmutableList.of(401, 404, 500); - - for (int statusCode : errors) { + for (int statusCode : HTTP_ERRORS) { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() .setStatusCode(statusCode) .setContent("test error"); @@ -207,7 +211,85 @@ public void testSubscribeError() throws Exception { assertNotNull(interceptor.getResponse()); HttpRequest request = interceptor.getResponse().getRequest(); assertEquals("POST", request.getRequestMethod()); - assertEquals(TEST_IID_URL, request.getUrl().toString()); + assertEquals(TEST_IID_SUBSCRIBE_URL, request.getUrl().toString()); + assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); + FirebaseApp.getInstance().delete(); + } + } + + @Test + public void testInvalidUnsubscribe() { + FirebaseOptions options = new FirebaseOptions.Builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setProjectId("test-project") + .build(); + FirebaseApp.initializeApp(options); + FirebaseMessaging messaging = FirebaseMessaging.getInstance(); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + messaging.setInterceptor(interceptor); + + for (TopicMgtArgs args : INVALID_TOPIC_MGT_ARGS) { + try { + messaging.unsubscribeFromTopicAsync(args.registrationTokens, args.topic); + fail("No error thrown for invalid args"); + } catch (IllegalArgumentException expected) { + // expected + } + } + + assertNull(interceptor.getResponse()); + } + + @Test + public void testUnsubscribe() throws Exception { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setContent("{\"results\": [{}, {\"error\": \"error_reason\"}]}"); + FirebaseMessaging messaging = initMessaging(response); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + messaging.setInterceptor(interceptor); + + TopicManagementResponse result = messaging.unsubscribeFromTopicAsync( + ImmutableList.of("id1", "id2"), "test-topic").get(); + assertEquals(1, result.getSuccessCount()); + assertEquals(1, result.getFailureCount()); + assertEquals(1, result.getErrors().size()); + assertEquals(1, result.getErrors().get(0).getIndex()); + assertEquals("error_reason", result.getErrors().get(0).getReason()); + + assertNotNull(interceptor.getResponse()); + HttpRequest request = interceptor.getResponse().getRequest(); + assertEquals("POST", request.getRequestMethod()); + assertEquals(TEST_IID_UNSUBSCRIBE_URL, request.getUrl().toString()); + assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); + assertEquals("true", request.getHeaders().get("access_token_auth")); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + request.getContent().writeTo(out); + assertEquals("{\"to\":\"test-topic\",\"registration_tokens\":[\"id1\",\"id2\"]}", + out.toString()); + } + + @Test + public void testUnsubscribeError() throws Exception { + for (int statusCode : HTTP_ERRORS) { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setStatusCode(statusCode) + .setContent("test error"); + FirebaseMessaging messaging = initMessaging(response); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + messaging.setInterceptor(interceptor); + try { + messaging.unsubscribeFromTopicAsync(ImmutableList.of("id1", "id2"), "test-topic").get(); + fail("No error thrown for HTTP error"); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseMessagingException); + assertTrue(e.getCause().getCause() instanceof HttpResponseException); + } + + assertNotNull(interceptor.getResponse()); + HttpRequest request = interceptor.getResponse().getRequest(); + assertEquals("POST", request.getRequestMethod()); + assertEquals(TEST_IID_UNSUBSCRIBE_URL, request.getUrl().toString()); assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); FirebaseApp.getInstance().delete(); } From 174b3027e3777c171ad3f39bafa816902a48ee5e Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Fri, 29 Dec 2017 14:17:35 -0800 Subject: [PATCH 07/32] Improved error handling and json parsing --- .../firebase/messaging/FirebaseMessaging.java | 34 +++++++++++-------- .../messaging/TopicManagementResponse.java | 2 +- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java index 68b52cf82..11586d981 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java @@ -2,6 +2,7 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; import com.google.api.client.http.GenericUrl; import com.google.api.client.http.HttpRequest; @@ -10,7 +11,6 @@ import com.google.api.client.http.HttpResponseInterceptor; import com.google.api.client.http.HttpTransport; import com.google.api.client.http.json.JsonHttpContent; -import com.google.api.client.json.GenericJson; import com.google.api.client.json.JsonFactory; import com.google.api.client.json.JsonObjectParser; import com.google.api.client.util.Key; @@ -22,7 +22,6 @@ import com.google.common.collect.ImmutableMap; import com.google.firebase.FirebaseApp; import com.google.firebase.ImplFirebaseTrampolines; -import com.google.firebase.iid.FirebaseInstanceId; import com.google.firebase.internal.FirebaseService; import com.google.firebase.internal.TaskToApiFuture; import com.google.firebase.tasks.Task; @@ -66,9 +65,9 @@ public static FirebaseMessaging getInstance() { } /** - * Gets the {@link FirebaseInstanceId} instance for the specified {@link FirebaseApp}. + * Gets the {@link FirebaseMessaging} instance for the specified {@link FirebaseApp}. * - * @return The {@link FirebaseInstanceId} instance for the specified {@link FirebaseApp}. + * @return The {@link FirebaseMessaging} instance for the specified {@link FirebaseApp}. */ public static synchronized FirebaseMessaging getInstance(FirebaseApp app) { FirebaseMessagingService service = ImplFirebaseTrampolines.getService(app, SERVICE_ID, @@ -112,7 +111,7 @@ private Task subscribeToTopic( public TopicManagementResponse call() throws FirebaseMessagingException { try { return makeTopicManagementRequest(registrationTokens, topic, IID_SUBSCRIBE_PATH); - } catch (IOException e) { + } catch (Exception e) { throw new FirebaseMessagingException("Error while calling IID service", e); } } @@ -134,7 +133,7 @@ private Task unsubscribeFromTopic( public TopicManagementResponse call() throws FirebaseMessagingException { try { return makeTopicManagementRequest(registrationTokens, topic, IID_UNSUBSCRIBE_PATH); - } catch (IOException e) { + } catch (Exception e) { throw new FirebaseMessagingException("Error while calling IID service", e); } } @@ -149,8 +148,9 @@ private String makeSendRequest(Message message) throws IOException { HttpResponse response = null; try { response = request.execute(); - Map map = response.parseAs(Map.class); - return (String) map.get("name"); + MessagingServiceResponse parsed = new MessagingServiceResponse(); + jsonFactory.createJsonParser(response.getContent()).parseAndClose(parsed); + return parsed.name; } finally { if (response != null) { response.disconnect(); @@ -174,12 +174,11 @@ private TopicManagementResponse makeTopicManagementRequest( HttpResponse response = null; try { response = request.execute(); - Map parsed = response.parseAs(Map.class); - List results = (List) parsed.get("results"); - if (results == null || results.isEmpty()) { - throw new IOException("Unexpected topic management response"); - } - return new TopicManagementResponse(results); + InstanceIdServiceResponse parsed = new InstanceIdServiceResponse(); + jsonFactory.createJsonParser(response.getContent()).parseAndClose(parsed); + checkState(parsed.results != null && !parsed.results.isEmpty(), + "unexpected response from topic management service"); + return new TopicManagementResponse(parsed.results); } finally { if (response != null) { response.disconnect(); @@ -224,7 +223,12 @@ public void destroy() { } } - public static class TopicMgtOutput { + private static class MessagingServiceResponse { + @Key("name") + private String name; + } + + private static class InstanceIdServiceResponse { @Key("results") private List> results; } diff --git a/src/main/java/com/google/firebase/messaging/TopicManagementResponse.java b/src/main/java/com/google/firebase/messaging/TopicManagementResponse.java index 62a81539d..fbc978a2b 100644 --- a/src/main/java/com/google/firebase/messaging/TopicManagementResponse.java +++ b/src/main/java/com/google/firebase/messaging/TopicManagementResponse.java @@ -10,7 +10,7 @@ public class TopicManagementResponse { private final int failureCount; private final List errors; - TopicManagementResponse(List results) { + TopicManagementResponse(List> results) { int successCount = 0; int failureCount = 0; ImmutableList.Builder errors = ImmutableList.builder(); From 97a15a099ada13f0b6d705a429a512690171a1bf Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Tue, 2 Jan 2018 14:50:42 -0800 Subject: [PATCH 08/32] More tests, validation and dry run mode --- .../firebase/messaging/FirebaseMessaging.java | 19 +- .../google/firebase/messaging/Message.java | 5 + .../messaging/FirebaseMessagingIT.java | 3 +- .../messaging/FirebaseMessagingTest.java | 183 +++++++++++++++++- 4 files changed, 193 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java index 11586d981..f673b3e0e 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java @@ -79,16 +79,20 @@ public static synchronized FirebaseMessaging getInstance(FirebaseApp app) { } public ApiFuture sendAsync(Message message) { - return new TaskToApiFuture<>(send(message)); + return sendAsync(message, false); } - private Task send(final Message message) { + public ApiFuture sendAsync(Message message, boolean dryRun) { + return new TaskToApiFuture<>(send(message, dryRun)); + } + + private Task send(final Message message, final boolean dryRun) { checkNotNull(message, "message must not be null"); return ImplFirebaseTrampolines.submitCallable(app, new Callable() { @Override public String call() throws FirebaseMessagingException { try { - return makeSendRequest(message); + return makeSendRequest(message, dryRun); } catch (Exception e) { throw new FirebaseMessagingException("Error while calling FCM service", e); } @@ -140,9 +144,14 @@ public TopicManagementResponse call() throws FirebaseMessagingException { }); } - private String makeSendRequest(Message message) throws IOException { + private String makeSendRequest(Message message, boolean dryRun) throws IOException { + ImmutableMap.Builder payload = ImmutableMap.builder() + .put("message", message); + if (dryRun) { + payload.put("validate_only", true); + } HttpRequest request = requestFactory.buildPostRequest( - new GenericUrl(url), new JsonHttpContent(jsonFactory, ImmutableMap.of("message", message))); + new GenericUrl(url), new JsonHttpContent(jsonFactory, payload.build())); request.setParser(new JsonObjectParser(jsonFactory)); request.setResponseInterceptor(interceptor); HttpResponse response = null; diff --git a/src/main/java/com/google/firebase/messaging/Message.java b/src/main/java/com/google/firebase/messaging/Message.java index c2d61b8ef..edf92aa52 100644 --- a/src/main/java/com/google/firebase/messaging/Message.java +++ b/src/main/java/com/google/firebase/messaging/Message.java @@ -47,6 +47,11 @@ private Message(Builder builder) { !Strings.isNullOrEmpty(builder.condition) ); checkArgument(count == 1, "Exactly one of token, topic or condition must be specified"); + if (builder.topic != null) { + checkArgument(!builder.topic.startsWith("/topics/"), + "Topic name must not contain the /topics/ prefix"); + checkArgument(builder.topic.matches("[a-zA-Z0-9-_.~%]+"), "Malformed topic name"); + } this.token = builder.token; this.topic = builder.topic; this.condition = builder.condition; diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java index 5588f6229..11204d5f0 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java @@ -4,7 +4,6 @@ import com.google.common.collect.ImmutableList; import com.google.firebase.testing.IntegrationTestUtils; -import java.util.List; import org.junit.BeforeClass; import org.junit.Test; @@ -31,7 +30,7 @@ public void testSend() throws Exception { .setWebpushConfig(WebpushConfig.builder().putHeader("X-Custom-Val", "Foo").build()) .setTopic("foo-bar") .build(); - String resp = messaging.sendAsync(message).get(); + String resp = messaging.sendAsync(message, true).get(); System.out.println(resp); } diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java index a84fb09b4..c6d1d7273 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java @@ -7,18 +7,24 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import com.google.api.client.googleapis.util.Utils; import com.google.api.client.http.HttpRequest; import com.google.api.client.http.HttpResponseException; +import com.google.api.client.json.JsonParser; import com.google.api.client.testing.http.MockHttpTransport; import com.google.api.client.testing.http.MockLowLevelHttpResponse; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.auth.MockGoogleCredentials; import com.google.firebase.testing.TestResponseInterceptor; import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.ExecutionException; import org.junit.After; import org.junit.Test; @@ -97,20 +103,57 @@ public void testSend() throws Exception { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() .setContent("{\"name\": \"mock-name\"}"); FirebaseMessaging messaging = initMessaging(response); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - messaging.setInterceptor(interceptor); - String resp = messaging.sendAsync(Message.builder().setTopic("test-topic").build()).get(); - assertEquals("mock-name", resp); + Map> testMessages = buildTestMessages(); - assertNotNull(interceptor.getResponse()); - HttpRequest request = interceptor.getResponse().getRequest(); - assertEquals("POST", request.getRequestMethod()); - assertEquals(TEST_FCM_URL, request.getUrl().toString()); - assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); + for (Map.Entry> entry : testMessages.entrySet()) { + response.setContent("{\"name\": \"mock-name\"}"); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + messaging.setInterceptor(interceptor); + String resp = messaging.sendAsync(entry.getKey()).get(); + assertEquals("mock-name", resp); + + assertNotNull(interceptor.getResponse()); + HttpRequest request = interceptor.getResponse().getRequest(); + assertEquals("POST", request.getRequestMethod()); + assertEquals(TEST_FCM_URL, request.getUrl().toString()); + assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); + + checkRequest(request, ImmutableMap.of("message", entry.getValue())); + } + } + + @Test + public void testSendDryRun() throws Exception { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setContent("{\"name\": \"mock-name\"}"); + FirebaseMessaging messaging = initMessaging(response); + Map> testMessages = buildTestMessages(); + + for (Map.Entry> entry : testMessages.entrySet()) { + response.setContent("{\"name\": \"mock-name\"}"); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + messaging.setInterceptor(interceptor); + String resp = messaging.sendAsync(entry.getKey(), true).get(); + assertEquals("mock-name", resp); + + assertNotNull(interceptor.getResponse()); + HttpRequest request = interceptor.getResponse().getRequest(); + assertEquals("POST", request.getRequestMethod()); + assertEquals(TEST_FCM_URL, request.getUrl().toString()); + assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); + checkRequest(request, ImmutableMap.of("message", entry.getValue(), "validate_only", true)); + } + } + + private static void checkRequest( + HttpRequest request, Map expected) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); request.getContent().writeTo(out); - assertEquals("{\"message\":{\"topic\":\"test-topic\"}}", out.toString()); + JsonParser parser = Utils.getDefaultJsonFactory().createJsonParser(out.toString()); + Map parsed = new HashMap<>(); + parser.parseAndClose(parsed); + assertEquals(expected, parsed); } @Test @@ -320,4 +363,124 @@ private static class TopicMgtArgs { this.topic = topic; } } + + private static Map> buildTestMessages() { + ImmutableMap.Builder> builder = ImmutableMap.builder(); + + // Empty message + builder.put( + Message.builder().setTopic("test-topic").build(), + ImmutableMap.of("topic", "test-topic")); + + // Notification message + builder.put( + Message.builder() + .setNotification(new Notification("test title", "test body")) + .setTopic("test-topic") + .build(), + ImmutableMap.of( + "topic", "test-topic", + "notification", ImmutableMap.of("title", "test title", "body", "test body"))); + + // Data message + builder.put( + Message.builder() + .putData("k1", "v1") + .putData("k2", "v2") + .putAllData(ImmutableMap.of("k3", "v3", "k4", "v4")) + .setTopic("test-topic") + .build(), + ImmutableMap.of( + "topic", "test-topic", + "data", ImmutableMap.of("k1", "v1", "k2", "v2", "k3", "v3", "k4", "v4"))); + + // Android message + builder.put( + Message.builder() + .setAndroidConfig(AndroidConfig.builder() + .setPriority(AndroidConfig.Priority.high) + .setTtl("1.23s") + .setRestrictedPackageName("test-package") + .setCollapseKey("test-key") + .setNotification(AndroidNotification.builder() + .setClickAction("test-action") + .setTitle("test-title") + .setBody("test-body") + .setIcon("test-icon") + .setColor("#112233") + .setTag("test-tag") + .setSound("test-sound") + .setTitleLocKey("test-title-key") + .setBodyLocKey("test-body-key") + .addTitleLocArg("t-arg1") + .addAllTitleLocArgs(ImmutableList.of("t-arg2", "t-arg3")) + .addBodyLocArg("b-arg1") + .addAllBodyLocArgs(ImmutableList.of("b-arg2", "b-arg3")) + .build()) + .build()) + .setTopic("test-topic") + .build(), + ImmutableMap.of( + "topic", "test-topic", + "android", ImmutableMap.of( + "priority", "high", + "collapse_key", "test-key", + "ttl", "1.23s", + "restricted_package_name", "test-package", + "notification", ImmutableMap.builder() + .put("click_action", "test-action") + .put("title", "test-title") + .put("body", "test-body") + .put("icon", "test-icon") + .put("color", "#112233") + .put("tag", "test-tag") + .put("sound", "test-sound") + .put("title_loc_key", "test-title-key") + .put("title_loc_args", ImmutableList.of("t-arg1", "t-arg2", "t-arg3")) + .put("body_loc_key", "test-body-key") + .put("body_loc_args", ImmutableList.of("b-arg1", "b-arg2", "b-arg3")) + .build() + ) + )); + + // APNS message + builder.put( + Message.builder() + .setApnsConfig(ApnsConfig.builder() + .putHeader("h1", "v1") + .putAllHeaders(ImmutableMap.of("h2", "v2", "h3", "v3")) + .setPayload(ImmutableMap.of("k1", "v1", "k2", true)) + .build()) + .setTopic("test-topic") + .build(), + ImmutableMap.of( + "topic", "test-topic", + "apns", ImmutableMap.of( + "headers", ImmutableMap.of("h1", "v1", "h2", "v2", "h3", "v3"), + "payload", ImmutableMap.of("k1", "v1", "k2", true)) + )); + + // Webpush message + builder.put( + Message.builder() + .setWebpushConfig(WebpushConfig.builder() + .putHeader("h1", "v1") + .putAllHeaders(ImmutableMap.of("h2", "v2", "h3", "v3")) + .putData("k1", "v1") + .putAllData(ImmutableMap.of("k2", "v2", "k3", "v3")) + .setNotification(new WebpushNotification("test-title", "test-body", "test-icon")) + .build()) + .setTopic("test-topic") + .build(), + ImmutableMap.of( + "topic", "test-topic", + "webpush", ImmutableMap.of( + "headers", ImmutableMap.of("h1", "v1", "h2", "v2", "h3", "v3"), + "data", ImmutableMap.of("k1", "v1", "k2", "v2", "k3", "v3"), + "notification", ImmutableMap.of( + "title", "test-title", "body", "test-body", "icon", "test-icon")) + )); + + return builder.build(); + } } From 4798b435e99b881cf4897d770d39b03d6bdba80a Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Tue, 2 Jan 2018 17:01:21 -0800 Subject: [PATCH 09/32] Improved error handling and tests --- .../firebase/messaging/FirebaseMessaging.java | 114 +++++++++++++----- .../messaging/FirebaseMessagingException.java | 19 ++- .../messaging/TopicManagementResponse.java | 15 ++- .../messaging/FirebaseMessagingIT.java | 7 +- .../messaging/FirebaseMessagingTest.java | 69 ++++++++--- 5 files changed, 169 insertions(+), 55 deletions(-) diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java index f673b3e0e..6b4a36df1 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java @@ -8,6 +8,7 @@ import com.google.api.client.http.HttpRequest; import com.google.api.client.http.HttpRequestFactory; import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpResponseInterceptor; import com.google.api.client.http.HttpTransport; import com.google.api.client.http.json.JsonHttpContent; @@ -34,6 +35,16 @@ public class FirebaseMessaging { private static final String FCM_URL = "https://fcm.googleapis.com/v1/projects/%s/messages:send"; + private static final String INTERNAL_ERROR = "internal-error"; + private static final Map ERROR_CODES = ImmutableMap.builder() + .put("INVALID_ARGUMENT", "invalid-argument") + .put("NOT_FOUND", "registration-token-not-registered") + .put("PERMISSION_DENIED", "authentication-error") + .put("RESOURCE_EXHAUSTED", "message-rate-exceeded") + .put("UNAUTHENTICATED", "authentication-error") + .put("UNAVAILABLE", "server-unavailable") + .build(); + private static final String IID_HOST = "https://iid.googleapis.com"; private static final String IID_SUBSCRIBE_PATH = "iid/v1:batchAdd"; private static final String IID_UNSUBSCRIBE_PATH = "iid/v1:batchRemove"; @@ -91,11 +102,7 @@ private Task send(final Message message, final boolean dryRun) { return ImplFirebaseTrampolines.submitCallable(app, new Callable() { @Override public String call() throws FirebaseMessagingException { - try { - return makeSendRequest(message, dryRun); - } catch (Exception e) { - throw new FirebaseMessagingException("Error while calling FCM service", e); - } + return makeSendRequest(message, dryRun); } }); } @@ -113,11 +120,7 @@ private Task subscribeToTopic( return ImplFirebaseTrampolines.submitCallable(app, new Callable() { @Override public TopicManagementResponse call() throws FirebaseMessagingException { - try { - return makeTopicManagementRequest(registrationTokens, topic, IID_SUBSCRIBE_PATH); - } catch (Exception e) { - throw new FirebaseMessagingException("Error while calling IID service", e); - } + return makeTopicManagementRequest(registrationTokens, topic, IID_SUBSCRIBE_PATH); } }); } @@ -135,62 +138,113 @@ private Task unsubscribeFromTopic( return ImplFirebaseTrampolines.submitCallable(app, new Callable() { @Override public TopicManagementResponse call() throws FirebaseMessagingException { - try { - return makeTopicManagementRequest(registrationTokens, topic, IID_UNSUBSCRIBE_PATH); - } catch (Exception e) { - throw new FirebaseMessagingException("Error while calling IID service", e); - } + return makeTopicManagementRequest(registrationTokens, topic, IID_UNSUBSCRIBE_PATH); } }); } - private String makeSendRequest(Message message, boolean dryRun) throws IOException { + private String makeSendRequest(Message message, + boolean dryRun) throws FirebaseMessagingException { ImmutableMap.Builder payload = ImmutableMap.builder() .put("message", message); if (dryRun) { payload.put("validate_only", true); } - HttpRequest request = requestFactory.buildPostRequest( - new GenericUrl(url), new JsonHttpContent(jsonFactory, payload.build())); - request.setParser(new JsonObjectParser(jsonFactory)); - request.setResponseInterceptor(interceptor); HttpResponse response = null; try { + HttpRequest request = requestFactory.buildPostRequest( + new GenericUrl(url), new JsonHttpContent(jsonFactory, payload.build())); + request.setParser(new JsonObjectParser(jsonFactory)); + request.setResponseInterceptor(interceptor); response = request.execute(); MessagingServiceResponse parsed = new MessagingServiceResponse(); jsonFactory.createJsonParser(response.getContent()).parseAndClose(parsed); return parsed.name; + } catch (HttpResponseException e) { + handleSendHttpError(e); + return null; + } catch (IOException e) { + throw new FirebaseMessagingException( + INTERNAL_ERROR, "Error while calling IID backend service", e); } finally { - if (response != null) { - response.disconnect(); + disconnectQuietly(response); + } + } + + private void handleSendHttpError(HttpResponseException e) throws FirebaseMessagingException { + try { + Map response = jsonFactory.fromString(e.getContent(), Map.class); + Map error = (Map) response.get("error"); + if (error != null) { + String status = (String) error.get("status"); + String code = ERROR_CODES.get(status); + if (code != null) { + String message = (String) error.get("message"); + throw new FirebaseMessagingException(code, message, e); + } } + } catch (IOException ignored) { + // ignored } + String msg = String.format( + "Unexpected HTTP response with status: %d; body: %s", e.getStatusCode(), e.getContent()); + throw new FirebaseMessagingException(INTERNAL_ERROR, msg, e); } - private TopicManagementResponse makeTopicManagementRequest( - List registrationTokens, String topic, String path) throws IOException { + private TopicManagementResponse makeTopicManagementRequest(List registrationTokens, + String topic, String path) throws FirebaseMessagingException { Map payload = ImmutableMap.of( "to", topic, "registration_tokens", registrationTokens ); final String url = String.format("%s/%s", IID_HOST, path); - HttpRequest request = requestFactory.buildPostRequest( - new GenericUrl(url), new JsonHttpContent(jsonFactory, payload)); - request.getHeaders().set("access_token_auth", "true"); - request.setParser(new JsonObjectParser(jsonFactory)); - request.setResponseInterceptor(interceptor); HttpResponse response = null; try { + HttpRequest request = requestFactory.buildPostRequest( + new GenericUrl(url), new JsonHttpContent(jsonFactory, payload)); + request.getHeaders().set("access_token_auth", "true"); + request.setParser(new JsonObjectParser(jsonFactory)); + request.setResponseInterceptor(interceptor); response = request.execute(); InstanceIdServiceResponse parsed = new InstanceIdServiceResponse(); jsonFactory.createJsonParser(response.getContent()).parseAndClose(parsed); checkState(parsed.results != null && !parsed.results.isEmpty(), "unexpected response from topic management service"); return new TopicManagementResponse(parsed.results); + } catch (HttpResponseException e) { + handleTopicManagementHttpError(e); + return null; + } catch (IOException e) { + throw new FirebaseMessagingException( + INTERNAL_ERROR, "Error while calling IID backend service", e); } finally { - if (response != null) { + disconnectQuietly(response); + } + } + + private void handleTopicManagementHttpError( + HttpResponseException e) throws FirebaseMessagingException { + try { + Map response = jsonFactory.fromString(e.getContent(), Map.class); + String error = (String) response.get("error"); + if (!Strings.isNullOrEmpty(error)) { + throw new FirebaseMessagingException(INTERNAL_ERROR, error, e); + } + } catch (IOException ignored) { + // ignored + } + String msg = String.format( + "Unexpected HTTP response with status: %d; body: %s", e.getStatusCode(), e.getContent()); + throw new FirebaseMessagingException(INTERNAL_ERROR, msg, e); + } + + private static void disconnectQuietly(HttpResponse response) { + if (response != null) { + try { response.disconnect(); + } catch (IOException ignored) { + // ignored } } } diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessagingException.java b/src/main/java/com/google/firebase/messaging/FirebaseMessagingException.java index 3d7ab0cbf..ab2884c3e 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessagingException.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessagingException.java @@ -1,10 +1,25 @@ package com.google.firebase.messaging; +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.common.base.Strings; import com.google.firebase.FirebaseException; +import com.google.firebase.internal.NonNull; public class FirebaseMessagingException extends FirebaseException { - FirebaseMessagingException(String detailMessage, Throwable cause) { - super(detailMessage, cause); + private final String errorCode; + + FirebaseMessagingException(String errorCode, String message, Throwable cause) { + super(message, cause); + checkArgument(!Strings.isNullOrEmpty(errorCode)); + this.errorCode = errorCode; + } + + + /** Returns an error code that may provide more information about the error. */ + @NonNull + public String getErrorCode() { + return errorCode; } } diff --git a/src/main/java/com/google/firebase/messaging/TopicManagementResponse.java b/src/main/java/com/google/firebase/messaging/TopicManagementResponse.java index fbc978a2b..f489d41c5 100644 --- a/src/main/java/com/google/firebase/messaging/TopicManagementResponse.java +++ b/src/main/java/com/google/firebase/messaging/TopicManagementResponse.java @@ -1,11 +1,23 @@ package com.google.firebase.messaging; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import java.util.List; import java.util.Map; public class TopicManagementResponse { + private static final String UNKNOWN_ERROR = "unknown-error"; + + // Server error codes as defined in https://developers.google.com/instance-id/reference/server + // TODO: Should we handle other error codes here (e.g. PERMISSION_DENIED)? + private static final Map ERROR_CODES = ImmutableMap.builder() + .put("INVALID_ARGUMENT", "invalid-argument") + .put("NOT_FOUND", "registration-token-not-registered") + .put("INTERNAL", "internal-error") + .put("TOO_MANY_TOPICS", "too-many-topics") + .build(); + private final int successCount; private final int failureCount; private final List errors; @@ -46,7 +58,8 @@ public static class Error { private Error(int index, String reason) { this.index = index; - this.reason = reason; + this.reason = ERROR_CODES.containsKey(reason) + ? ERROR_CODES.get(reason) : UNKNOWN_ERROR; } public int getIndex() { diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java index 11204d5f0..ea5921d73 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java @@ -1,7 +1,9 @@ package com.google.firebase.messaging; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.firebase.testing.IntegrationTestUtils; import org.junit.BeforeClass; @@ -21,7 +23,6 @@ public static void setUpClass() throws Exception { @Test public void testSend() throws Exception { FirebaseMessaging messaging = FirebaseMessaging.getInstance(); - Message message = Message.builder() .setNotification(new Notification("Title", "Body")) .setAndroidConfig(AndroidConfig.builder() @@ -30,8 +31,8 @@ public void testSend() throws Exception { .setWebpushConfig(WebpushConfig.builder().putHeader("X-Custom-Val", "Foo").build()) .setTopic("foo-bar") .build(); - String resp = messaging.sendAsync(message, true).get(); - System.out.println(resp); + String id = messaging.sendAsync(message, true).get(); + assertTrue(!Strings.isNullOrEmpty(id)); } @Test diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java index c6d1d7273..c7d753ff1 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java @@ -158,11 +158,11 @@ private static void checkRequest( @Test public void testSendError() throws Exception { - for (int statusCode : HTTP_ERRORS) { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() - .setStatusCode(statusCode) - .setContent("test error"); - FirebaseMessaging messaging = initMessaging(response); + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + FirebaseMessaging messaging = initMessaging(response); + for (int code : HTTP_ERRORS) { + response.setStatusCode(code) + .setContent("{}"); TestResponseInterceptor interceptor = new TestResponseInterceptor(); messaging.setInterceptor(interceptor); try { @@ -170,6 +170,36 @@ public void testSendError() throws Exception { fail("No error thrown for HTTP error"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseMessagingException); + String msg = String.format("Unexpected HTTP response with status: %d; body: {}", code); + assertEquals(msg, e.getCause().getMessage()); + assertTrue(e.getCause().getCause() instanceof HttpResponseException); + } + + assertNotNull(interceptor.getResponse()); + HttpRequest request = interceptor.getResponse().getRequest(); + assertEquals("POST", request.getRequestMethod()); + assertEquals(TEST_FCM_URL, request.getUrl().toString()); + assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); + } + } + + @Test + public void testSendBackendError() throws Exception { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + FirebaseMessaging messaging = initMessaging(response); + for (int code : HTTP_ERRORS) { + response.setStatusCode(code).setContent( + "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\"}}"); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + messaging.setInterceptor(interceptor); + try { + messaging.sendAsync(Message.builder().setTopic("test-topic").build()).get(); + fail("No error thrown for HTTP error"); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseMessagingException); + assertEquals( + "invalid-argument", ((FirebaseMessagingException) e.getCause()).getErrorCode()); + assertEquals("test error", e.getCause().getMessage()); assertTrue(e.getCause().getCause() instanceof HttpResponseException); } @@ -178,7 +208,6 @@ public void testSendError() throws Exception { assertEquals("POST", request.getRequestMethod()); assertEquals(TEST_FCM_URL, request.getUrl().toString()); assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); - FirebaseApp.getInstance().delete(); } } @@ -219,7 +248,7 @@ public void testSubscribe() throws Exception { assertEquals(1, result.getFailureCount()); assertEquals(1, result.getErrors().size()); assertEquals(1, result.getErrors().get(0).getIndex()); - assertEquals("error_reason", result.getErrors().get(0).getReason()); + assertEquals("unknown-error", result.getErrors().get(0).getReason()); assertNotNull(interceptor.getResponse()); HttpRequest request = interceptor.getResponse().getRequest(); @@ -236,11 +265,10 @@ public void testSubscribe() throws Exception { @Test public void testSubscribeError() throws Exception { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + FirebaseMessaging messaging = initMessaging(response); for (int statusCode : HTTP_ERRORS) { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() - .setStatusCode(statusCode) - .setContent("test error"); - FirebaseMessaging messaging = initMessaging(response); + response.setStatusCode(statusCode).setContent("{\"error\": \"test error\"}"); TestResponseInterceptor interceptor = new TestResponseInterceptor(); messaging.setInterceptor(interceptor); try { @@ -248,7 +276,10 @@ public void testSubscribeError() throws Exception { fail("No error thrown for HTTP error"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseMessagingException); - assertTrue(e.getCause().getCause() instanceof HttpResponseException); + FirebaseMessagingException cause = (FirebaseMessagingException) e.getCause(); + assertEquals("internal-error", cause.getErrorCode()); + assertEquals("test error", cause.getMessage()); + assertTrue(cause.getCause() instanceof HttpResponseException); } assertNotNull(interceptor.getResponse()); @@ -256,7 +287,6 @@ public void testSubscribeError() throws Exception { assertEquals("POST", request.getRequestMethod()); assertEquals(TEST_IID_SUBSCRIBE_URL, request.getUrl().toString()); assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); - FirebaseApp.getInstance().delete(); } } @@ -297,7 +327,7 @@ public void testUnsubscribe() throws Exception { assertEquals(1, result.getFailureCount()); assertEquals(1, result.getErrors().size()); assertEquals(1, result.getErrors().get(0).getIndex()); - assertEquals("error_reason", result.getErrors().get(0).getReason()); + assertEquals("unknown-error", result.getErrors().get(0).getReason()); assertNotNull(interceptor.getResponse()); HttpRequest request = interceptor.getResponse().getRequest(); @@ -314,11 +344,10 @@ public void testUnsubscribe() throws Exception { @Test public void testUnsubscribeError() throws Exception { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + FirebaseMessaging messaging = initMessaging(response); for (int statusCode : HTTP_ERRORS) { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() - .setStatusCode(statusCode) - .setContent("test error"); - FirebaseMessaging messaging = initMessaging(response); + response.setStatusCode(statusCode).setContent("{\"error\": \"test error\"}"); TestResponseInterceptor interceptor = new TestResponseInterceptor(); messaging.setInterceptor(interceptor); try { @@ -326,6 +355,9 @@ public void testUnsubscribeError() throws Exception { fail("No error thrown for HTTP error"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseMessagingException); + FirebaseMessagingException cause = (FirebaseMessagingException) e.getCause(); + assertEquals("internal-error", cause.getErrorCode()); + assertEquals("test error", cause.getMessage()); assertTrue(e.getCause().getCause() instanceof HttpResponseException); } @@ -334,7 +366,6 @@ public void testUnsubscribeError() throws Exception { assertEquals("POST", request.getRequestMethod()); assertEquals(TEST_IID_UNSUBSCRIBE_URL, request.getUrl().toString()); assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); - FirebaseApp.getInstance().delete(); } } From 4aecab9faa1ccb6b3d78b357c9a1672359f0f5e7 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Tue, 2 Jan 2018 17:12:00 -0800 Subject: [PATCH 10/32] Added license header; Implemented topic name normalization --- .../firebase/messaging/AndroidConfig.java | 16 ++++++++++++++++ .../messaging/AndroidNotification.java | 16 ++++++++++++++++ .../google/firebase/messaging/ApnsConfig.java | 16 ++++++++++++++++ .../firebase/messaging/FirebaseMessaging.java | 19 +++++++++++++++++++ .../messaging/FirebaseMessagingException.java | 16 ++++++++++++++++ .../google/firebase/messaging/Message.java | 16 ++++++++++++++++ .../firebase/messaging/Notification.java | 16 ++++++++++++++++ .../messaging/TopicManagementResponse.java | 16 ++++++++++++++++ .../firebase/messaging/WebpushConfig.java | 16 ++++++++++++++++ .../messaging/WebpushNotification.java | 16 ++++++++++++++++ .../messaging/FirebaseMessagingIT.java | 4 ++-- .../messaging/FirebaseMessagingTest.java | 4 ++-- 12 files changed, 167 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/google/firebase/messaging/AndroidConfig.java b/src/main/java/com/google/firebase/messaging/AndroidConfig.java index 32d2dcdad..4a0897147 100644 --- a/src/main/java/com/google/firebase/messaging/AndroidConfig.java +++ b/src/main/java/com/google/firebase/messaging/AndroidConfig.java @@ -1,3 +1,19 @@ +/* + * Copyright 2017 Google Inc. + * + * 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; diff --git a/src/main/java/com/google/firebase/messaging/AndroidNotification.java b/src/main/java/com/google/firebase/messaging/AndroidNotification.java index 71794b176..aa73a2fb6 100644 --- a/src/main/java/com/google/firebase/messaging/AndroidNotification.java +++ b/src/main/java/com/google/firebase/messaging/AndroidNotification.java @@ -1,3 +1,19 @@ +/* + * Copyright 2017 Google Inc. + * + * 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; diff --git a/src/main/java/com/google/firebase/messaging/ApnsConfig.java b/src/main/java/com/google/firebase/messaging/ApnsConfig.java index 21cfd78be..2f6953c16 100644 --- a/src/main/java/com/google/firebase/messaging/ApnsConfig.java +++ b/src/main/java/com/google/firebase/messaging/ApnsConfig.java @@ -1,3 +1,19 @@ +/* + * Copyright 2017 Google Inc. + * + * 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; diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java index 6b4a36df1..7f160f6fa 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java @@ -1,3 +1,19 @@ +/* + * Copyright 2017 Google Inc. + * + * 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; @@ -193,6 +209,9 @@ private void handleSendHttpError(HttpResponseException e) throws FirebaseMessagi private TopicManagementResponse makeTopicManagementRequest(List registrationTokens, String topic, String path) throws FirebaseMessagingException { + if (!topic.startsWith("/topics/")) { + topic = "/topics/" + topic; + } Map payload = ImmutableMap.of( "to", topic, "registration_tokens", registrationTokens diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessagingException.java b/src/main/java/com/google/firebase/messaging/FirebaseMessagingException.java index ab2884c3e..1373efe47 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessagingException.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessagingException.java @@ -1,3 +1,19 @@ +/* + * Copyright 2017 Google Inc. + * + * 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; diff --git a/src/main/java/com/google/firebase/messaging/Message.java b/src/main/java/com/google/firebase/messaging/Message.java index edf92aa52..fccf37fdd 100644 --- a/src/main/java/com/google/firebase/messaging/Message.java +++ b/src/main/java/com/google/firebase/messaging/Message.java @@ -1,3 +1,19 @@ +/* + * Copyright 2017 Google Inc. + * + * 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; diff --git a/src/main/java/com/google/firebase/messaging/Notification.java b/src/main/java/com/google/firebase/messaging/Notification.java index 5613a7994..b97cad999 100644 --- a/src/main/java/com/google/firebase/messaging/Notification.java +++ b/src/main/java/com/google/firebase/messaging/Notification.java @@ -1,3 +1,19 @@ +/* + * Copyright 2017 Google Inc. + * + * 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; diff --git a/src/main/java/com/google/firebase/messaging/TopicManagementResponse.java b/src/main/java/com/google/firebase/messaging/TopicManagementResponse.java index f489d41c5..e60897ebe 100644 --- a/src/main/java/com/google/firebase/messaging/TopicManagementResponse.java +++ b/src/main/java/com/google/firebase/messaging/TopicManagementResponse.java @@ -1,3 +1,19 @@ +/* + * Copyright 2017 Google Inc. + * + * 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.common.collect.ImmutableList; diff --git a/src/main/java/com/google/firebase/messaging/WebpushConfig.java b/src/main/java/com/google/firebase/messaging/WebpushConfig.java index 6d6d7e408..601282af9 100644 --- a/src/main/java/com/google/firebase/messaging/WebpushConfig.java +++ b/src/main/java/com/google/firebase/messaging/WebpushConfig.java @@ -1,3 +1,19 @@ +/* + * Copyright 2017 Google Inc. + * + * 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; diff --git a/src/main/java/com/google/firebase/messaging/WebpushNotification.java b/src/main/java/com/google/firebase/messaging/WebpushNotification.java index c1ff4197f..1fd09e4a6 100644 --- a/src/main/java/com/google/firebase/messaging/WebpushNotification.java +++ b/src/main/java/com/google/firebase/messaging/WebpushNotification.java @@ -1,3 +1,19 @@ +/* + * Copyright 2017 Google Inc. + * + * 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; diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java index ea5921d73..7081704c3 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java @@ -39,7 +39,7 @@ public void testSend() throws Exception { public void testSubscribe() throws Exception { FirebaseMessaging messaging = FirebaseMessaging.getInstance(); TopicManagementResponse results = messaging.subscribeToTopicAsync( - ImmutableList.of(TEST_REGISTRATION_TOKEN), "/topics/mock-topic").get(); + ImmutableList.of(TEST_REGISTRATION_TOKEN), "mock-topic").get(); assertEquals(1, results.getSuccessCount() + results.getFailureCount()); } @@ -47,7 +47,7 @@ public void testSubscribe() throws Exception { public void testUnsubscribe() throws Exception { FirebaseMessaging messaging = FirebaseMessaging.getInstance(); TopicManagementResponse results = messaging.unsubscribeFromTopicAsync( - ImmutableList.of(TEST_REGISTRATION_TOKEN), "/topics/mock-topic").get(); + ImmutableList.of(TEST_REGISTRATION_TOKEN), "mock-topic").get(); assertEquals(1, results.getSuccessCount() + results.getFailureCount()); } } diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java index c7d753ff1..cd628cc79 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java @@ -259,7 +259,7 @@ public void testSubscribe() throws Exception { ByteArrayOutputStream out = new ByteArrayOutputStream(); request.getContent().writeTo(out); - assertEquals("{\"to\":\"test-topic\",\"registration_tokens\":[\"id1\",\"id2\"]}", + assertEquals("{\"to\":\"/topics/test-topic\",\"registration_tokens\":[\"id1\",\"id2\"]}", out.toString()); } @@ -338,7 +338,7 @@ public void testUnsubscribe() throws Exception { ByteArrayOutputStream out = new ByteArrayOutputStream(); request.getContent().writeTo(out); - assertEquals("{\"to\":\"test-topic\",\"registration_tokens\":[\"id1\",\"id2\"]}", + assertEquals("{\"to\":\"/topics/test-topic\",\"registration_tokens\":[\"id1\",\"id2\"]}", out.toString()); } From 5a22fcbf96e4fb63807fb3bcc94a473b9f4d06b4 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Tue, 2 Jan 2018 22:03:28 -0800 Subject: [PATCH 11/32] Improved error handling --- .../firebase/messaging/FirebaseMessaging.java | 97 +++++++++++++------ .../messaging/FirebaseMessagingTest.java | 57 +++++++++-- 2 files changed, 117 insertions(+), 37 deletions(-) diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java index 7f160f6fa..81cab1728 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java @@ -30,6 +30,7 @@ import com.google.api.client.http.json.JsonHttpContent; import com.google.api.client.json.JsonFactory; import com.google.api.client.json.JsonObjectParser; +import com.google.api.client.json.JsonParser; import com.google.api.client.util.Key; import com.google.api.core.ApiFuture; import com.google.auth.http.HttpCredentialsAdapter; @@ -52,14 +53,24 @@ public class FirebaseMessaging { private static final String FCM_URL = "https://fcm.googleapis.com/v1/projects/%s/messages:send"; private static final String INTERNAL_ERROR = "internal-error"; - private static final Map ERROR_CODES = ImmutableMap.builder() - .put("INVALID_ARGUMENT", "invalid-argument") - .put("NOT_FOUND", "registration-token-not-registered") - .put("PERMISSION_DENIED", "authentication-error") - .put("RESOURCE_EXHAUSTED", "message-rate-exceeded") - .put("UNAUTHENTICATED", "authentication-error") - .put("UNAVAILABLE", "server-unavailable") - .build(); + private static final String UNKNOWN_ERROR = "unknown-error"; + private static final Map FCM_ERROR_CODES = + ImmutableMap.builder() + .put("INVALID_ARGUMENT", "invalid-argument") + .put("NOT_FOUND", "registration-token-not-registered") + .put("PERMISSION_DENIED", "authentication-error") + .put("RESOURCE_EXHAUSTED", "message-rate-exceeded") + .put("UNAUTHENTICATED", "authentication-error") + .put("UNAVAILABLE", "server-unavailable") + .build(); + static final Map IID_ERROR_CODES = + ImmutableMap.builder() + .put(400, "invalid-argument") + .put(401, "authentication-error") + .put(403, "authentication-error") + .put(500, INTERNAL_ERROR) + .put(503, "server-unavailable") + .build(); private static final String IID_HOST = "https://iid.googleapis.com"; private static final String IID_SUBSCRIBE_PATH = "iid/v1:batchAdd"; @@ -181,7 +192,7 @@ private String makeSendRequest(Message message, return null; } catch (IOException e) { throw new FirebaseMessagingException( - INTERNAL_ERROR, "Error while calling IID backend service", e); + INTERNAL_ERROR, "Error while calling FCM backend service", e); } finally { disconnectQuietly(response); } @@ -189,22 +200,23 @@ private String makeSendRequest(Message message, private void handleSendHttpError(HttpResponseException e) throws FirebaseMessagingException { try { - Map response = jsonFactory.fromString(e.getContent(), Map.class); - Map error = (Map) response.get("error"); - if (error != null) { - String status = (String) error.get("status"); - String code = ERROR_CODES.get(status); - if (code != null) { - String message = (String) error.get("message"); - throw new FirebaseMessagingException(code, message, e); - } + MessagingServiceErrorResponse response = new MessagingServiceErrorResponse(); + JsonParser parser = jsonFactory.createJsonParser(e.getContent()); + parser.parseAndClose(response); + String status = response.getStringValue("status", null); + String code = FCM_ERROR_CODES.get(status); + if (code != null) { + String message = response.getStringValue( + "message", "Error while calling FCM backend service"); + throw new FirebaseMessagingException(code, message, e); } } catch (IOException ignored) { // ignored } - String msg = String.format( - "Unexpected HTTP response with status: %d; body: %s", e.getStatusCode(), e.getContent()); - throw new FirebaseMessagingException(INTERNAL_ERROR, msg, e); + + String msg = String.format("Unexpected HTTP response with status: %d; body: %s", + e.getStatusCode(), e.getContent()); + throw new FirebaseMessagingException(UNKNOWN_ERROR, msg, e); } private TopicManagementResponse makeTopicManagementRequest(List registrationTokens, @@ -244,18 +256,24 @@ private TopicManagementResponse makeTopicManagementRequest(List registra private void handleTopicManagementHttpError( HttpResponseException e) throws FirebaseMessagingException { - try { - Map response = jsonFactory.fromString(e.getContent(), Map.class); - String error = (String) response.get("error"); - if (!Strings.isNullOrEmpty(error)) { - throw new FirebaseMessagingException(INTERNAL_ERROR, error, e); + // Infer error code from HTTP status + String code = IID_ERROR_CODES.get(e.getStatusCode()); + if (code != null) { + try { + InstanceIdServiceErrorResponse response = new InstanceIdServiceErrorResponse(); + JsonParser parser = jsonFactory.createJsonParser(e.getContent()); + parser.parseAndClose(response); + String message = !Strings.isNullOrEmpty(response.error) + ? response.error : "Error while calling IID backend service"; + throw new FirebaseMessagingException(code, message, e); + } catch (IOException ignored) { + // ignored } - } catch (IOException ignored) { - // ignored } + String msg = String.format( "Unexpected HTTP response with status: %d; body: %s", e.getStatusCode(), e.getContent()); - throw new FirebaseMessagingException(INTERNAL_ERROR, msg, e); + throw new FirebaseMessagingException(UNKNOWN_ERROR, msg, e); } private static void disconnectQuietly(HttpResponse response) { @@ -310,8 +328,29 @@ private static class MessagingServiceResponse { private String name; } + private static class MessagingServiceErrorResponse { + @Key("error") + private Map error; + + + String getStringValue(String key, String def) { + if (error != null) { + String value = (String) error.get(key); + if (!Strings.isNullOrEmpty(value)) { + return value; + } + } + return def; + } + } + private static class InstanceIdServiceResponse { @Key("results") private List> results; } + + private static class InstanceIdServiceErrorResponse { + @Key("error") + private String error; + } } diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java index cd628cc79..e4d4ea4cd 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java @@ -211,6 +211,31 @@ public void testSendBackendError() throws Exception { } } + @Test + public void testSendBackendUnexpectedError() throws Exception { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + FirebaseMessaging messaging = initMessaging(response); + response.setStatusCode(500).setContent("not json"); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + messaging.setInterceptor(interceptor); + try { + messaging.sendAsync(Message.builder().setTopic("test-topic").build()).get(); + fail("No error thrown for HTTP error"); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseMessagingException); + assertEquals("unknown-error", ((FirebaseMessagingException) e.getCause()).getErrorCode()); + assertEquals("Unexpected HTTP response with status: 500; body: not json", + e.getCause().getMessage()); + assertTrue(e.getCause().getCause() instanceof HttpResponseException); + } + + assertNotNull(interceptor.getResponse()); + HttpRequest request = interceptor.getResponse().getRequest(); + assertEquals("POST", request.getRequestMethod()); + assertEquals(TEST_FCM_URL, request.getUrl().toString()); + assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); + } + @Test public void testInvalidSubscribe() { FirebaseOptions options = new FirebaseOptions.Builder() @@ -276,10 +301,10 @@ public void testSubscribeError() throws Exception { fail("No error thrown for HTTP error"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseMessagingException); - FirebaseMessagingException cause = (FirebaseMessagingException) e.getCause(); - assertEquals("internal-error", cause.getErrorCode()); - assertEquals("test error", cause.getMessage()); - assertTrue(cause.getCause() instanceof HttpResponseException); + FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); + assertEquals(getTopicManagementErrorCode(statusCode), error.getErrorCode()); + assertEquals(getTopicManagementErrorMessage(statusCode, "test error"), error.getMessage()); + assertTrue(error.getCause() instanceof HttpResponseException); } assertNotNull(interceptor.getResponse()); @@ -355,10 +380,10 @@ public void testUnsubscribeError() throws Exception { fail("No error thrown for HTTP error"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseMessagingException); - FirebaseMessagingException cause = (FirebaseMessagingException) e.getCause(); - assertEquals("internal-error", cause.getErrorCode()); - assertEquals("test error", cause.getMessage()); - assertTrue(e.getCause().getCause() instanceof HttpResponseException); + FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); + assertEquals(getTopicManagementErrorCode(statusCode), error.getErrorCode()); + assertEquals(getTopicManagementErrorMessage(statusCode, "test error"), error.getMessage()); + assertTrue(error.getCause() instanceof HttpResponseException); } assertNotNull(interceptor.getResponse()); @@ -369,6 +394,22 @@ public void testUnsubscribeError() throws Exception { } } + private static String getTopicManagementErrorCode(int statusCode) { + String code = FirebaseMessaging.IID_ERROR_CODES.get(statusCode); + if (code == null) { + code = "unknown-error"; + } + return code; + } + + private static String getTopicManagementErrorMessage(int statusCode, String message) { + if (FirebaseMessaging.IID_ERROR_CODES.containsKey(statusCode)) { + return message; + } + return String.format("Unexpected HTTP response with status: %d; body: {\"error\": \"%s\"}", + statusCode, message); + } + private static FirebaseMessaging initMessaging(MockLowLevelHttpResponse mockResponse) { MockHttpTransport transport = new MockHttpTransport.Builder() .setLowLevelHttpResponse(mockResponse) From b85662544ac37e3b7893082870e0a8f53559fd0f Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Tue, 2 Jan 2018 22:29:51 -0800 Subject: [PATCH 12/32] Improved error handling --- .../firebase/messaging/FirebaseMessaging.java | 62 +++++++++---------- .../messaging/FirebaseMessagingTest.java | 58 ++++------------- 2 files changed, 43 insertions(+), 77 deletions(-) diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java index 81cab1728..0ca413883 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java @@ -199,24 +199,24 @@ private String makeSendRequest(Message message, } private void handleSendHttpError(HttpResponseException e) throws FirebaseMessagingException { + MessagingServiceErrorResponse response = new MessagingServiceErrorResponse(); try { - MessagingServiceErrorResponse response = new MessagingServiceErrorResponse(); JsonParser parser = jsonFactory.createJsonParser(e.getContent()); parser.parseAndClose(response); - String status = response.getStringValue("status", null); - String code = FCM_ERROR_CODES.get(status); - if (code != null) { - String message = response.getStringValue( - "message", "Error while calling FCM backend service"); - throw new FirebaseMessagingException(code, message, e); - } } catch (IOException ignored) { // ignored } - String msg = String.format("Unexpected HTTP response with status: %d; body: %s", - e.getStatusCode(), e.getContent()); - throw new FirebaseMessagingException(UNKNOWN_ERROR, msg, e); + String code = FCM_ERROR_CODES.get(response.getString("status")); + if (code == null) { + code = UNKNOWN_ERROR; + } + String msg = response.getString("message"); + if (Strings.isNullOrEmpty(msg)) { + msg = String.format("Unexpected HTTP response with status: %d; body: %s", + e.getStatusCode(), e.getContent()); + } + throw new FirebaseMessagingException(code, msg, e); } private TopicManagementResponse makeTopicManagementRequest(List registrationTokens, @@ -256,24 +256,25 @@ private TopicManagementResponse makeTopicManagementRequest(List registra private void handleTopicManagementHttpError( HttpResponseException e) throws FirebaseMessagingException { + InstanceIdServiceErrorResponse response = new InstanceIdServiceErrorResponse(); + try { + JsonParser parser = jsonFactory.createJsonParser(e.getContent()); + parser.parseAndClose(response); + } catch (IOException ignored) { + // ignored + } + // Infer error code from HTTP status String code = IID_ERROR_CODES.get(e.getStatusCode()); - if (code != null) { - try { - InstanceIdServiceErrorResponse response = new InstanceIdServiceErrorResponse(); - JsonParser parser = jsonFactory.createJsonParser(e.getContent()); - parser.parseAndClose(response); - String message = !Strings.isNullOrEmpty(response.error) - ? response.error : "Error while calling IID backend service"; - throw new FirebaseMessagingException(code, message, e); - } catch (IOException ignored) { - // ignored - } + if (code == null) { + code = UNKNOWN_ERROR; } - - String msg = String.format( - "Unexpected HTTP response with status: %d; body: %s", e.getStatusCode(), e.getContent()); - throw new FirebaseMessagingException(UNKNOWN_ERROR, msg, e); + String msg = response.error; + if (Strings.isNullOrEmpty(msg)) { + msg = String.format("Unexpected HTTP response with status: %d; body: %s", + e.getStatusCode(), e.getContent()); + } + throw new FirebaseMessagingException(code, msg, e); } private static void disconnectQuietly(HttpResponse response) { @@ -333,14 +334,11 @@ private static class MessagingServiceErrorResponse { private Map error; - String getStringValue(String key, String def) { + String getString(String key) { if (error != null) { - String value = (String) error.get(key); - if (!Strings.isNullOrEmpty(value)) { - return value; - } + return (String) error.get(key); } - return def; + return null; } } diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java index e4d4ea4cd..47f0741f9 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java @@ -161,8 +161,7 @@ public void testSendError() throws Exception { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); FirebaseMessaging messaging = initMessaging(response); for (int code : HTTP_ERRORS) { - response.setStatusCode(code) - .setContent("{}"); + response.setStatusCode(code).setContent("{}"); TestResponseInterceptor interceptor = new TestResponseInterceptor(); messaging.setInterceptor(interceptor); try { @@ -170,9 +169,11 @@ public void testSendError() throws Exception { fail("No error thrown for HTTP error"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseMessagingException); - String msg = String.format("Unexpected HTTP response with status: %d; body: {}", code); - assertEquals(msg, e.getCause().getMessage()); - assertTrue(e.getCause().getCause() instanceof HttpResponseException); + FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); + assertEquals("unknown-error", error.getErrorCode()); + assertEquals("Unexpected HTTP response with status: " + code + "; body: {}", + error.getMessage()); + assertTrue(error.getCause() instanceof HttpResponseException); } assertNotNull(interceptor.getResponse()); @@ -184,7 +185,7 @@ public void testSendError() throws Exception { } @Test - public void testSendBackendError() throws Exception { + public void testSendErrorWithDetails() throws Exception { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); FirebaseMessaging messaging = initMessaging(response); for (int code : HTTP_ERRORS) { @@ -197,10 +198,10 @@ public void testSendBackendError() throws Exception { fail("No error thrown for HTTP error"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseMessagingException); - assertEquals( - "invalid-argument", ((FirebaseMessagingException) e.getCause()).getErrorCode()); - assertEquals("test error", e.getCause().getMessage()); - assertTrue(e.getCause().getCause() instanceof HttpResponseException); + FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); + assertEquals("invalid-argument", error.getErrorCode()); + assertEquals("test error", error.getMessage()); + assertTrue(error.getCause() instanceof HttpResponseException); } assertNotNull(interceptor.getResponse()); @@ -211,31 +212,6 @@ public void testSendBackendError() throws Exception { } } - @Test - public void testSendBackendUnexpectedError() throws Exception { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - FirebaseMessaging messaging = initMessaging(response); - response.setStatusCode(500).setContent("not json"); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - messaging.setInterceptor(interceptor); - try { - messaging.sendAsync(Message.builder().setTopic("test-topic").build()).get(); - fail("No error thrown for HTTP error"); - } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseMessagingException); - assertEquals("unknown-error", ((FirebaseMessagingException) e.getCause()).getErrorCode()); - assertEquals("Unexpected HTTP response with status: 500; body: not json", - e.getCause().getMessage()); - assertTrue(e.getCause().getCause() instanceof HttpResponseException); - } - - assertNotNull(interceptor.getResponse()); - HttpRequest request = interceptor.getResponse().getRequest(); - assertEquals("POST", request.getRequestMethod()); - assertEquals(TEST_FCM_URL, request.getUrl().toString()); - assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); - } - @Test public void testInvalidSubscribe() { FirebaseOptions options = new FirebaseOptions.Builder() @@ -303,7 +279,7 @@ public void testSubscribeError() throws Exception { assertTrue(e.getCause() instanceof FirebaseMessagingException); FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); assertEquals(getTopicManagementErrorCode(statusCode), error.getErrorCode()); - assertEquals(getTopicManagementErrorMessage(statusCode, "test error"), error.getMessage()); + assertEquals("test error", error.getMessage()); assertTrue(error.getCause() instanceof HttpResponseException); } @@ -382,7 +358,7 @@ public void testUnsubscribeError() throws Exception { assertTrue(e.getCause() instanceof FirebaseMessagingException); FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); assertEquals(getTopicManagementErrorCode(statusCode), error.getErrorCode()); - assertEquals(getTopicManagementErrorMessage(statusCode, "test error"), error.getMessage()); + assertEquals("test error", error.getMessage()); assertTrue(error.getCause() instanceof HttpResponseException); } @@ -402,14 +378,6 @@ private static String getTopicManagementErrorCode(int statusCode) { return code; } - private static String getTopicManagementErrorMessage(int statusCode, String message) { - if (FirebaseMessaging.IID_ERROR_CODES.containsKey(statusCode)) { - return message; - } - return String.format("Unexpected HTTP response with status: %d; body: {\"error\": \"%s\"}", - statusCode, message); - } - private static FirebaseMessaging initMessaging(MockLowLevelHttpResponse mockResponse) { MockHttpTransport transport = new MockHttpTransport.Builder() .setLowLevelHttpResponse(mockResponse) From 84f990838ea9026e72d87ab383571722ce02cfd7 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Tue, 16 Jan 2018 21:15:23 -0800 Subject: [PATCH 13/32] Added javadocs --- .../firebase/messaging/AndroidConfig.java | 81 ++++++++++++- .../messaging/AndroidNotification.java | 113 +++++++++++++++++- .../google/firebase/messaging/ApnsConfig.java | 39 +++++- .../firebase/messaging/FirebaseMessaging.java | 33 ++++- .../google/firebase/messaging/Message.java | 84 ++++++++++++- .../firebase/messaging/Notification.java | 10 ++ .../firebase/messaging/WebpushConfig.java | 56 ++++++++- .../messaging/WebpushNotification.java | 22 +++- 8 files changed, 421 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/google/firebase/messaging/AndroidConfig.java b/src/main/java/com/google/firebase/messaging/AndroidConfig.java index 4a0897147..248718634 100644 --- a/src/main/java/com/google/firebase/messaging/AndroidConfig.java +++ b/src/main/java/com/google/firebase/messaging/AndroidConfig.java @@ -20,9 +20,14 @@ import com.google.api.client.util.Key; import com.google.common.collect.ImmutableMap; +import com.google.firebase.internal.NonNull; import java.util.HashMap; import java.util.Map; +/** + * Represents the Android-specific options that can be included in a {@link Message}. + * Instances of this class are thread-safe and immutable. + */ public class AndroidConfig { @Key("collapse_key") @@ -66,11 +71,19 @@ private AndroidConfig(Builder builder) { this.notification = builder.notification; } + /** + * Priority levels that can be set on an {@link AndroidConfig}. + */ public enum Priority { high, normal, } + /** + * Creates a new {@link AndroidConfig.Builder}. + * + * @return A {@link AndroidConfig.Builder} instance. + */ public static Builder builder() { return new Builder(); } @@ -88,41 +101,103 @@ private Builder() { } + /** + * Sets a collapse key for the message. Collapse key serves as an identifier for a group of + * messages that can be collapsed, so that only the last message gets sent when delivery can be + * resumed. A maximum of 4 different collapse keys is allowed at any given time. + * + * @param collapseKey A collapse key string. + * @return This builder. + */ public Builder setCollapseKey(String collapseKey) { this.collapseKey = collapseKey; return this; } + /** + * Sets the priority of the message. + * + * @param priority A value from the {@link Priority} enum. + * @return This builder. + */ public Builder setPriority(Priority priority) { this.priority = priority; return this; } + /** + * Sets the time-to-live duration of the message. This indicates how long (in seconds) + * the message should be kept in FCM storage if the target device is offline. Set to 0 to + * send the message immediately. The duration must be encoded as a string, where the + * string ends in the suffix "s" (indicating seconds) and is preceded by the number of seconds, + * with nanoseconds expressed as fractional seconds. For example, 3 seconds with 0 nanoseconds + * should be encoded as {@code "3s"}, while 3 seconds and 1 nanosecond should be + * expressed as {@code "3.000000001s"}. + * + * @param ttl Time-to-live duration encoded as a string with suffix {@code "s"}. + * @return This builder. + */ public Builder setTtl(String ttl) { this.ttl = ttl; return this; } + /** + * Sets the package name of the application where the registration tokens must match in order + * to receive the message. + * + * @param restrictedPackageName A package name string. + * @return This builder. + */ public Builder setRestrictedPackageName(String restrictedPackageName) { this.restrictedPackageName = restrictedPackageName; return this; } - public Builder putData(String key, String value) { + /** + * Adds the given key-value pair to the message as a data field. Key or the value may not be + * null. When set, overrides any data fields set using the methods + * {@link Message.Builder#putData(String, String)} and {@link Message.Builder#putAllData(Map)}. + * + * @param key Name of the data field. Must not be null. + * @param value Value of the data field. Must not be null. + * @return This builder. + */ + public Builder putData(@NonNull String key, @NonNull String value) { this.data.put(key, value); return this; } - public Builder putAllData(Map data) { - this.data.putAll(data); + /** + * Adds all the key-value pairs in the given map to the message as data fields. None of the + * keys or values may be null. When set, overrides any data fields set using the methods + * {@link Message.Builder#putData(String, String)} and {@link Message.Builder#putAllData(Map)}. + * + * @param map A non-null map of data fields. Map must not contain null keys or values. + * @return This builder. + */ + public Builder putAllData(@NonNull Map map) { + this.data.putAll(map); return this; } + /** + * Sets the Android notification to be included in the message. + * + * @param notification An {@link AndroidNotification} instance. + * @return This builder. + */ public Builder setNotification(AndroidNotification notification) { this.notification = notification; return this; } + /** + * Creates a new {@link AndroidConfig} instance from the parameters set on this builder. + * + * @return A new {@link AndroidConfig} instance. + * @throws IllegalArgumentException If any of the parameters set on the builder are invalid. + */ public AndroidConfig build() { return new AndroidConfig(this); } diff --git a/src/main/java/com/google/firebase/messaging/AndroidNotification.java b/src/main/java/com/google/firebase/messaging/AndroidNotification.java index aa73a2fb6..0a26d711a 100644 --- a/src/main/java/com/google/firebase/messaging/AndroidNotification.java +++ b/src/main/java/com/google/firebase/messaging/AndroidNotification.java @@ -21,9 +21,14 @@ import com.google.api.client.util.Key; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; +import com.google.firebase.internal.NonNull; import java.util.ArrayList; import java.util.List; +/** + * Represents the Android-specific notification options that can be included in a {@link Message}. + * Instances of this class are thread-safe and immutable. + */ public class AndroidNotification { @Key("title") @@ -90,6 +95,11 @@ private AndroidNotification(Builder builder) { } } + /** + * Creates a new {@link AndroidNotification.Builder}. + * + * @return A {@link AndroidNotification.Builder} instance. + */ public static Builder builder() { return new Builder(); } @@ -111,75 +121,168 @@ public static class Builder { private Builder() { } + /** + * Sets the title of the Android notification. When provided, overrides the title sent + * via {@link Notification}. + * + * @param title Title of the notification. + * @return This builder. + */ public Builder setTitle(String title) { this.title = title; return this; } + /** + * Sets the body of the Android notification. When provided, overrides the body sent + * via {@link Notification}. + * + * @param body Body of the notification. + * @return This builder. + */ public Builder setBody(String body) { this.body = body; return this; } + /** + * Sets the icon of the Android notification. + * + * @param icon Icon resource for the notification. + * @return This builder. + */ public Builder setIcon(String icon) { this.icon = icon; return this; } + /** + * Sets the notification icon color. + * + * @param color Color specified in the {@code #rrggbb} format. + * @return This builder. + */ public Builder setColor(String color) { this.color = color; return this; } + /** + * Sets the sound to be played when the device receives the notification. + * + * @param sound File name of the sound resource or "default". + * @return This builder. + */ public Builder setSound(String sound) { this.sound = sound; return this; } + /** + * Sets the notification tag. This is an identifier used to replace existing notifications in + * the notification drawer. If not specified, each request creates a new notification. + * + * @param tag Notification tag. + * @return This builder. + */ public Builder setTag(String tag) { this.tag = tag; return this; } + /** + * Sets the action associated with a user click on the notification. If specified, an activity + * with a matching intent filter is launched when a user clicks on the notification. + * + * @param clickAction Click action name. + * @return This builder. + */ public Builder setClickAction(String clickAction) { this.clickAction = clickAction; return this; } + /** + * Sets the key to the body string in the app's string resources to use to localize the body + * text to the user's current localization. + * + * @param bodyLocKey Resource key string. + * @return This builder. + */ public Builder setBodyLocKey(String bodyLocKey) { this.bodyLocKey = bodyLocKey; return this; } - public Builder addBodyLocArg(String arg) { + /** + * Adds a resource key string that will be used in place of the format specifiers in + * {@code bodyLocKey}. + * + * @param arg Resource key string. + * @return This builder. + */ + public Builder addBodyLocArg(@NonNull String arg) { this.bodyLocArgs.add(arg); return this; } - public Builder addAllBodyLocArgs(List args) { + /** + * Adds a list of resource keys that will be used in place of the format specifiers in + * {@code bodyLocKey}. + * + * @param args List of resource key strings. + * @return This builder. + */ + public Builder addAllBodyLocArgs(@NonNull List args) { this.bodyLocArgs.addAll(args); return this; } + /** + * Sets the key to the title string in the app's string resources to use to localize the title + * text to the user's current localization. + * + * @param titleLocKey Resource key string. + * @return This builder. + */ public Builder setTitleLocKey(String titleLocKey) { this.titleLocKey = titleLocKey; return this; } - public Builder addTitleLocArg(String arg) { + /** + * Adds a resource key string that will be used in place of the format specifiers in + * {@code titleLocKey}. + * + * @param arg Resource key string. + * @return This builder. + */ + public Builder addTitleLocArg(@NonNull String arg) { this.titleLocArgs.add(arg); return this; } - public Builder addAllTitleLocArgs(List args) { + /** + * Adds a list of resource keys that will be used in place of the format specifiers in + * {@code titleLocKey}. + * + * @param args List of resource key strings. + * @return This builder. + */ + public Builder addAllTitleLocArgs(@NonNull List args) { this.titleLocArgs.addAll(args); return this; } + /** + * Creates a new {@link AndroidNotification} instance from the parameters set on this builder. + * + * @return A new {@link AndroidNotification} instance. + * @throws IllegalArgumentException If any of the parameters set on the builder are invalid. + */ public AndroidNotification build() { return new AndroidNotification(this); } - } } diff --git a/src/main/java/com/google/firebase/messaging/ApnsConfig.java b/src/main/java/com/google/firebase/messaging/ApnsConfig.java index 2f6953c16..9b4d74eec 100644 --- a/src/main/java/com/google/firebase/messaging/ApnsConfig.java +++ b/src/main/java/com/google/firebase/messaging/ApnsConfig.java @@ -18,9 +18,14 @@ import com.google.api.client.util.Key; import com.google.common.collect.ImmutableMap; +import com.google.firebase.internal.NonNull; import java.util.HashMap; import java.util.Map; +/** + * Represents the APNS-specific options that can be included in a {@link Message}. + * Instances of this class are thread-safe and immutable. + */ public class ApnsConfig { @Key("headers") @@ -35,6 +40,11 @@ private ApnsConfig(Builder builder) { ? null : ImmutableMap.copyOf(builder.payload); } + /** + * Creates a new {@link ApnsConfig.Builder}. + * + * @return A {@link ApnsConfig.Builder} instance. + */ public static Builder builder() { return new Builder(); } @@ -44,21 +54,46 @@ public static class Builder { private final Map headers = new HashMap<>(); private Map payload; - public Builder putHeader(String key, String value) { + /** + * Sets the given key-value pair as an APNS header. + * + * @param key Name of the header field. Must not be null. + * @param value Value of the header field. Must not be null. + * @return This builder. + */ + public Builder putHeader(@NonNull String key, @NonNull String value) { headers.put(key, value); return this; } - public Builder putAllHeaders(Map map) { + /** + * Adds all the key-value pairs in the given map as APNS headers. + * + * @param map A non-null map of headers. Map must not contain null keys or values. + * @return This builder. + */ + public Builder putAllHeaders(@NonNull Map map) { headers.putAll(map); return this; } + /** + * Sets APNS payload as JSON-serializable map. + * + * @param payload Map containing both aps dictionary and custom payload. + * @return This builder. + */ public Builder setPayload(Map payload) { this.payload = payload; return this; } + /** + * Creates a new {@link ApnsConfig} instance from the parameters set on this builder. + * + * @return A new {@link ApnsConfig} instance. + * @throws IllegalArgumentException If any of the parameters set on the builder are invalid. + */ public ApnsConfig build() { return new ApnsConfig(this); } diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java index 0ca413883..f59dfabca 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java @@ -41,6 +41,7 @@ import com.google.firebase.FirebaseApp; import com.google.firebase.ImplFirebaseTrampolines; import com.google.firebase.internal.FirebaseService; +import com.google.firebase.internal.NonNull; import com.google.firebase.internal.TaskToApiFuture; import com.google.firebase.tasks.Task; import java.io.IOException; @@ -48,6 +49,12 @@ import java.util.Map; import java.util.concurrent.Callable; +/** + * This class is the entry point for all server-side Firebase Cloud Messaging actions. + * + *

You can get an instance of FirebaseMessaging via {@link #getInstance(FirebaseApp)}, and + * then use it to send messages or manage FCM topic subscriptions. + */ public class FirebaseMessaging { private static final String FCM_URL = "https://fcm.googleapis.com/v1/projects/%s/messages:send"; @@ -98,6 +105,11 @@ private FirebaseMessaging(FirebaseApp app) { this.url = String.format(FCM_URL, projectId); } + /** + * Gets the {@link FirebaseMessaging} instance for the default {@link FirebaseApp}. + * + * @return The {@link FirebaseMessaging} instance for the default {@link FirebaseApp}. + */ public static FirebaseMessaging getInstance() { return getInstance(FirebaseApp.getInstance()); } @@ -116,11 +128,28 @@ public static synchronized FirebaseMessaging getInstance(FirebaseApp app) { return service.getInstance(); } - public ApiFuture sendAsync(Message message) { + /** + * Sends the given {@link Message} via Firebase Cloud Messaging. + * + * @param message A non-null {@link Message} to be sent. + * @return An {@code ApiFuture} that will complete with a message ID string when the message + * has been sent. + */ + public ApiFuture sendAsync(@NonNull Message message) { return sendAsync(message, false); } - public ApiFuture sendAsync(Message message, boolean dryRun) { + /** + * Sends the given {@link Message} via Firebase Cloud Messaging. If the {@code dryRun} option + * is set to true, this will not actually send the message. Instead it will perform all the + * necessary validations, and emulate the send operation. + * + * @param message A non-null {@link Message} to be sent. + * @param dryRun a boolean indicating whether to perform a dry run (validation only) of the send. + * @return An {@code ApiFuture} that will complete with a message ID string when the message + * has been sent, or when the emulation has finished. + */ + public ApiFuture sendAsync(@NonNull Message message, boolean dryRun) { return new TaskToApiFuture<>(send(message, dryRun)); } diff --git a/src/main/java/com/google/firebase/messaging/Message.java b/src/main/java/com/google/firebase/messaging/Message.java index fccf37fdd..23eb4a866 100644 --- a/src/main/java/com/google/firebase/messaging/Message.java +++ b/src/main/java/com/google/firebase/messaging/Message.java @@ -22,9 +22,20 @@ import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; import com.google.common.primitives.Booleans; +import com.google.firebase.internal.NonNull; import java.util.HashMap; import java.util.Map; +/** + * Represents a message that can be sent via Firebase Cloud Messaging (FCM). Contains payload + * information as well as the recipient information. In particular, the message must contain + * exactly one of token, topic or condition parameters. Instances of this class are thread-safe + * and immutable. Use {@link Message.Builder} co crete new instances. + * + * @see + * FCM message + * format + */ public class Message { @Key("data") @@ -73,6 +84,11 @@ private Message(Builder builder) { this.condition = builder.condition; } + /** + * Creates a new {@link Message.Builder}. + * + * @return A {@link Message.Builder} instance. + */ public static Builder builder() { return new Builder(); } @@ -92,51 +108,115 @@ private Builder() { } + /** + * Sets the notification information to be included in the message. + * + * @param notification A {@link Notification} instance. + * @return This builder. + */ public Builder setNotification(Notification notification) { this.notification = notification; return this; } + /** + * Sets the Android-specific information to be included in the message. + * + * @param androidConfig An {@link AndroidConfig} instance. + * @return This builder. + */ public Builder setAndroidConfig(AndroidConfig androidConfig) { this.androidConfig = androidConfig; return this; } + /** + * Sets the Webpush-specific information to be included in the message. + * + * @param webpushConfig A {@link WebpushConfig} instance. + * @return This builder. + */ public Builder setWebpushConfig(WebpushConfig webpushConfig) { this.webpushConfig = webpushConfig; return this; } + /** + * Sets the information specific to APNS (Apple Push Notification Service). + * + * @param apnsConfig An {@link ApnsConfig} instance. + * @return This builder. + */ public Builder setApnsConfig(ApnsConfig apnsConfig) { this.apnsConfig = apnsConfig; return this; } + /** + * Sets the registration token of the device to which the message should be sent. + * + * @param token A valid device registration token. + * @return This builder. + */ public Builder setToken(String token) { this.token = token; return this; } + /** + * Sets the name of the FCM topic to which the message should be sent. Topic names must + * not contain the {@code /topics/} prefix. + * + * @param topic A valid topic name excluding the {@code /topics/} prefix. + * @return This builder. + */ public Builder setTopic(String topic) { this.topic = topic; return this; } + /** + * Sets the FCM condition to which the message should be sent. + * + * @param condition A valid condition string (e.g. {@code "'foo' in topics"}). + * @return This builder. + */ public Builder setCondition(String condition) { this.condition = condition; return this; } - public Builder putData(String key, String value) { + /** + * Adds the given key-value pair to the message as a data field. Key or the value may not be + * null. + * + * @param key Name of the data field. Must not be null. + * @param value Value of the data field. Must not be null. + * @return This builder. + */ + public Builder putData(@NonNull String key, @NonNull String value) { this.data.put(key, value); return this; } - public Builder putAllData(Map map) { + /** + * Adds all the key-value pairs in the given map to the message as data fields. None of the + * keys or values may be null. + * + * @param map A non-null map of data fields. Map must not contain null keys or values. + * @return This builder. + */ + public Builder putAllData(@NonNull Map map) { this.data.putAll(map); return this; } + /** + * Creates a new {@link Message} instance from the parameters set on this builder. + * + * @return A new {@link Message} instance. + * @throws IllegalArgumentException If any of the parameters set on the builder are invalid. + */ public Message build() { return new Message(this); } diff --git a/src/main/java/com/google/firebase/messaging/Notification.java b/src/main/java/com/google/firebase/messaging/Notification.java index b97cad999..5cfc5ecc4 100644 --- a/src/main/java/com/google/firebase/messaging/Notification.java +++ b/src/main/java/com/google/firebase/messaging/Notification.java @@ -18,6 +18,10 @@ import com.google.api.client.util.Key; +/** + * Represents the notification parameters that can be included in a {@link Message}. Instances + * of this class are thread-safe and immutable. + */ public class Notification { @Key("title") @@ -26,6 +30,12 @@ public class Notification { @Key("body") private final String body; + /** + * Creates a new {@code Notification} using the given title and body. + * + * @param title Title of the notification. + * @param body Body of the notification. + */ public Notification(String title, String body) { this.title = title; this.body = body; diff --git a/src/main/java/com/google/firebase/messaging/WebpushConfig.java b/src/main/java/com/google/firebase/messaging/WebpushConfig.java index 601282af9..f364ae123 100644 --- a/src/main/java/com/google/firebase/messaging/WebpushConfig.java +++ b/src/main/java/com/google/firebase/messaging/WebpushConfig.java @@ -18,9 +18,14 @@ import com.google.api.client.util.Key; import com.google.common.collect.ImmutableMap; +import com.google.firebase.internal.NonNull; import java.util.HashMap; import java.util.Map; +/** + * Represents the Webpush protocol options that can be included in a {@link Message}. Instances + * of this class are thread-safe and immutable. + */ public class WebpushConfig { @Key("headers") @@ -38,6 +43,11 @@ private WebpushConfig(Builder builder) { this.notification = builder.notification; } + /** + * Creates a new {@link WebpushConfig.Builder}. + * + * @return A {@link WebpushConfig.Builder} instance. + */ public static Builder builder() { return new Builder(); } @@ -48,31 +58,73 @@ public static class Builder { private final Map data = new HashMap<>(); private WebpushNotification notification; - public Builder putHeader(String key, String value) { + /** + * Adds the given key-value pair as a Webpush HTTP header. Refer to + * Webpush specification + * for supported headers. + * + * @param key Name of the header. Must not be null. + * @param value Value of the header. Must not be null. + * @return This builder. + */ + public Builder putHeader(@NonNull String key, @NonNull String value) { headers.put(key, value); return this; } - public Builder putAllHeaders(Map map) { + /** + * Adds all the key-value pairs in the given map as Webpush headers. Refer to + * Webpush specification + * for supported headers. + * + * @param map A non-null map of header values. Map must not contain null keys or values. + * @return This builder. + */ + public Builder putAllHeaders(@NonNull Map map) { headers.putAll(map); return this; } + /** + * Sets the given key-value pair as a Webpush data field. + * + * @param key Name of the data field. Must not be null. + * @param value Value of the data field. Must not be null. + * @return This builder. + */ public Builder putData(String key, String value) { data.put(key, value); return this; } + /** + * Adds all the key-value pairs in the given map as Webpush data fields. + * + * @param map A non-null map of data values. Map must not contain null keys or values. + * @return This builder. + */ public Builder putAllData(Map map) { data.putAll(map); return this; } + /** + * Sets the Webpush notification to be included in the message. + * + * @param notification A {@link WebpushNotification} instance. + * @return This builder. + */ public Builder setNotification(WebpushNotification notification) { this.notification = notification; return this; } + /** + * Creates a new {@link WebpushConfig} instance from the parameters set on this builder. + * + * @return A new {@link WebpushConfig} instance. + * @throws IllegalArgumentException If any of the parameters set on the builder are invalid. + */ public WebpushConfig build() { return new WebpushConfig(this); } diff --git a/src/main/java/com/google/firebase/messaging/WebpushNotification.java b/src/main/java/com/google/firebase/messaging/WebpushNotification.java index 1fd09e4a6..8377b2435 100644 --- a/src/main/java/com/google/firebase/messaging/WebpushNotification.java +++ b/src/main/java/com/google/firebase/messaging/WebpushNotification.java @@ -17,7 +17,12 @@ package com.google.firebase.messaging; import com.google.api.client.util.Key; +import com.google.firebase.internal.Nullable; +/** + * Represents the Webpush-specific notification options that can be included in a {@link Message}. + * Instances of this class are thread-safe and immutable. + */ public class WebpushNotification { @Key("title") @@ -29,11 +34,26 @@ public class WebpushNotification { @Key("icon") private final String icon; + /** + * Creates a new notification with the given title and body. Overrides the options set via + * {@link Notification}. + * + * @param title Title of the notification. + * @param body Body of the notification. + */ public WebpushNotification(String title, String body) { this(title, body, null); } - public WebpushNotification(String title, String body, String icon) { + /** + * Creates a new notification with the given title, body and icon. Overrides the options set via + * {@link Notification}. + * + * @param title Title of the notification. + * @param body Body of the notification. + * @param icon URL to the notifications icon. + */ + public WebpushNotification(String title, String body, @Nullable String icon) { this.title = title; this.body = body; this.icon = icon; From 13ae5565b5c52cb03ec1e76bf39ad7814d4efd50 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Tue, 16 Jan 2018 21:19:47 -0800 Subject: [PATCH 14/32] Fixing auth (ADC) test failure --- .../firebase/auth/FirebaseAuthTest.java | 33 +------------------ 1 file changed, 1 insertion(+), 32 deletions(-) diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java index 49f5ef9e7..e2c709ccd 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java @@ -16,7 +16,6 @@ package com.google.firebase.auth; -import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotSame; @@ -37,7 +36,6 @@ import com.google.auth.oauth2.UserCredentials; import com.google.common.base.Defaults; import com.google.common.base.Strings; -import com.google.common.collect.ImmutableMap; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.ImplFirebaseTrampolines; @@ -47,14 +45,11 @@ import com.google.firebase.testing.ServiceAccount; import com.google.firebase.testing.TestUtils; import java.io.ByteArrayInputStream; -import java.io.File; import java.io.IOException; import java.io.InputStream; -import java.io.PrintWriter; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; -import java.nio.file.Files; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -107,7 +102,7 @@ public static Collection data() throws Exception { }, { new FirebaseOptions.Builder() - .setCredentials(createApplicationDefaultCredential()) + .setCredentials(TestUtils.getApplicationDefaultCredentials()) .build(), /* isCertCredential */ false }, @@ -124,32 +119,6 @@ public static Collection data() throws Exception { }); } - private static GoogleCredentials createApplicationDefaultCredential() throws IOException { - final MockTokenServerTransport transport = new MockTokenServerTransport(); - transport.addServiceAccount(ServiceAccount.EDITOR.getEmail(), ACCESS_TOKEN); - - // Set the GOOGLE_APPLICATION_CREDENTIALS environment variable for application-default - // credentials. This requires us to write the credentials to the location specified by the - // environment variable. - File credentialsFile = File.createTempFile("google-test-credentials", "json"); - PrintWriter writer = new PrintWriter(Files.newBufferedWriter(credentialsFile.toPath(), UTF_8)); - writer.print(ServiceAccount.EDITOR.asString()); - writer.close(); - Map environmentVariables = - ImmutableMap.builder() - .put("GOOGLE_APPLICATION_CREDENTIALS", credentialsFile.getAbsolutePath()) - .build(); - TestUtils.setEnvironmentVariables(environmentVariables); - credentialsFile.deleteOnExit(); - - return GoogleCredentials.getApplicationDefault(new HttpTransportFactory() { - @Override - public HttpTransport create() { - return transport; - } - }); - } - private static GoogleCredentials createRefreshTokenCredential() throws IOException { final MockTokenServerTransport transport = new MockTokenServerTransport(); From c2d8462a1dff6fbb6eba40561b8979f716f8e2bf Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Wed, 17 Jan 2018 11:10:35 -0800 Subject: [PATCH 15/32] Updated documentation --- .../firebase/messaging/AndroidConfig.java | 2 +- .../messaging/AndroidNotification.java | 8 ++--- .../google/firebase/messaging/ApnsConfig.java | 6 ++-- .../firebase/messaging/FirebaseMessaging.java | 4 +-- .../messaging/TopicManagementResponse.java | 35 +++++++++++++++++++ 5 files changed, 46 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/google/firebase/messaging/AndroidConfig.java b/src/main/java/com/google/firebase/messaging/AndroidConfig.java index 248718634..4d4a6730c 100644 --- a/src/main/java/com/google/firebase/messaging/AndroidConfig.java +++ b/src/main/java/com/google/firebase/messaging/AndroidConfig.java @@ -104,7 +104,7 @@ private Builder() { /** * Sets a collapse key for the message. Collapse key serves as an identifier for a group of * messages that can be collapsed, so that only the last message gets sent when delivery can be - * resumed. A maximum of 4 different collapse keys is allowed at any given time. + * resumed. A maximum of 4 different collapse keys may be active at any given time. * * @param collapseKey A collapse key string. * @return This builder. diff --git a/src/main/java/com/google/firebase/messaging/AndroidNotification.java b/src/main/java/com/google/firebase/messaging/AndroidNotification.java index 0a26d711a..2293adb4b 100644 --- a/src/main/java/com/google/firebase/messaging/AndroidNotification.java +++ b/src/main/java/com/google/firebase/messaging/AndroidNotification.java @@ -203,8 +203,8 @@ public Builder setClickAction(String clickAction) { } /** - * Sets the key to the body string in the app's string resources to use to localize the body - * text to the user's current localization. + * Sets the key of the body string in the app's string resources to use to localize the body + * text. * * @param bodyLocKey Resource key string. * @return This builder. @@ -239,8 +239,8 @@ public Builder addAllBodyLocArgs(@NonNull List args) { } /** - * Sets the key to the title string in the app's string resources to use to localize the title - * text to the user's current localization. + * Sets the key of the title string in the app's string resources to use to localize the title + * text. * * @param titleLocKey Resource key string. * @return This builder. diff --git a/src/main/java/com/google/firebase/messaging/ApnsConfig.java b/src/main/java/com/google/firebase/messaging/ApnsConfig.java index 9b4d74eec..6e0ebe1a1 100644 --- a/src/main/java/com/google/firebase/messaging/ApnsConfig.java +++ b/src/main/java/com/google/firebase/messaging/ApnsConfig.java @@ -24,7 +24,9 @@ /** * Represents the APNS-specific options that can be included in a {@link Message}. - * Instances of this class are thread-safe and immutable. + * Instances of this class are thread-safe and immutable. Refer to + * + * Apple documentation for various headers and payload fields supported by APNS. */ public class ApnsConfig { @@ -78,7 +80,7 @@ public Builder putAllHeaders(@NonNull Map map) { } /** - * Sets APNS payload as JSON-serializable map. + * Sets the APNS payload as a JSON-serializable map. * * @param payload Map containing both aps dictionary and custom payload. * @return This builder. diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java index f59dfabca..d330b09bf 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java @@ -141,8 +141,8 @@ public ApiFuture sendAsync(@NonNull Message message) { /** * Sends the given {@link Message} via Firebase Cloud Messaging. If the {@code dryRun} option - * is set to true, this will not actually send the message. Instead it will perform all the - * necessary validations, and emulate the send operation. + * is set to true, the message will not be actually sent. Instead FCM performs all the + * necessary validations, and emulates the send operation. * * @param message A non-null {@link Message} to be sent. * @param dryRun a boolean indicating whether to perform a dry run (validation only) of the send. diff --git a/src/main/java/com/google/firebase/messaging/TopicManagementResponse.java b/src/main/java/com/google/firebase/messaging/TopicManagementResponse.java index e60897ebe..85304c344 100644 --- a/src/main/java/com/google/firebase/messaging/TopicManagementResponse.java +++ b/src/main/java/com/google/firebase/messaging/TopicManagementResponse.java @@ -18,9 +18,13 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.firebase.internal.NonNull; import java.util.List; import java.util.Map; +/** + * The response produced by FCM topic management operations. + */ public class TopicManagementResponse { private static final String UNKNOWN_ERROR = "unknown-error"; @@ -56,18 +60,38 @@ public class TopicManagementResponse { this.errors = errors.build(); } + /** + * Gets the number of registration tokens that were successfully subscribed or unsubscribed. + * + * @return The number of successes. + */ public int getSuccessCount() { return successCount; } + /** + * Gets the number of registration tokens that could not be subscribed or unsubscribed, and + * resulted in an error. + * + * @return The number of failures. + */ public int getFailureCount() { return failureCount; } + /** + * Gets a list of errors encountered while executing the topic management operation. + * + * @return A non-null list. + */ + @NonNull public List getErrors() { return errors; } + /** + * A topic management error. + */ public static class Error { private final int index; private final String reason; @@ -78,10 +102,21 @@ private Error(int index, String reason) { ? ERROR_CODES.get(reason) : UNKNOWN_ERROR; } + /** + * Index of the registration token to which this error is related to. + * + * @return An index into the original registration token list. + */ public int getIndex() { return index; } + /** + * String describing the nature of the error. + * + * @return A non-null, non-empty error message. + */ + @NonNull public String getReason() { return reason; } From a4661a0f44d1dc35d1531eac78f9e3459d6bccb7 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Wed, 17 Jan 2018 11:21:25 -0800 Subject: [PATCH 16/32] Updated documentation --- .../firebase/messaging/FirebaseMessaging.java | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java index d330b09bf..643a14153 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java @@ -163,8 +163,16 @@ public String call() throws FirebaseMessagingException { }); } + /** + * Subscribes a list of registration tokens to a topic. + * + * @param registrationTokens A non-null, non-empty list of device registration tokens, with at + * most 1000 entries. + * @param topic Name of the topic to subscribe to. May contain the {@code /topics/} prefix. + * @return An {@code ApiFuture} that will complete with a {@link TopicManagementResponse}. + */ public ApiFuture subscribeToTopicAsync( - List registrationTokens, String topic) { + @NonNull List registrationTokens, @NonNull String topic) { return new TaskToApiFuture<>(subscribeToTopic(registrationTokens, topic)); } @@ -181,8 +189,16 @@ public TopicManagementResponse call() throws FirebaseMessagingException { }); } + /** + * Unubscribes a list of registration tokens from a topic. + * + * @param registrationTokens A non-null, non-empty list of device registration tokens, with at + * most 1000 entries. + * @param topic Name of the topic to unsubscribe from. May contain the {@code /topics/} prefix. + * @return An {@code ApiFuture} that will complete with a {@link TopicManagementResponse}. + */ public ApiFuture unsubscribeFromTopicAsync( - List registrationTokens, String topic) { + @NonNull List registrationTokens, @NonNull String topic) { return new TaskToApiFuture<>(unsubscribeFromTopic(registrationTokens, topic)); } From 084e8e9691ff376bce8eaa31377ad589d238c31f Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Wed, 17 Jan 2018 16:22:36 -0800 Subject: [PATCH 17/32] Updated javadocs --- .../google/firebase/messaging/AndroidNotification.java | 2 +- .../java/com/google/firebase/messaging/WebpushConfig.java | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/google/firebase/messaging/AndroidNotification.java b/src/main/java/com/google/firebase/messaging/AndroidNotification.java index 2293adb4b..aeb828bc6 100644 --- a/src/main/java/com/google/firebase/messaging/AndroidNotification.java +++ b/src/main/java/com/google/firebase/messaging/AndroidNotification.java @@ -122,7 +122,7 @@ private Builder() { } /** - * Sets the title of the Android notification. When provided, overrides the title sent + * Sets the title of the Android notification. When provided, overrides the title set * via {@link Notification}. * * @param title Title of the notification. diff --git a/src/main/java/com/google/firebase/messaging/WebpushConfig.java b/src/main/java/com/google/firebase/messaging/WebpushConfig.java index f364ae123..de528e2cf 100644 --- a/src/main/java/com/google/firebase/messaging/WebpushConfig.java +++ b/src/main/java/com/google/firebase/messaging/WebpushConfig.java @@ -86,7 +86,9 @@ public Builder putAllHeaders(@NonNull Map map) { } /** - * Sets the given key-value pair as a Webpush data field. + * Sets the given key-value pair as a Webpush data field. When set, overrides any data fields + * set using the methods {@link Message.Builder#putData(String, String)} and + * {@link Message.Builder#putAllData(Map)}. * * @param key Name of the data field. Must not be null. * @param value Value of the data field. Must not be null. @@ -98,7 +100,9 @@ public Builder putData(String key, String value) { } /** - * Adds all the key-value pairs in the given map as Webpush data fields. + * Adds all the key-value pairs in the given map as Webpush data fields. When set, overrides any + * data fields set using the methods {@link Message.Builder#putData(String, String)} and + * {@link Message.Builder#putAllData(Map)}. * * @param map A non-null map of data values. Map must not contain null keys or values. * @return This builder. From f898ad1916feddb4aa2e374af18cdcb5bc6b50f0 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Thu, 18 Jan 2018 10:43:57 -0800 Subject: [PATCH 18/32] Updated test --- .../com/google/firebase/messaging/FirebaseMessagingIT.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java index 7081704c3..7f9f7d4e0 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java @@ -3,7 +3,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.firebase.testing.IntegrationTestUtils; import org.junit.BeforeClass; @@ -32,7 +31,7 @@ public void testSend() throws Exception { .setTopic("foo-bar") .build(); String id = messaging.sendAsync(message, true).get(); - assertTrue(!Strings.isNullOrEmpty(id)); + assertTrue(id != null && id.matches("^projects/.*/messages/.*$")); } @Test From 9e5e7e726762fac54600eb45188f687efda04b9a Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Thu, 18 Jan 2018 14:49:30 -0800 Subject: [PATCH 19/32] Renamed Priority enum values (using uppercase) --- .../com/google/firebase/messaging/AndroidConfig.java | 6 +++--- .../google/firebase/messaging/FirebaseMessagingTest.java | 2 +- .../java/com/google/firebase/messaging/MessageTest.java | 9 +++++---- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/google/firebase/messaging/AndroidConfig.java b/src/main/java/com/google/firebase/messaging/AndroidConfig.java index 4d4a6730c..886bc61db 100644 --- a/src/main/java/com/google/firebase/messaging/AndroidConfig.java +++ b/src/main/java/com/google/firebase/messaging/AndroidConfig.java @@ -51,7 +51,7 @@ public class AndroidConfig { private AndroidConfig(Builder builder) { this.collapseKey = builder.collapseKey; if (builder.priority != null) { - this.priority = builder.priority.name(); + this.priority = builder.priority.name().toLowerCase(); } else { this.priority = null; } @@ -75,8 +75,8 @@ private AndroidConfig(Builder builder) { * Priority levels that can be set on an {@link AndroidConfig}. */ public enum Priority { - high, - normal, + HIGH, + NORMAL, } /** diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java index 47f0741f9..2d9e66e68 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java @@ -438,7 +438,7 @@ private static Map> buildTestMessages() { builder.put( Message.builder() .setAndroidConfig(AndroidConfig.builder() - .setPriority(AndroidConfig.Priority.high) + .setPriority(AndroidConfig.Priority.HIGH) .setTtl("1.23s") .setRestrictedPackageName("test-package") .setCollapseKey("test-key") diff --git a/src/test/java/com/google/firebase/messaging/MessageTest.java b/src/test/java/com/google/firebase/messaging/MessageTest.java index 401e0cf02..0b2e3e0d9 100644 --- a/src/test/java/com/google/firebase/messaging/MessageTest.java +++ b/src/test/java/com/google/firebase/messaging/MessageTest.java @@ -8,6 +8,7 @@ import com.google.api.client.json.JsonParser; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.firebase.messaging.AndroidConfig.Priority; import java.io.IOException; import java.util.HashMap; import java.util.List; @@ -68,7 +69,7 @@ public void testAndroidMessageWithoutNotification() throws IOException { Message message = Message.builder() .setAndroidConfig(AndroidConfig.builder() .setCollapseKey("test-key") - .setPriority(AndroidConfig.Priority.high) + .setPriority(Priority.HIGH) .setTtl("10s") .setRestrictedPackageName("test-pkg-name") .putData("k1", "v1") @@ -91,7 +92,7 @@ public void testAndroidMessageWithNotification() throws IOException { Message message = Message.builder() .setAndroidConfig(AndroidConfig.builder() .setCollapseKey("test-key") - .setPriority(AndroidConfig.Priority.high) + .setPriority(Priority.HIGH) .setTtl("10.001s") .setRestrictedPackageName("test-pkg-name") .setNotification(AndroidNotification.builder() @@ -140,7 +141,7 @@ public void testAndroidMessageWithoutLocalization() throws IOException { Message message = Message.builder() .setAndroidConfig(AndroidConfig.builder() .setCollapseKey("test-key") - .setPriority(AndroidConfig.Priority.high) + .setPriority(Priority.NORMAL) .setTtl("10.001s") .setRestrictedPackageName("test-pkg-name") .setNotification(AndroidNotification.builder() @@ -166,7 +167,7 @@ public void testAndroidMessageWithoutLocalization() throws IOException { .build(); Map data = ImmutableMap.of( "collapse_key", "test-key", - "priority", "high", + "priority", "normal", "ttl", "10.001s", "restricted_package_name", "test-pkg-name", "notification", notification From 1b598a4f4c00e675ced03c61ed1744872ee27762 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Thu, 18 Jan 2018 17:07:25 -0800 Subject: [PATCH 20/32] Adding APNS types --- .../messaging/AndroidNotification.java | 13 +- .../google/firebase/messaging/ApnsConfig.java | 47 +++- .../com/google/firebase/messaging/Aps.java | 167 ++++++++++++++ .../google/firebase/messaging/ApsAlert.java | 218 ++++++++++++++++++ .../messaging/FirebaseMessagingTest.java | 27 ++- .../firebase/messaging/MessageTest.java | 166 +++++++++---- 6 files changed, 576 insertions(+), 62 deletions(-) create mode 100644 src/main/java/com/google/firebase/messaging/Aps.java create mode 100644 src/main/java/com/google/firebase/messaging/ApsAlert.java diff --git a/src/main/java/com/google/firebase/messaging/AndroidNotification.java b/src/main/java/com/google/firebase/messaging/AndroidNotification.java index 2293adb4b..817393bc7 100644 --- a/src/main/java/com/google/firebase/messaging/AndroidNotification.java +++ b/src/main/java/com/google/firebase/messaging/AndroidNotification.java @@ -209,7 +209,7 @@ public Builder setClickAction(String clickAction) { * @param bodyLocKey Resource key string. * @return This builder. */ - public Builder setBodyLocKey(String bodyLocKey) { + public Builder setBodyLocalizationKey(String bodyLocKey) { this.bodyLocKey = bodyLocKey; return this; } @@ -221,7 +221,7 @@ public Builder setBodyLocKey(String bodyLocKey) { * @param arg Resource key string. * @return This builder. */ - public Builder addBodyLocArg(@NonNull String arg) { + public Builder addBodyLocalizationArg(@NonNull String arg) { this.bodyLocArgs.add(arg); return this; } @@ -233,7 +233,7 @@ public Builder addBodyLocArg(@NonNull String arg) { * @param args List of resource key strings. * @return This builder. */ - public Builder addAllBodyLocArgs(@NonNull List args) { + public Builder addAllBodyLocalizationArgs(@NonNull List args) { this.bodyLocArgs.addAll(args); return this; } @@ -245,7 +245,7 @@ public Builder addAllBodyLocArgs(@NonNull List args) { * @param titleLocKey Resource key string. * @return This builder. */ - public Builder setTitleLocKey(String titleLocKey) { + public Builder setTitleLocalizationKey(String titleLocKey) { this.titleLocKey = titleLocKey; return this; } @@ -257,7 +257,7 @@ public Builder setTitleLocKey(String titleLocKey) { * @param arg Resource key string. * @return This builder. */ - public Builder addTitleLocArg(@NonNull String arg) { + public Builder addTitleLocalizationArg(@NonNull String arg) { this.titleLocArgs.add(arg); return this; } @@ -269,7 +269,7 @@ public Builder addTitleLocArg(@NonNull String arg) { * @param args List of resource key strings. * @return This builder. */ - public Builder addAllTitleLocArgs(@NonNull List args) { + public Builder addAllTitleLocalizationArgs(@NonNull List args) { this.titleLocArgs.addAll(args); return this; } @@ -284,5 +284,4 @@ public AndroidNotification build() { return new AndroidNotification(this); } } - } diff --git a/src/main/java/com/google/firebase/messaging/ApnsConfig.java b/src/main/java/com/google/firebase/messaging/ApnsConfig.java index 6e0ebe1a1..d9ffd5587 100644 --- a/src/main/java/com/google/firebase/messaging/ApnsConfig.java +++ b/src/main/java/com/google/firebase/messaging/ApnsConfig.java @@ -16,6 +16,8 @@ package com.google.firebase.messaging; +import static com.google.common.base.Preconditions.checkArgument; + import com.google.api.client.util.Key; import com.google.common.collect.ImmutableMap; import com.google.firebase.internal.NonNull; @@ -37,9 +39,14 @@ public class ApnsConfig { private final Map payload; private ApnsConfig(Builder builder) { + checkArgument(builder.aps != null, "aps must be specified"); + checkArgument(!builder.customData.containsKey("aps"), + "aps cannot be specified as part of custom data"); this.headers = builder.headers.isEmpty() ? null : ImmutableMap.copyOf(builder.headers); - this.payload = builder.payload == null || builder.payload.isEmpty() - ? null : ImmutableMap.copyOf(builder.payload); + this.payload = ImmutableMap.builder() + .putAll(builder.customData) + .put("aps", builder.aps) + .build(); } /** @@ -54,10 +61,11 @@ public static Builder builder() { public static class Builder { private final Map headers = new HashMap<>(); - private Map payload; + private final Map customData = new HashMap<>(); + private Aps aps; /** - * Sets the given key-value pair as an APNS header. + * Adds the given key-value pair as an APNS header. * * @param key Name of the header field. Must not be null. * @param value Value of the header field. Must not be null. @@ -80,13 +88,36 @@ public Builder putAllHeaders(@NonNull Map map) { } /** - * Sets the APNS payload as a JSON-serializable map. + * Sets the aps dictionary of the APNS message. + * + * @param aps A non-null instance of {@link Aps}. + * @return This builder. + */ + public Builder setAps(@NonNull Aps aps) { + this.aps = aps; + return this; + } + + /** + * Adds the given key-value pair as an APNS custom data field. + * + * @param key Name of the data field. Must not be null. + * @param value Value of the data field. Must not be null. + * @return This builder. + */ + public Builder putCustomData(@NonNull String key, @NonNull Object value) { + this.customData.put(key, value); + return this; + } + + /** + * Adds all the key-value pairs in the given map as APNS custom data fields. * - * @param payload Map containing both aps dictionary and custom payload. + * @param map A non-null map. Map must not contain null keys or values. * @return This builder. */ - public Builder setPayload(Map payload) { - this.payload = payload; + public Builder putAllCustomData(@NonNull Map map) { + this.customData.putAll(map); return this; } diff --git a/src/main/java/com/google/firebase/messaging/Aps.java b/src/main/java/com/google/firebase/messaging/Aps.java new file mode 100644 index 000000000..5fadb862a --- /dev/null +++ b/src/main/java/com/google/firebase/messaging/Aps.java @@ -0,0 +1,167 @@ +/* + * Copyright 2017 Google Inc. + * + * 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 com.google.api.client.util.Key; +import com.google.common.base.Strings; +import com.google.common.primitives.Booleans; + +/** + * Represents the + * aps dictionary that is part of every APNS message. + */ +public class Aps { + + @Key("alert") + private final Object alert; + + @Key("badge") + private final Integer badge; + + @Key("sound") + private final String sound; + + @Key("content-available") + private final Integer contentAvailable; + + @Key("category") + private final String category; + + @Key("thread-id") + private final String threadId; + + private Aps(Builder builder) { + int alerts = Booleans.countTrue( + !Strings.isNullOrEmpty(builder.alertString), + builder.alert != null); + checkArgument(alerts != 2, "Multiple alert specifications (string and ApsAlert) found."); + if (builder.alert != null) { + this.alert = builder.alert; + } else { + this.alert = builder.alertString; + } + this.badge = builder.badge; + this.sound = builder.sound; + this.contentAvailable = builder.contentAvailable ? 1 : null; + this.category = builder.category; + this.threadId = builder.threadId; + } + + /** + * Creates a new {@link Aps.Builder}. + * + * @return A {@link Aps.Builder} instance. + */ + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private String alertString; + private ApsAlert alert; + private Integer badge; + private String sound; + private boolean contentAvailable; + private String category; + private String threadId; + + /** + * Sets the alert field as a string. + * + * @param alert A string alert. + * @return This builder. + */ + public Builder setAlert(String alert) { + this.alertString = alert; + return this; + } + + /** + * Sets the alert as a dictionary. + * + * @param alert An instance of {@link ApsAlert}. + * @return This builder. + */ + public Builder setAlert(ApsAlert alert) { + this.alert = alert; + return this; + } + + /** + * Sets the badge to be displayed with the message. Set to 0 to remove the badge. Do not + * specify any value to keep the badge unchanged. + * + * @param badge An integer representing the badge. + * @return This builder. + */ + public Builder setBadge(int badge) { + this.badge = badge; + return this; + } + + /** + * Sets the sound to be played with the message. + * + * @param sound Sound file name or {@code "default"}. + * @return This builder. + */ + public Builder setSound(String sound) { + this.sound = sound; + return this; + } + + /** + * Sets whether to configure a background update notification. + * + * @param contentAvailable true to perform a background update. + * @return This builder. + */ + public Builder setContentAvailable(boolean contentAvailable) { + this.contentAvailable = contentAvailable; + return this; + } + + /** + * Sets the notification type. + * + * @param category A string identifier. + * @return This builder. + */ + public Builder setCategory(String category) { + this.category = category; + return this; + } + + /** + * Sets an app-specific identifier for grouping notifications. + * + * @param threadId A string identifier. + * @return This builder. + */ + public Builder setThreadId(String threadId) { + this.threadId = threadId; + return this; + } + + public Aps build() { + return new Aps(this); + } + } +} diff --git a/src/main/java/com/google/firebase/messaging/ApsAlert.java b/src/main/java/com/google/firebase/messaging/ApsAlert.java new file mode 100644 index 000000000..42cdc54ff --- /dev/null +++ b/src/main/java/com/google/firebase/messaging/ApsAlert.java @@ -0,0 +1,218 @@ +/* + * Copyright 2017 Google Inc. + * + * 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 com.google.api.client.util.Key; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.firebase.internal.NonNull; +import java.util.ArrayList; +import java.util.List; + +public class ApsAlert { + + @Key("title") + private final String title; + + @Key("body") + private final String body; + + @Key("loc-key") + private final String locKey; + + @Key("loc-args") + private final List locArgs; + + @Key("title-loc-key") + private final String titleLocKey; + + @Key("title-loc-args") + private final List titleLocArgs; + + @Key("action-loc-key") + private final String actionLocKey; + + @Key("launch-image") + private final String launchImage; + + private ApsAlert(Builder builder) { + this.title = builder.title; + this.body = builder.body; + this.actionLocKey = builder.actionLocKey; + this.locKey = builder.locKey; + if (!builder.locArgs.isEmpty()) { + checkArgument(!Strings.isNullOrEmpty(builder.locKey), + "locKey is required when specifying locArgs"); + this.locArgs = ImmutableList.copyOf(builder.locArgs); + } else { + this.locArgs = null; + } + + this.titleLocKey = builder.titleLocKey; + if (!builder.titleLocArgs.isEmpty()) { + checkArgument(!Strings.isNullOrEmpty(builder.titleLocKey), + "titleLocKey is required when specifying titleLocArgs"); + this.titleLocArgs = ImmutableList.copyOf(builder.titleLocArgs); + } else { + this.titleLocArgs = null; + } + this.launchImage = builder.launchImage; + } + + /** + * Creates a new {@link ApsAlert.Builder}. + * + * @return A {@link ApsAlert.Builder} instance. + */ + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private String title; + private String body; + private String locKey; + private List locArgs = new ArrayList<>(); + private String titleLocKey; + private List titleLocArgs = new ArrayList<>(); + private String actionLocKey; + private String launchImage; + + private Builder() { + } + + /** + * Sets the title of the alert. When provided, overrides the title sent + * via {@link Notification}. + * + * @param title Title of the notification. + * @return This builder. + */ + public Builder setTitle(String title) { + this.title = title; + return this; + } + + /** + * Sets the body of the alert. When provided, overrides the body sent + * via {@link Notification}. + * + * @param body Body of the notification. + * @return This builder. + */ + public Builder setBody(String body) { + this.body = body; + return this; + } + + public Builder setActionLocalizationKey(String actionLocKey) { + this.actionLocKey = actionLocKey; + return this; + } + + /** + * Sets the key of the body string in the app's string resources to use to localize the body + * text. + * + * @param locKey Resource key string. + * @return This builder. + */ + public Builder setLocalizationKey(String locKey) { + this.locKey = locKey; + return this; + } + + /** + * Adds a resource key string that will be used in place of the format specifiers in + * {@code bodyLocKey}. + * + * @param arg Resource key string. + * @return This builder. + */ + public Builder addLocalizationArg(@NonNull String arg) { + this.locArgs.add(arg); + return this; + } + + /** + * Adds a list of resource keys that will be used in place of the format specifiers in + * {@code bodyLocKey}. + * + * @param args List of resource key strings. + * @return This builder. + */ + public Builder addAllLocalizationArgs(@NonNull List args) { + this.locArgs.addAll(args); + return this; + } + + /** + * Sets the key of the title string in the app's string resources to use to localize the title + * text. + * + * @param titleLocKey Resource key string. + * @return This builder. + */ + public Builder setTitleLocalizationKey(String titleLocKey) { + this.titleLocKey = titleLocKey; + return this; + } + + /** + * Adds a resource key string that will be used in place of the format specifiers in + * {@code titleLocKey}. + * + * @param arg Resource key string. + * @return This builder. + */ + public Builder addTitleLocalizationArg(@NonNull String arg) { + this.titleLocArgs.add(arg); + return this; + } + + /** + * Adds a list of resource keys that will be used in place of the format specifiers in + * {@code titleLocKey}. + * + * @param args List of resource key strings. + * @return This builder. + */ + public Builder addAllTitleLocArgs(@NonNull List args) { + this.titleLocArgs.addAll(args); + return this; + } + + public Builder setLaunchImage(String launchImage) { + this.launchImage = launchImage; + return this; + } + + /** + * Creates a new {@link ApsAlert} instance from the parameters set on this builder. + * + * @return A new {@link ApsAlert} instance. + * @throws IllegalArgumentException If any of the parameters set on the builder are invalid. + */ + public ApsAlert build() { + return new ApsAlert(this); + } + } + +} diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java index 2d9e66e68..df901bc52 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java @@ -22,6 +22,7 @@ import com.google.firebase.testing.TestResponseInterceptor; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.math.BigDecimal; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -450,12 +451,12 @@ private static Map> buildTestMessages() { .setColor("#112233") .setTag("test-tag") .setSound("test-sound") - .setTitleLocKey("test-title-key") - .setBodyLocKey("test-body-key") - .addTitleLocArg("t-arg1") - .addAllTitleLocArgs(ImmutableList.of("t-arg2", "t-arg3")) - .addBodyLocArg("b-arg1") - .addAllBodyLocArgs(ImmutableList.of("b-arg2", "b-arg3")) + .setTitleLocalizationKey("test-title-key") + .setBodyLocalizationKey("test-body-key") + .addTitleLocalizationArg("t-arg1") + .addAllTitleLocalizationArgs(ImmutableList.of("t-arg2", "t-arg3")) + .addBodyLocalizationArg("b-arg1") + .addAllBodyLocalizationArgs(ImmutableList.of("b-arg2", "b-arg3")) .build()) .build()) .setTopic("test-topic") @@ -489,7 +490,14 @@ private static Map> buildTestMessages() { .setApnsConfig(ApnsConfig.builder() .putHeader("h1", "v1") .putAllHeaders(ImmutableMap.of("h2", "v2", "h3", "v3")) - .setPayload(ImmutableMap.of("k1", "v1", "k2", true)) + .putAllCustomData(ImmutableMap.of("k1", "v1", "k2", true)) + .setAps(Aps.builder() + .setBadge(42) + .setAlert(ApsAlert.builder() + .setTitle("test-title") + .setBody("test-body") + .build()) + .build()) .build()) .setTopic("test-topic") .build(), @@ -497,7 +505,10 @@ private static Map> buildTestMessages() { "topic", "test-topic", "apns", ImmutableMap.of( "headers", ImmutableMap.of("h1", "v1", "h2", "v2", "h3", "v3"), - "payload", ImmutableMap.of("k1", "v1", "k2", true)) + "payload", ImmutableMap.of("k1", "v1", "k2", true, + "aps", ImmutableMap.of("badge", new BigDecimal(42), + "alert", ImmutableMap.of( + "title", "test-title", "body", "test-body")))) )); // Webpush message diff --git a/src/test/java/com/google/firebase/messaging/MessageTest.java b/src/test/java/com/google/firebase/messaging/MessageTest.java index 0b2e3e0d9..b135dbf87 100644 --- a/src/test/java/com/google/firebase/messaging/MessageTest.java +++ b/src/test/java/com/google/firebase/messaging/MessageTest.java @@ -10,6 +10,7 @@ import com.google.common.collect.ImmutableMap; import com.google.firebase.messaging.AndroidConfig.Priority; import java.io.IOException; +import java.math.BigDecimal; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -103,12 +104,12 @@ public void testAndroidMessageWithNotification() throws IOException { .setColor("#112233") .setTag("android-tag") .setClickAction("android-click") - .setTitleLocKey("title-loc") - .addTitleLocArg("title-arg1") - .addAllTitleLocArgs(ImmutableList.of("title-arg2", "title-arg3")) - .setBodyLocKey("body-loc") - .addBodyLocArg("body-arg1") - .addAllBodyLocArgs(ImmutableList.of("body-arg2", "body-arg3")) + .setTitleLocalizationKey("title-loc") + .addTitleLocalizationArg("title-arg1") + .addAllTitleLocalizationArgs(ImmutableList.of("title-arg2", "title-arg3")) + .setBodyLocalizationKey("body-loc") + .addBodyLocalizationArg("body-arg1") + .addAllBodyLocalizationArgs(ImmutableList.of("body-arg2", "body-arg3")) .build()) .build()) .setTopic("test-topic") @@ -200,8 +201,8 @@ public void testInvalidAndroidConfig() throws IOException { AndroidNotification.builder().setColor("foo"), AndroidNotification.builder().setColor("123"), AndroidNotification.builder().setColor("#AABBCK"), - AndroidNotification.builder().addBodyLocArg("foo"), - AndroidNotification.builder().addTitleLocArg("foo") + AndroidNotification.builder().addBodyLocalizationArg("foo"), + AndroidNotification.builder().addTitleLocalizationArg("foo") ); for (int i = 0; i < notificationBuilders.size(); i++) { try { @@ -292,68 +293,155 @@ public void testWebpushMessageWithNotification() throws IOException { @Test public void testEmptyApnsMessage() throws IOException { Message message = Message.builder() - .setApnsConfig(ApnsConfig.builder().build()) + .setApnsConfig(ApnsConfig.builder().setAps(Aps.builder().build()).build()) .setTopic("test-topic") .build(); - Map data = ImmutableMap.of(); + Map data = ImmutableMap.of("payload", + ImmutableMap.of("aps", ImmutableMap.of())); assertJsonEquals(ImmutableMap.of("topic", "test-topic", "apns", data), message); } @Test - public void testApnsMessageWithoutPayload() throws IOException { + public void testApnsMessageWithPayload() throws IOException { Message message = Message.builder() .setApnsConfig(ApnsConfig.builder() .putHeader("k1", "v1") .putAllHeaders(ImmutableMap.of("k2", "v2", "k3", "v3")) + .putCustomData("cd1", "cd-v1") + .putAllCustomData(ImmutableMap.of("cd2", "cd-v2", "cd3", true)) + .setAps(Aps.builder().build()) .build()) .setTopic("test-topic") .build(); + + Map payload = ImmutableMap.builder() + .put("cd1", "cd-v1") + .put("cd2", "cd-v2") + .put("cd3", true) + .put("aps", ImmutableMap.of()) + .build(); Map data = ImmutableMap.of( - "headers", ImmutableMap.of("k1", "v1", "k2", "v2", "k3", "v3") + "headers", ImmutableMap.of("k1", "v1", "k2", "v2", "k3", "v3"), + "payload", payload ); assertJsonEquals(ImmutableMap.of("topic", "test-topic", "apns", data), message); + } - message = Message.builder() + @Test + public void testApnsMessageWithPayloadAndAps() throws IOException { + Message message = Message.builder() .setApnsConfig(ApnsConfig.builder() - .putHeader("k1", "v1") - .putAllHeaders(ImmutableMap.of("k2", "v2", "k3", "v3")) - .setPayload(null) + .putCustomData("cd1", "cd-v1") + .setAps(Aps.builder() + .setAlert("alert string") + .setBadge(42) + .setCategory("test-category") + .setContentAvailable(true) + .setSound("test-sound") + .setThreadId("test-thread-id") + .build()) .build()) .setTopic("test-topic") .build(); - assertJsonEquals(ImmutableMap.of("topic", "test-topic", "apns", data), message); + Map payload = ImmutableMap.of( + "cd1", "cd-v1", + "aps", ImmutableMap.builder() + .put("alert", "alert string") + .put("badge", new BigDecimal(42)) + .put("category", "test-category") + .put("content-available", new BigDecimal(1)) + .put("sound", "test-sound") + .put("thread-id", "test-thread-id") + .build()); + assertJsonEquals( + ImmutableMap.of( + "topic", "test-topic", + "apns", ImmutableMap.of("payload", payload)), + message); message = Message.builder() .setApnsConfig(ApnsConfig.builder() - .putHeader("k1", "v1") - .putAllHeaders(ImmutableMap.of("k2", "v2", "k3", "v3")) - .setPayload(ImmutableMap.of()) + .putCustomData("cd1", "cd-v1") + .setAps(Aps.builder() + .setAlert(ApsAlert.builder() + .setTitle("test-title") + .setBody("test-body") + .setLocalizationKey("test-loc-key") + .setActionLocalizationKey("test-action-loc-key") + .setTitleLocalizationKey("test-title-loc-key") + .addLocalizationArg("arg1") + .addAllLocalizationArgs(ImmutableList.of("arg2", "arg3")) + .addTitleLocalizationArg("arg4") + .addAllTitleLocArgs(ImmutableList.of("arg5", "arg6")) + .setLaunchImage("test-image") + .build()) + .setCategory("test-category") + .setSound("test-sound") + .setThreadId("test-thread-id") + .build()) .build()) .setTopic("test-topic") .build(); - assertJsonEquals(ImmutableMap.of("topic", "test-topic", "apns", data), message); + payload = ImmutableMap.of( + "cd1", "cd-v1", + "aps", ImmutableMap.builder() + .put("alert", ImmutableMap.builder() + .put("title", "test-title") + .put("body", "test-body") + .put("loc-key", "test-loc-key") + .put("action-loc-key", "test-action-loc-key") + .put("title-loc-key", "test-title-loc-key") + .put("loc-args", ImmutableList.of("arg1", "arg2", "arg3")) + .put("title-loc-args", ImmutableList.of("arg4", "arg5", "arg6")) + .put("launch-image", "test-image") + .build()) + .put("category", "test-category") + .put("sound", "test-sound") + .put("thread-id", "test-thread-id") + .build()); + assertJsonEquals( + ImmutableMap.of( + "topic", "test-topic", + "apns", ImmutableMap.of("payload", payload)), + message); } @Test - public void testApnsMessageWithPayload() throws IOException { - Map payload = ImmutableMap.builder() - .put("k1", "v1") - .put("k2", true) - .put("k3", ImmutableMap.of("k4", "v4")) - .build(); - Message message = Message.builder() - .setApnsConfig(ApnsConfig.builder() - .putHeader("k1", "v1") - .putAllHeaders(ImmutableMap.of("k2", "v2", "k3", "v3")) - .setPayload(payload) - .build()) - .setTopic("test-topic") - .build(); - Map data = ImmutableMap.of( - "headers", ImmutableMap.of("k1", "v1", "k2", "v2", "k3", "v3"), - "payload", payload + public void testInvalidApnsConfig() { + List configBuilders = ImmutableList.of( + ApnsConfig.builder(), + ApnsConfig.builder().putCustomData("aps", "foo"), + ApnsConfig.builder().putCustomData("aps", "foo").setAps(Aps.builder().build()) ); - assertJsonEquals(ImmutableMap.of("topic", "test-topic", "apns", data), message); + for (int i = 0; i < configBuilders.size(); i++) { + try { + configBuilders.get(i).build(); + fail("No error thrown for invalid config: " + i); + } catch (IllegalArgumentException expected) { + // expected + } + } + + Aps.Builder builder = Aps.builder().setAlert("string").setAlert(ApsAlert.builder().build()); + try { + builder.build(); + fail("No error thrown for invalid aps"); + } catch (IllegalArgumentException expected) { + // expected + } + + List notificationBuilders = ImmutableList.of( + ApsAlert.builder().addLocalizationArg("foo"), + ApsAlert.builder().addTitleLocalizationArg("foo") + ); + for (int i = 0; i < notificationBuilders.size(); i++) { + try { + notificationBuilders.get(i).build(); + fail("No error thrown for invalid alert: " + i); + } catch (IllegalArgumentException expected) { + // expected + } + } } private static void assertJsonEquals( From f971206d3483525de05d974dfaf79b87ecf1b424 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Thu, 18 Jan 2018 21:07:27 -0800 Subject: [PATCH 21/32] Updated APNS documentation --- .../java/com/google/firebase/messaging/Aps.java | 2 +- .../com/google/firebase/messaging/ApsAlert.java | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/google/firebase/messaging/Aps.java b/src/main/java/com/google/firebase/messaging/Aps.java index 5fadb862a..ecfb85191 100644 --- a/src/main/java/com/google/firebase/messaging/Aps.java +++ b/src/main/java/com/google/firebase/messaging/Aps.java @@ -128,7 +128,7 @@ public Builder setSound(String sound) { } /** - * Sets whether to configure a background update notification. + * Specifies whether to configure a background update notification. * * @param contentAvailable true to perform a background update. * @return This builder. diff --git a/src/main/java/com/google/firebase/messaging/ApsAlert.java b/src/main/java/com/google/firebase/messaging/ApsAlert.java index 42cdc54ff..e85a9fb32 100644 --- a/src/main/java/com/google/firebase/messaging/ApsAlert.java +++ b/src/main/java/com/google/firebase/messaging/ApsAlert.java @@ -25,6 +25,10 @@ import java.util.ArrayList; import java.util.List; +/** + * Represents the + * alert property that can be included in the aps dictionary of an APNS payload. + */ public class ApsAlert { @Key("title") @@ -122,6 +126,13 @@ public Builder setBody(String body) { return this; } + /** + * Sets the key of the text in the app's string resources to use to localize the action button + * text. + * + * @param actionLocKey Resource key string. + * @return This builder. + */ public Builder setActionLocalizationKey(String actionLocKey) { this.actionLocKey = actionLocKey; return this; @@ -199,6 +210,12 @@ public Builder addAllTitleLocArgs(@NonNull List args) { return this; } + /** + * Sets the launch image for the notification action. + * + * @param launchImage An image file name. + * @return This builder. + */ public Builder setLaunchImage(String launchImage) { this.launchImage = launchImage; return this; From 7a2b311d6d92cfe753901cd70939424ef3ca370d Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Thu, 18 Jan 2018 23:18:50 -0800 Subject: [PATCH 22/32] Updated integration test --- .../firebase/messaging/FirebaseMessagingIT.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java index 7f9f7d4e0..db0e7a54c 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java @@ -27,7 +27,18 @@ public void testSend() throws Exception { .setAndroidConfig(AndroidConfig.builder() .setRestrictedPackageName("com.demoapps.hkj") .build()) - .setWebpushConfig(WebpushConfig.builder().putHeader("X-Custom-Val", "Foo").build()) + .setApnsConfig(ApnsConfig.builder() + .setAps(Aps.builder() + .setAlert(ApsAlert.builder() + .setTitle("Title") + .setBody("Body") + .build()) + .build()) + .build()) + .setWebpushConfig(WebpushConfig.builder() + .putHeader("X-Custom-Val", "Foo") + .setNotification(new WebpushNotification("Title", "Body")) + .build()) .setTopic("foo-bar") .build(); String id = messaging.sendAsync(message, true).get(); From ac5625d9acf8faece8186f69948c1fad2f213bee Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Thu, 25 Jan 2018 17:39:30 -0800 Subject: [PATCH 23/32] Making TTL a long argument --- .../firebase/messaging/AndroidConfig.java | 32 ++++++++--------- .../messaging/FirebaseMessagingTest.java | 5 +-- .../firebase/messaging/MessageTest.java | 34 +++++++------------ 3 files changed, 29 insertions(+), 42 deletions(-) diff --git a/src/main/java/com/google/firebase/messaging/AndroidConfig.java b/src/main/java/com/google/firebase/messaging/AndroidConfig.java index 886bc61db..c9eb151a2 100644 --- a/src/main/java/com/google/firebase/messaging/AndroidConfig.java +++ b/src/main/java/com/google/firebase/messaging/AndroidConfig.java @@ -23,6 +23,7 @@ import com.google.firebase.internal.NonNull; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.TimeUnit; /** * Represents the Android-specific options that can be included in a {@link Message}. @@ -56,16 +57,17 @@ private AndroidConfig(Builder builder) { this.priority = null; } if (builder.ttl != null) { - checkArgument(builder.ttl.endsWith("s"), "ttl must end with 's'"); - String numeric = builder.ttl.substring(0, builder.ttl.length() - 1); - checkArgument(numeric.matches("[0-9.\\-]*"), "malformed ttl string"); - try { - checkArgument(Double.parseDouble(numeric) >= 0, "ttl must not be negative"); - } catch (NumberFormatException e) { - throw new IllegalArgumentException("invalid ttl value", e); + checkArgument(builder.ttl >= 0, "ttl must not be negative"); + long seconds = TimeUnit.MILLISECONDS.toSeconds(builder.ttl); + long nanos = TimeUnit.MILLISECONDS.toNanos(builder.ttl - seconds * 1000L); + if (nanos > 0) { + this.ttl = String.format("%d.%09ds", seconds, nanos); + } else { + this.ttl = String.format("%ds", seconds); } + } else { + this.ttl = null; } - this.ttl = builder.ttl; this.restrictedPackageName = builder.restrictedPackageName; this.data = builder.data.isEmpty() ? null : ImmutableMap.copyOf(builder.data); this.notification = builder.notification; @@ -92,7 +94,7 @@ public static class Builder { private String collapseKey; private Priority priority; - private String ttl; + private Long ttl; private String restrictedPackageName; private final Map data = new HashMap<>(); private AndroidNotification notification; @@ -126,18 +128,12 @@ public Builder setPriority(Priority priority) { } /** - * Sets the time-to-live duration of the message. This indicates how long (in seconds) - * the message should be kept in FCM storage if the target device is offline. Set to 0 to - * send the message immediately. The duration must be encoded as a string, where the - * string ends in the suffix "s" (indicating seconds) and is preceded by the number of seconds, - * with nanoseconds expressed as fractional seconds. For example, 3 seconds with 0 nanoseconds - * should be encoded as {@code "3s"}, while 3 seconds and 1 nanosecond should be - * expressed as {@code "3.000000001s"}. + * Sets the time-to-live duration of the message in milliseconds. * - * @param ttl Time-to-live duration encoded as a string with suffix {@code "s"}. + * @param ttl Time-to-live duration in milliseconds. * @return This builder. */ - public Builder setTtl(String ttl) { + public Builder setTtl(long ttl) { this.ttl = ttl; return this; } diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java index df901bc52..d5f2bff5f 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java @@ -27,6 +27,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; import org.junit.After; import org.junit.Test; @@ -440,7 +441,7 @@ private static Map> buildTestMessages() { Message.builder() .setAndroidConfig(AndroidConfig.builder() .setPriority(AndroidConfig.Priority.HIGH) - .setTtl("1.23s") + .setTtl(TimeUnit.SECONDS.toMillis(123)) .setRestrictedPackageName("test-package") .setCollapseKey("test-key") .setNotification(AndroidNotification.builder() @@ -466,7 +467,7 @@ private static Map> buildTestMessages() { "android", ImmutableMap.of( "priority", "high", "collapse_key", "test-key", - "ttl", "1.23s", + "ttl", "123s", "restricted_package_name", "test-package", "notification", ImmutableMap.builder() .put("click_action", "test-action") diff --git a/src/test/java/com/google/firebase/messaging/MessageTest.java b/src/test/java/com/google/firebase/messaging/MessageTest.java index b135dbf87..9da1ef0e7 100644 --- a/src/test/java/com/google/firebase/messaging/MessageTest.java +++ b/src/test/java/com/google/firebase/messaging/MessageTest.java @@ -14,6 +14,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; import org.junit.Test; public class MessageTest { @@ -71,7 +72,7 @@ public void testAndroidMessageWithoutNotification() throws IOException { .setAndroidConfig(AndroidConfig.builder() .setCollapseKey("test-key") .setPriority(Priority.HIGH) - .setTtl("10s") + .setTtl(10) .setRestrictedPackageName("test-pkg-name") .putData("k1", "v1") .putAllData(ImmutableMap.of("k2", "v2", "k3", "v3")) @@ -81,7 +82,7 @@ public void testAndroidMessageWithoutNotification() throws IOException { Map data = ImmutableMap.of( "collapse_key", "test-key", "priority", "high", - "ttl", "10s", + "ttl", "0.010000000s", // 10 ms = 10,000,000 ns "restricted_package_name", "test-pkg-name", "data", ImmutableMap.of("k1", "v1", "k2", "v2", "k3", "v3") ); @@ -94,7 +95,7 @@ public void testAndroidMessageWithNotification() throws IOException { .setAndroidConfig(AndroidConfig.builder() .setCollapseKey("test-key") .setPriority(Priority.HIGH) - .setTtl("10.001s") + .setTtl(TimeUnit.DAYS.toMillis(30)) .setRestrictedPackageName("test-pkg-name") .setNotification(AndroidNotification.builder() .setTitle("android-title") @@ -130,7 +131,7 @@ public void testAndroidMessageWithNotification() throws IOException { Map data = ImmutableMap.of( "collapse_key", "test-key", "priority", "high", - "ttl", "10.001s", + "ttl", "2592000s", "restricted_package_name", "test-pkg-name", "notification", notification ); @@ -143,7 +144,7 @@ public void testAndroidMessageWithoutLocalization() throws IOException { .setAndroidConfig(AndroidConfig.builder() .setCollapseKey("test-key") .setPriority(Priority.NORMAL) - .setTtl("10.001s") + .setTtl(TimeUnit.SECONDS.toMillis(10)) .setRestrictedPackageName("test-pkg-name") .setNotification(AndroidNotification.builder() .setTitle("android-title") @@ -169,7 +170,7 @@ public void testAndroidMessageWithoutLocalization() throws IOException { Map data = ImmutableMap.of( "collapse_key", "test-key", "priority", "normal", - "ttl", "10.001s", + "ttl", "10s", "restricted_package_name", "test-pkg-name", "notification", notification ); @@ -178,22 +179,11 @@ public void testAndroidMessageWithoutLocalization() throws IOException { @Test public void testInvalidAndroidConfig() throws IOException { - List configBuilders = ImmutableList.of( - AndroidConfig.builder().setTtl(""), - AndroidConfig.builder().setTtl("s"), - AndroidConfig.builder().setTtl("10"), - AndroidConfig.builder().setTtl("10e1s"), - AndroidConfig.builder().setTtl("1.2.3s"), - AndroidConfig.builder().setTtl("10 s"), - AndroidConfig.builder().setTtl("-10s") - ); - for (int i = 0; i < configBuilders.size(); i++) { - try { - configBuilders.get(i).build(); - fail("No error thrown for invalid config: " + i); - } catch (IllegalArgumentException expected) { - // expected - } + try { + AndroidConfig.builder().setTtl(-1).build(); + fail("No error thrown for invalid ttl"); + } catch (IllegalArgumentException expected) { + // expected } List notificationBuilders = ImmutableList.of( From 83d2ac40f2f33e16678e27b2e21425c0de56b3fb Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Thu, 25 Jan 2018 17:46:20 -0800 Subject: [PATCH 24/32] Updated changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 109c30be7..ea4853f10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ method now throws a clear exception when invoked with a bucket URL instead of the name. +### Cloud Messaging + +- [feature] Added the `FirebaseCloudMessaging` API for sending + Firebase notifications and managing topic subscriptions. + # v5.8.0 ### Initialization From c6043be800abd518fa70f6bab6664436ade16e50 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Tue, 30 Jan 2018 12:09:33 -0800 Subject: [PATCH 25/32] Addressing a number of readability nits, and other code review comments --- .../firebase/messaging/AndroidConfig.java | 21 +++++++++---------- .../messaging/AndroidNotification.java | 7 +++---- .../google/firebase/messaging/ApnsConfig.java | 4 +++- .../com/google/firebase/messaging/Aps.java | 17 +++++++-------- .../google/firebase/messaging/ApsAlert.java | 5 ++--- .../firebase/messaging/FirebaseMessaging.java | 9 ++++---- .../messaging/FirebaseMessagingException.java | 2 +- .../google/firebase/messaging/Message.java | 8 +++---- .../firebase/messaging/Notification.java | 2 +- .../messaging/TopicManagementResponse.java | 8 ++----- .../firebase/messaging/WebpushConfig.java | 4 +++- .../messaging/WebpushNotification.java | 2 +- .../messaging/FirebaseMessagingIT.java | 16 ++++++++++++++ .../messaging/FirebaseMessagingTest.java | 16 ++++++++++++++ .../firebase/messaging/MessageTest.java | 16 ++++++++++++++ 15 files changed, 90 insertions(+), 47 deletions(-) diff --git a/src/main/java/com/google/firebase/messaging/AndroidConfig.java b/src/main/java/com/google/firebase/messaging/AndroidConfig.java index c9eb151a2..065376a79 100644 --- a/src/main/java/com/google/firebase/messaging/AndroidConfig.java +++ b/src/main/java/com/google/firebase/messaging/AndroidConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google Inc. + * Copyright 2018 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -59,9 +59,9 @@ private AndroidConfig(Builder builder) { if (builder.ttl != null) { checkArgument(builder.ttl >= 0, "ttl must not be negative"); long seconds = TimeUnit.MILLISECONDS.toSeconds(builder.ttl); - long nanos = TimeUnit.MILLISECONDS.toNanos(builder.ttl - seconds * 1000L); - if (nanos > 0) { - this.ttl = String.format("%d.%09ds", seconds, nanos); + long subsecondNanos = TimeUnit.MILLISECONDS.toNanos(builder.ttl - seconds * 1000L); + if (subsecondNanos > 0) { + this.ttl = String.format("%d.%09ds", seconds, subsecondNanos); } else { this.ttl = String.format("%ds", seconds); } @@ -99,9 +99,7 @@ public static class Builder { private final Map data = new HashMap<>(); private AndroidNotification notification; - private Builder() { - - } + private Builder() {} /** * Sets a collapse key for the message. Collapse key serves as an identifier for a group of @@ -151,8 +149,8 @@ public Builder setRestrictedPackageName(String restrictedPackageName) { } /** - * Adds the given key-value pair to the message as a data field. Key or the value may not be - * null. When set, overrides any data fields set using the methods + * Adds the given key-value pair to the message as a data field. Key and the value may not be + * null. When set, overrides any data fields set on the top-level {@link Message} via * {@link Message.Builder#putData(String, String)} and {@link Message.Builder#putAllData(Map)}. * * @param key Name of the data field. Must not be null. @@ -166,8 +164,9 @@ public Builder putData(@NonNull String key, @NonNull String value) { /** * Adds all the key-value pairs in the given map to the message as data fields. None of the - * keys or values may be null. When set, overrides any data fields set using the methods - * {@link Message.Builder#putData(String, String)} and {@link Message.Builder#putAllData(Map)}. + * keys and values may be null. When set, overrides any data fields set on the top-level + * {@link Message} via {@link Message.Builder#putData(String, String)} and + * {@link Message.Builder#putAllData(Map)}. * * @param map A non-null map of data fields. Map must not contain null keys or values. * @return This builder. diff --git a/src/main/java/com/google/firebase/messaging/AndroidNotification.java b/src/main/java/com/google/firebase/messaging/AndroidNotification.java index de2d1a1d0..27f315591 100644 --- a/src/main/java/com/google/firebase/messaging/AndroidNotification.java +++ b/src/main/java/com/google/firebase/messaging/AndroidNotification.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google Inc. + * Copyright 2018 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -118,8 +118,7 @@ public static class Builder { private String titleLocKey; private List titleLocArgs = new ArrayList<>(); - private Builder() { - } + private Builder() {} /** * Sets the title of the Android notification. When provided, overrides the title set @@ -192,7 +191,7 @@ public Builder setTag(String tag) { /** * Sets the action associated with a user click on the notification. If specified, an activity - * with a matching intent filter is launched when a user clicks on the notification. + * with a matching Intent Filter is launched when a user clicks on the notification. * * @param clickAction Click action name. * @return This builder. diff --git a/src/main/java/com/google/firebase/messaging/ApnsConfig.java b/src/main/java/com/google/firebase/messaging/ApnsConfig.java index d9ffd5587..40d76d6b2 100644 --- a/src/main/java/com/google/firebase/messaging/ApnsConfig.java +++ b/src/main/java/com/google/firebase/messaging/ApnsConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google Inc. + * Copyright 2018 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -64,6 +64,8 @@ public static class Builder { private final Map customData = new HashMap<>(); private Aps aps; + private Builder() {} + /** * Adds the given key-value pair as an APNS header. * diff --git a/src/main/java/com/google/firebase/messaging/Aps.java b/src/main/java/com/google/firebase/messaging/Aps.java index ecfb85191..c35d550d4 100644 --- a/src/main/java/com/google/firebase/messaging/Aps.java +++ b/src/main/java/com/google/firebase/messaging/Aps.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google Inc. + * Copyright 2018 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,6 @@ import com.google.api.client.util.Key; import com.google.common.base.Strings; -import com.google.common.primitives.Booleans; /** * Represents the @@ -47,10 +46,8 @@ public class Aps { private final String threadId; private Aps(Builder builder) { - int alerts = Booleans.countTrue( - !Strings.isNullOrEmpty(builder.alertString), - builder.alert != null); - checkArgument(alerts != 2, "Multiple alert specifications (string and ApsAlert) found."); + checkArgument(Strings.isNullOrEmpty(builder.alertString) || (builder.alert == null), + "Multiple alert specifications (string and ApsAlert) found."); if (builder.alert != null) { this.alert = builder.alert; } else { @@ -82,6 +79,8 @@ public static class Builder { private String category; private String threadId; + private Builder() {} + /** * Sets the alert field as a string. * @@ -105,8 +104,8 @@ public Builder setAlert(ApsAlert alert) { } /** - * Sets the badge to be displayed with the message. Set to 0 to remove the badge. Do not - * specify any value to keep the badge unchanged. + * Sets the badge to be displayed with the message. Set to 0 to remove the badge. When not + * invoked, the badge will remain unchanged. * * @param badge An integer representing the badge. * @return This builder. @@ -130,7 +129,7 @@ public Builder setSound(String sound) { /** * Specifies whether to configure a background update notification. * - * @param contentAvailable true to perform a background update. + * @param contentAvailable True to perform a background update. * @return This builder. */ public Builder setContentAvailable(boolean contentAvailable) { diff --git a/src/main/java/com/google/firebase/messaging/ApsAlert.java b/src/main/java/com/google/firebase/messaging/ApsAlert.java index e85a9fb32..6f7e249f3 100644 --- a/src/main/java/com/google/firebase/messaging/ApsAlert.java +++ b/src/main/java/com/google/firebase/messaging/ApsAlert.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google Inc. + * Copyright 2018 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -99,8 +99,7 @@ public static class Builder { private String actionLocKey; private String launchImage; - private Builder() { - } + private Builder() {} /** * Sets the title of the alert. When provided, overrides the title sent diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java index 643a14153..16d37e3fe 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google Inc. + * Copyright 2018 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -140,9 +140,10 @@ public ApiFuture sendAsync(@NonNull Message message) { } /** - * Sends the given {@link Message} via Firebase Cloud Messaging. If the {@code dryRun} option - * is set to true, the message will not be actually sent. Instead FCM performs all the - * necessary validations, and emulates the send operation. + * Sends the given {@link Message} via Firebase Cloud Messaging. + * + *

If the {@code dryRun} option is set to true, the message will not be actually sent. Instead + * FCM performs all the necessary validations, and emulates the send operation. * * @param message A non-null {@link Message} to be sent. * @param dryRun a boolean indicating whether to perform a dry run (validation only) of the send. diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessagingException.java b/src/main/java/com/google/firebase/messaging/FirebaseMessagingException.java index 1373efe47..5f57474ea 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessagingException.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessagingException.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google Inc. + * Copyright 2018 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/com/google/firebase/messaging/Message.java b/src/main/java/com/google/firebase/messaging/Message.java index 23eb4a866..8a449942d 100644 --- a/src/main/java/com/google/firebase/messaging/Message.java +++ b/src/main/java/com/google/firebase/messaging/Message.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google Inc. + * Copyright 2018 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,7 +30,7 @@ * Represents a message that can be sent via Firebase Cloud Messaging (FCM). Contains payload * information as well as the recipient information. In particular, the message must contain * exactly one of token, topic or condition parameters. Instances of this class are thread-safe - * and immutable. Use {@link Message.Builder} co crete new instances. + * and immutable. Use {@link Message.Builder} to crete new instances. * * @see * FCM message @@ -104,9 +104,7 @@ public static class Builder { private String topic; private String condition; - private Builder() { - - } + private Builder() {} /** * Sets the notification information to be included in the message. diff --git a/src/main/java/com/google/firebase/messaging/Notification.java b/src/main/java/com/google/firebase/messaging/Notification.java index 5cfc5ecc4..eaa1e1443 100644 --- a/src/main/java/com/google/firebase/messaging/Notification.java +++ b/src/main/java/com/google/firebase/messaging/Notification.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google Inc. + * Copyright 2018 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/com/google/firebase/messaging/TopicManagementResponse.java b/src/main/java/com/google/firebase/messaging/TopicManagementResponse.java index 85304c344..230c64df2 100644 --- a/src/main/java/com/google/firebase/messaging/TopicManagementResponse.java +++ b/src/main/java/com/google/firebase/messaging/TopicManagementResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google Inc. + * Copyright 2018 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,24 +39,20 @@ public class TopicManagementResponse { .build(); private final int successCount; - private final int failureCount; private final List errors; TopicManagementResponse(List> results) { int successCount = 0; - int failureCount = 0; ImmutableList.Builder errors = ImmutableList.builder(); for (int i = 0; i < results.size(); i++) { Map result = results.get(i); if (result.isEmpty()) { successCount++; } else { - failureCount++; errors.add(new Error(i, (String) result.get("error"))); } } this.successCount = successCount; - this.failureCount = failureCount; this.errors = errors.build(); } @@ -76,7 +72,7 @@ public int getSuccessCount() { * @return The number of failures. */ public int getFailureCount() { - return failureCount; + return errors.size(); } /** diff --git a/src/main/java/com/google/firebase/messaging/WebpushConfig.java b/src/main/java/com/google/firebase/messaging/WebpushConfig.java index de528e2cf..6b5ac4ac2 100644 --- a/src/main/java/com/google/firebase/messaging/WebpushConfig.java +++ b/src/main/java/com/google/firebase/messaging/WebpushConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google Inc. + * Copyright 2018 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -58,6 +58,8 @@ public static class Builder { private final Map data = new HashMap<>(); private WebpushNotification notification; + private Builder() {} + /** * Adds the given key-value pair as a Webpush HTTP header. Refer to * Webpush specification diff --git a/src/main/java/com/google/firebase/messaging/WebpushNotification.java b/src/main/java/com/google/firebase/messaging/WebpushNotification.java index 8377b2435..92876c562 100644 --- a/src/main/java/com/google/firebase/messaging/WebpushNotification.java +++ b/src/main/java/com/google/firebase/messaging/WebpushNotification.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google Inc. + * Copyright 2018 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java index db0e7a54c..fcca8ad2e 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java @@ -1,3 +1,19 @@ +/* + * Copyright 2018 Google Inc. + * + * 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 org.junit.Assert.assertEquals; diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java index d5f2bff5f..9efb4b29e 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java @@ -1,3 +1,19 @@ +/* + * Copyright 2018 Google Inc. + * + * 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 org.junit.Assert.assertEquals; diff --git a/src/test/java/com/google/firebase/messaging/MessageTest.java b/src/test/java/com/google/firebase/messaging/MessageTest.java index 9da1ef0e7..5fbb499c3 100644 --- a/src/test/java/com/google/firebase/messaging/MessageTest.java +++ b/src/test/java/com/google/firebase/messaging/MessageTest.java @@ -1,3 +1,19 @@ +/* + * Copyright 2018 Google Inc. + * + * 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 org.junit.Assert.assertEquals; From 011e6e8e90827dde7451722e2ed9f2131a217ed3 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Thu, 1 Feb 2018 12:43:01 -0800 Subject: [PATCH 26/32] Fixing some nits --- src/main/java/com/google/firebase/messaging/Message.java | 2 +- .../java/com/google/firebase/messaging/FirebaseMessagingIT.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/google/firebase/messaging/Message.java b/src/main/java/com/google/firebase/messaging/Message.java index 8a449942d..507ab124c 100644 --- a/src/main/java/com/google/firebase/messaging/Message.java +++ b/src/main/java/com/google/firebase/messaging/Message.java @@ -30,7 +30,7 @@ * Represents a message that can be sent via Firebase Cloud Messaging (FCM). Contains payload * information as well as the recipient information. In particular, the message must contain * exactly one of token, topic or condition parameters. Instances of this class are thread-safe - * and immutable. Use {@link Message.Builder} to crete new instances. + * and immutable. Use {@link Message.Builder} to create new instances. * * @see * FCM message diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java index fcca8ad2e..f309889e8 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java @@ -41,7 +41,7 @@ public void testSend() throws Exception { Message message = Message.builder() .setNotification(new Notification("Title", "Body")) .setAndroidConfig(AndroidConfig.builder() - .setRestrictedPackageName("com.demoapps.hkj") + .setRestrictedPackageName("com.google.firebase.testing") .build()) .setApnsConfig(ApnsConfig.builder() .setAps(Aps.builder() From 668b29fd55336dea60156daa7673a16d58d2ae72 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Tue, 6 Feb 2018 15:51:39 -0800 Subject: [PATCH 27/32] Removing some duplicate code in messaging tests --- .../firebase/messaging/FirebaseMessaging.java | 2 +- .../google/firebase/messaging/Message.java | 2 +- .../messaging/FirebaseMessagingTest.java | 123 ++++++++---------- 3 files changed, 56 insertions(+), 71 deletions(-) diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java index 16d37e3fe..905d1fe26 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java @@ -364,7 +364,7 @@ private static class FirebaseMessagingService extends FirebaseService HTTP_ERRORS = ImmutableList.of(401, 404, 500); + private static final String MOCK_RESPONSE = "{\"name\": \"mock-name\"}"; private static final ImmutableList.Builder tooManyIds = ImmutableList.builder(); @@ -97,13 +98,7 @@ public void testNoProjectId() { @Test public void testNullMessage() throws Exception { - FirebaseOptions options = new FirebaseOptions.Builder() - .setCredentials(new MockGoogleCredentials("test-token")) - .setProjectId("test-project") - .build(); - FirebaseApp.initializeApp(options); - - FirebaseMessaging messaging = FirebaseMessaging.getInstance(); + FirebaseMessaging messaging = initDefaultMessaging(); TestResponseInterceptor interceptor = new TestResponseInterceptor(); messaging.setInterceptor(interceptor); try { @@ -119,12 +114,12 @@ public void testNullMessage() throws Exception { @Test public void testSend() throws Exception { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() - .setContent("{\"name\": \"mock-name\"}"); + .setContent(MOCK_RESPONSE); FirebaseMessaging messaging = initMessaging(response); Map> testMessages = buildTestMessages(); for (Map.Entry> entry : testMessages.entrySet()) { - response.setContent("{\"name\": \"mock-name\"}"); + response.setContent(MOCK_RESPONSE); TestResponseInterceptor interceptor = new TestResponseInterceptor(); messaging.setInterceptor(interceptor); String resp = messaging.sendAsync(entry.getKey()).get(); @@ -143,12 +138,12 @@ public void testSend() throws Exception { @Test public void testSendDryRun() throws Exception { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() - .setContent("{\"name\": \"mock-name\"}"); + .setContent(MOCK_RESPONSE); FirebaseMessaging messaging = initMessaging(response); Map> testMessages = buildTestMessages(); for (Map.Entry> entry : testMessages.entrySet()) { - response.setContent("{\"name\": \"mock-name\"}"); + response.setContent(MOCK_RESPONSE); TestResponseInterceptor interceptor = new TestResponseInterceptor(); messaging.setInterceptor(interceptor); String resp = messaging.sendAsync(entry.getKey(), true).get(); @@ -193,12 +188,7 @@ public void testSendError() throws Exception { error.getMessage()); assertTrue(error.getCause() instanceof HttpResponseException); } - - assertNotNull(interceptor.getResponse()); - HttpRequest request = interceptor.getResponse().getRequest(); - assertEquals("POST", request.getRequestMethod()); - assertEquals(TEST_FCM_URL, request.getUrl().toString()); - assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); + checkMessagingRequest(interceptor); } } @@ -221,23 +211,13 @@ public void testSendErrorWithDetails() throws Exception { assertEquals("test error", error.getMessage()); assertTrue(error.getCause() instanceof HttpResponseException); } - - assertNotNull(interceptor.getResponse()); - HttpRequest request = interceptor.getResponse().getRequest(); - assertEquals("POST", request.getRequestMethod()); - assertEquals(TEST_FCM_URL, request.getUrl().toString()); - assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); + checkMessagingRequest(interceptor); } } @Test public void testInvalidSubscribe() { - FirebaseOptions options = new FirebaseOptions.Builder() - .setCredentials(new MockGoogleCredentials("test-token")) - .setProjectId("test-project") - .build(); - FirebaseApp.initializeApp(options); - FirebaseMessaging messaging = FirebaseMessaging.getInstance(); + FirebaseMessaging messaging = initDefaultMessaging(); TestResponseInterceptor interceptor = new TestResponseInterceptor(); messaging.setInterceptor(interceptor); @@ -263,23 +243,7 @@ public void testSubscribe() throws Exception { TopicManagementResponse result = messaging.subscribeToTopicAsync( ImmutableList.of("id1", "id2"), "test-topic").get(); - assertEquals(1, result.getSuccessCount()); - assertEquals(1, result.getFailureCount()); - assertEquals(1, result.getErrors().size()); - assertEquals(1, result.getErrors().get(0).getIndex()); - assertEquals("unknown-error", result.getErrors().get(0).getReason()); - - assertNotNull(interceptor.getResponse()); - HttpRequest request = interceptor.getResponse().getRequest(); - assertEquals("POST", request.getRequestMethod()); - assertEquals(TEST_IID_SUBSCRIBE_URL, request.getUrl().toString()); - assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); - assertEquals("true", request.getHeaders().get("access_token_auth")); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - request.getContent().writeTo(out); - assertEquals("{\"to\":\"/topics/test-topic\",\"registration_tokens\":[\"id1\",\"id2\"]}", - out.toString()); + checkTopicManagementCall(TEST_IID_SUBSCRIBE_URL, result, interceptor); } @Test @@ -311,12 +275,7 @@ public void testSubscribeError() throws Exception { @Test public void testInvalidUnsubscribe() { - FirebaseOptions options = new FirebaseOptions.Builder() - .setCredentials(new MockGoogleCredentials("test-token")) - .setProjectId("test-project") - .build(); - FirebaseApp.initializeApp(options); - FirebaseMessaging messaging = FirebaseMessaging.getInstance(); + FirebaseMessaging messaging = initDefaultMessaging(); TestResponseInterceptor interceptor = new TestResponseInterceptor(); messaging.setInterceptor(interceptor); @@ -342,23 +301,7 @@ public void testUnsubscribe() throws Exception { TopicManagementResponse result = messaging.unsubscribeFromTopicAsync( ImmutableList.of("id1", "id2"), "test-topic").get(); - assertEquals(1, result.getSuccessCount()); - assertEquals(1, result.getFailureCount()); - assertEquals(1, result.getErrors().size()); - assertEquals(1, result.getErrors().get(0).getIndex()); - assertEquals("unknown-error", result.getErrors().get(0).getReason()); - - assertNotNull(interceptor.getResponse()); - HttpRequest request = interceptor.getResponse().getRequest(); - assertEquals("POST", request.getRequestMethod()); - assertEquals(TEST_IID_UNSUBSCRIBE_URL, request.getUrl().toString()); - assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); - assertEquals("true", request.getHeaders().get("access_token_auth")); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - request.getContent().writeTo(out); - assertEquals("{\"to\":\"/topics/test-topic\",\"registration_tokens\":[\"id1\",\"id2\"]}", - out.toString()); + checkTopicManagementCall(TEST_IID_UNSUBSCRIBE_URL, result, interceptor); } @Test @@ -412,6 +355,48 @@ private static FirebaseMessaging initMessaging(MockLowLevelHttpResponse mockResp return messaging; } + private static FirebaseMessaging initDefaultMessaging() { + FirebaseOptions options = new FirebaseOptions.Builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setProjectId("test-project") + .build(); + FirebaseApp app = FirebaseApp.initializeApp(options); + return FirebaseMessaging.getInstance(app); + } + + private static void checkMessagingRequest(TestResponseInterceptor interceptor) { + assertNotNull(interceptor.getResponse()); + HttpRequest request = interceptor.getResponse().getRequest(); + assertEquals("POST", request.getRequestMethod()); + assertEquals(TEST_FCM_URL, request.getUrl().toString()); + assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); + } + + private static void checkTopicManagementCall(String url, + TopicManagementResponse result, TestResponseInterceptor interceptor) throws IOException { + assertEquals(1, result.getSuccessCount()); + assertEquals(1, result.getFailureCount()); + assertEquals(1, result.getErrors().size()); + assertEquals(1, result.getErrors().get(0).getIndex()); + assertEquals("unknown-error", result.getErrors().get(0).getReason()); + + assertNotNull(interceptor.getResponse()); + HttpRequest request = interceptor.getResponse().getRequest(); + assertEquals("POST", request.getRequestMethod()); + assertEquals(url, request.getUrl().toString()); + assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); + assertEquals("true", request.getHeaders().get("access_token_auth")); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + request.getContent().writeTo(out); + Map parsed = new HashMap<>(); + JsonParser parser = Utils.getDefaultJsonFactory().createJsonParser(out.toString()); + parser.parseAndClose(parsed); + assertEquals(2, parsed.size()); + assertEquals("/topics/test-topic", parsed.get("to")); + assertEquals(ImmutableList.of("id1", "id2"), parsed.get("registration_tokens")); + } + private static class TopicMgtArgs { private final List registrationTokens; private final String topic; From 854c6dfdb407f4e6f4a1080169c0682fd935d01a Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Tue, 6 Feb 2018 16:45:01 -0800 Subject: [PATCH 28/32] Further deduping code in test --- .../messaging/FirebaseMessagingTest.java | 80 ++++++++----------- 1 file changed, 34 insertions(+), 46 deletions(-) diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java index 0d4609a9d..ac55255b9 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java @@ -125,12 +125,7 @@ public void testSend() throws Exception { String resp = messaging.sendAsync(entry.getKey()).get(); assertEquals("mock-name", resp); - assertNotNull(interceptor.getResponse()); - HttpRequest request = interceptor.getResponse().getRequest(); - assertEquals("POST", request.getRequestMethod()); - assertEquals(TEST_FCM_URL, request.getUrl().toString()); - assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); - + HttpRequest request = checkRequestHeader(interceptor); checkRequest(request, ImmutableMap.of("message", entry.getValue())); } } @@ -149,26 +144,11 @@ public void testSendDryRun() throws Exception { String resp = messaging.sendAsync(entry.getKey(), true).get(); assertEquals("mock-name", resp); - assertNotNull(interceptor.getResponse()); - HttpRequest request = interceptor.getResponse().getRequest(); - assertEquals("POST", request.getRequestMethod()); - assertEquals(TEST_FCM_URL, request.getUrl().toString()); - assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); - + HttpRequest request = checkRequestHeader(interceptor); checkRequest(request, ImmutableMap.of("message", entry.getValue(), "validate_only", true)); } } - private static void checkRequest( - HttpRequest request, Map expected) throws IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - request.getContent().writeTo(out); - JsonParser parser = Utils.getDefaultJsonFactory().createJsonParser(out.toString()); - Map parsed = new HashMap<>(); - parser.parseAndClose(parsed); - assertEquals(expected, parsed); - } - @Test public void testSendError() throws Exception { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); @@ -188,7 +168,7 @@ public void testSendError() throws Exception { error.getMessage()); assertTrue(error.getCause() instanceof HttpResponseException); } - checkMessagingRequest(interceptor); + checkRequestHeader(interceptor); } } @@ -211,7 +191,7 @@ public void testSendErrorWithDetails() throws Exception { assertEquals("test error", error.getMessage()); assertTrue(error.getCause() instanceof HttpResponseException); } - checkMessagingRequest(interceptor); + checkRequestHeader(interceptor); } } @@ -243,7 +223,8 @@ public void testSubscribe() throws Exception { TopicManagementResponse result = messaging.subscribeToTopicAsync( ImmutableList.of("id1", "id2"), "test-topic").get(); - checkTopicManagementCall(TEST_IID_SUBSCRIBE_URL, result, interceptor); + HttpRequest request = checkTopicManagementRequestHeader(interceptor, TEST_IID_SUBSCRIBE_URL); + checkTopicManagementRequest(request, result); } @Test @@ -265,11 +246,7 @@ public void testSubscribeError() throws Exception { assertTrue(error.getCause() instanceof HttpResponseException); } - assertNotNull(interceptor.getResponse()); - HttpRequest request = interceptor.getResponse().getRequest(); - assertEquals("POST", request.getRequestMethod()); - assertEquals(TEST_IID_SUBSCRIBE_URL, request.getUrl().toString()); - assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); + checkTopicManagementRequestHeader(interceptor, TEST_IID_SUBSCRIBE_URL); } } @@ -301,7 +278,8 @@ public void testUnsubscribe() throws Exception { TopicManagementResponse result = messaging.unsubscribeFromTopicAsync( ImmutableList.of("id1", "id2"), "test-topic").get(); - checkTopicManagementCall(TEST_IID_UNSUBSCRIBE_URL, result, interceptor); + HttpRequest request = checkTopicManagementRequestHeader(interceptor, TEST_IID_UNSUBSCRIBE_URL); + checkTopicManagementRequest(request, result); } @Test @@ -323,11 +301,7 @@ public void testUnsubscribeError() throws Exception { assertTrue(error.getCause() instanceof HttpResponseException); } - assertNotNull(interceptor.getResponse()); - HttpRequest request = interceptor.getResponse().getRequest(); - assertEquals("POST", request.getRequestMethod()); - assertEquals(TEST_IID_UNSUBSCRIBE_URL, request.getUrl().toString()); - assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); + checkTopicManagementRequestHeader(interceptor, TEST_IID_UNSUBSCRIBE_URL); } } @@ -364,29 +338,33 @@ private static FirebaseMessaging initDefaultMessaging() { return FirebaseMessaging.getInstance(app); } - private static void checkMessagingRequest(TestResponseInterceptor interceptor) { + private static void checkRequest( + HttpRequest request, Map expected) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + request.getContent().writeTo(out); + JsonParser parser = Utils.getDefaultJsonFactory().createJsonParser(out.toString()); + Map parsed = new HashMap<>(); + parser.parseAndClose(parsed); + assertEquals(expected, parsed); + } + + private static HttpRequest checkRequestHeader(TestResponseInterceptor interceptor) { assertNotNull(interceptor.getResponse()); HttpRequest request = interceptor.getResponse().getRequest(); assertEquals("POST", request.getRequestMethod()); assertEquals(TEST_FCM_URL, request.getUrl().toString()); assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); + return request; } - private static void checkTopicManagementCall(String url, - TopicManagementResponse result, TestResponseInterceptor interceptor) throws IOException { + private static void checkTopicManagementRequest( + HttpRequest request, TopicManagementResponse result) throws IOException { assertEquals(1, result.getSuccessCount()); assertEquals(1, result.getFailureCount()); assertEquals(1, result.getErrors().size()); assertEquals(1, result.getErrors().get(0).getIndex()); assertEquals("unknown-error", result.getErrors().get(0).getReason()); - assertNotNull(interceptor.getResponse()); - HttpRequest request = interceptor.getResponse().getRequest(); - assertEquals("POST", request.getRequestMethod()); - assertEquals(url, request.getUrl().toString()); - assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); - assertEquals("true", request.getHeaders().get("access_token_auth")); - ByteArrayOutputStream out = new ByteArrayOutputStream(); request.getContent().writeTo(out); Map parsed = new HashMap<>(); @@ -397,6 +375,16 @@ private static void checkTopicManagementCall(String url, assertEquals(ImmutableList.of("id1", "id2"), parsed.get("registration_tokens")); } + private static HttpRequest checkTopicManagementRequestHeader( + TestResponseInterceptor interceptor, String expectedUrl) { + assertNotNull(interceptor.getResponse()); + HttpRequest request = interceptor.getResponse().getRequest(); + assertEquals("POST", request.getRequestMethod()); + assertEquals(expectedUrl, request.getUrl().toString()); + assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); + return request; + } + private static class TopicMgtArgs { private final List registrationTokens; private final String topic; From b96b220de03ed94a9ecf3cb37bf28b2cf09db8f2 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Tue, 6 Feb 2018 18:05:09 -0800 Subject: [PATCH 29/32] Accept prefixed topic names (#136) * Accept prefixed topic names * Parsing topic name in a helper method * Updated javadoc for setTopic() --- .../google/firebase/messaging/Message.java | 25 ++++++++++++------- .../firebase/messaging/MessageTest.java | 20 +++++++++++++++ 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/google/firebase/messaging/Message.java b/src/main/java/com/google/firebase/messaging/Message.java index af2473dc6..564c8896e 100644 --- a/src/main/java/com/google/firebase/messaging/Message.java +++ b/src/main/java/com/google/firebase/messaging/Message.java @@ -74,16 +74,23 @@ private Message(Builder builder) { !Strings.isNullOrEmpty(builder.condition) ); checkArgument(count == 1, "Exactly one of token, topic or condition must be specified"); - if (builder.topic != null) { - checkArgument(!builder.topic.startsWith("/topics/"), - "Topic name must not contain the /topics/ prefix"); - checkArgument(builder.topic.matches("[a-zA-Z0-9-_.~%]+"), "Malformed topic name"); - } this.token = builder.token; - this.topic = builder.topic; + this.topic = stripPrefix(builder.topic); this.condition = builder.condition; } + private static String stripPrefix(String topic) { + if (Strings.isNullOrEmpty(topic)) { + return null; + } + if (topic.startsWith("/topics/")) { + topic = topic.replaceFirst("^/topics/", ""); + } + // Checks for illegal characters and empty string. + checkArgument(topic.matches("[a-zA-Z0-9-_.~%]+"), "Malformed topic name"); + return topic; + } + /** * Creates a new {@link Message.Builder}. * @@ -162,10 +169,10 @@ public Builder setToken(String token) { } /** - * Sets the name of the FCM topic to which the message should be sent. Topic names must - * not contain the {@code /topics/} prefix. + * Sets the name of the FCM topic to which the message should be sent. Topic names may + * contain the {@code /topics/} prefix. * - * @param topic A valid topic name excluding the {@code /topics/} prefix. + * @param topic A valid topic name. * @return This builder. */ public Builder setTopic(String topic) { diff --git a/src/test/java/com/google/firebase/messaging/MessageTest.java b/src/test/java/com/google/firebase/messaging/MessageTest.java index 5fbb499c3..21caeeb18 100644 --- a/src/test/java/com/google/firebase/messaging/MessageTest.java +++ b/src/test/java/com/google/firebase/messaging/MessageTest.java @@ -62,6 +62,26 @@ public void testDataMessage() throws IOException { assertJsonEquals(ImmutableMap.of("topic", "test-topic", "data", data), message); } + @Test + public void testInvalidTopicNames() { + List invalidTopicNames = ImmutableList.of("/topics/", "/foo/bar", "foo bar"); + for (String topicName : invalidTopicNames) { + try { + Message.builder().setTopic(topicName).build(); + } catch (IllegalArgumentException expected) { + // expected + } + } + } + + @Test + public void testPrefixedTopicName() throws IOException { + Message message = Message.builder() + .setTopic("/topics/test-topic") + .build(); + assertJsonEquals(ImmutableMap.of("topic", "test-topic"), message); + } + @Test public void testNotificationMessage() throws IOException { Message message = Message.builder() From 5531a0893d21675823a6be0a75fd6bf372a9478e Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Mon, 12 Feb 2018 14:40:56 -0800 Subject: [PATCH 30/32] Updated FCM error codes (#138) --- .../com/google/firebase/messaging/FirebaseMessaging.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java index 905d1fe26..00b68c681 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java @@ -63,11 +63,12 @@ public class FirebaseMessaging { private static final String UNKNOWN_ERROR = "unknown-error"; private static final Map FCM_ERROR_CODES = ImmutableMap.builder() + .put("APNS_AUTH_ERROR", "authentication-error") + .put("INTERNAL", INTERNAL_ERROR) .put("INVALID_ARGUMENT", "invalid-argument") - .put("NOT_FOUND", "registration-token-not-registered") - .put("PERMISSION_DENIED", "authentication-error") - .put("RESOURCE_EXHAUSTED", "message-rate-exceeded") - .put("UNAUTHENTICATED", "authentication-error") + .put("UNREGISTERED", "registration-token-not-registered") + .put("QUOTA_EXCEEDED", "message-rate-exceeded") + .put("SENDER_ID_MISMATCH", "authentication-error") .put("UNAVAILABLE", "server-unavailable") .build(); static final Map IID_ERROR_CODES = From 7fc9fe0dc31bf938385692b2abdec4c5e8f13282 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Mon, 12 Feb 2018 18:29:26 -0800 Subject: [PATCH 31/32] Merged with dev; updated changelog --- CHANGELOG.md | 42 ++++++++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5791dadd6..f3a734bf3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,30 +1,40 @@ # Unreleased -### Token revocation -- [added] The [`verifyIdTokenAsync(...)`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/auth/FirebaseAuth.html#verifyIdTokenAsync) - method has an added signature that accepts a boolean `checkRevoked` parameter. When `true`, an - additional check is performed to see whether the token has been revoked. -- [added] A new method [`FirebaseAuth.revokeRefreshTokensAsync(uid)`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/auth/FirebaseAuth.html#revokeRefreshTokens) - has been added to invalidate all tokens issued to a user before the current second. -- [added] A new getter `getTokensValidAfterTimestamp()` has been added to the - [`UserRecord`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/auth/UserRecord), - which denotes the time in epoch milliseconds before which tokens are not valid. This is truncated to 1000 milliseconds. - -### Initialization - -- [fixed] The [`FirebaseOptions.Builder.setStorageBucket()`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/FirebaseOptions.Builder.html#setStorageBucket(java.lang.String)) - method now throws a clear exception when invoked with a bucket URL - instead of the name. - ### Cloud Messaging - [feature] Added the `FirebaseCloudMessaging` API for sending Firebase notifications and managing topic subscriptions. +### Authentication + +- [added] The [`verifyIdTokenAsync()`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/auth/FirebaseAuth.html#verifyIdTokenAsync) + method has an overload that accepts a boolean `checkRevoked` parameter. + When `true`, an additional check is performed to see whether the token + has been revoked. +- [added] A new [`revokeRefreshTokensAsync(uid)`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/auth/FirebaseAuth.html#revokeRefreshTokens) + method has been added to invalidate all tokens issued to a user. +- [added] A new getter `getTokensValidAfterTimestamp()` has been added + to the [`UserRecord`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/auth/UserRecord) + class, which denotes the time before which tokens are not valid. + ### Realtime Database - [fixed] Exceptions thrown by database event handlers are now logged. +### Initialization + +- [fixed] The [`FirebaseOptions.Builder.setStorageBucket()`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/FirebaseOptions.Builder.html#setStorageBucket(java.lang.String)) + method now throws a clear exception when invoked with a bucket URL + instead of the name. +- [fixed] Implemented a fix for a potential Guava version conflict which + was causing an `IllegalStateException` (precondition failure) in some + environments. + +### Cloud Firestore + +- [fixed] Upgraded the Cloud Firestore client to the latest available + version. + # v5.8.0 ### Initialization From 374a643e7719a0ba4bd780cf8ae648a438ebb42e Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Mon, 12 Feb 2018 18:29:57 -0800 Subject: [PATCH 32/32] Updated changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f3a734bf3..fb55a1953 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ method has an overload that accepts a boolean `checkRevoked` parameter. When `true`, an additional check is performed to see whether the token has been revoked. -- [added] A new [`revokeRefreshTokensAsync(uid)`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/auth/FirebaseAuth.html#revokeRefreshTokens) +- [added] A new [`revokeRefreshTokensAsync()`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/auth/FirebaseAuth.html#revokeRefreshTokens) method has been added to invalidate all tokens issued to a user. - [added] A new getter `getTokensValidAfterTimestamp()` has been added to the [`UserRecord`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/auth/UserRecord)