Skip to content

Conversation

@hiranya911
Copy link
Contributor

Implements the messaging related types and the following entry points:

FirebaseMessaging.getInstance()
FirebaseMessaging.getInstance(FirebaseApp app)
FirebaseMessaging.sendAsync(Message msg)
FirebaseMessaging.sendAsync(Message msg, boolean dryRun)
FirebaseMessaging.subscribeToTopicAsync(List<String> tokens, String topic)
FirebaseMessaging.unsubscribeFromTopicAsync(List<String> tokens, String topic)

go/firebase-admin-fcm-api

@jcvillalta03
Copy link

@hiranya911 I am wondering if there is a ticket some where of features desired in this PR. I have been working on an implementation myself in a personal project and would love to contribute if there is an actual list of requirements for the API

@hiranya911
Copy link
Contributor Author

@jcvillalta03 The initial offering is going to include the methods described above. If you have ideas for more features or further expansions, please feel free to create a new issue in this repo.

@jcvillalta03
Copy link

Thanks @hiranya911. So at this point, this is initial offering is considered “code complete”?

@hiranya911
Copy link
Contributor Author

@jcvillalta03 for the moment yes. Equivalent set of operations are being implemented in other Admin SDK variants (Node, Python and Go) as well. But you should feel encouraged to propose changes/additions on top of what I have done here. If a certain use case cannot be handled properly with the initial set of APIs, we'd like to know, and see if we can do something about it.

@jcvillalta03
Copy link

Ok, thanks for the explanation. Sorry to keeping conversation going in comments, I just wanted to make sure that I understand the Contribution guidelines.
#101 (which I believe this implements) was a high-level request, but now I see that the requirements are to match functionality of other Admin SDKs. 👍 on using HTTP v1.

I look forward to this release!

Copy link
Contributor

@schmidt-sebastian schmidt-sebastian left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, this is very good. I left some nits from an initial review that apply to a few places in this PR. It might make sense for you to do a first pass on these before I look at this in more depth.

@@ -0,0 +1,201 @@
/*
* Copyright 2017 Google Inc.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The line should now be "Copyright 2018 Google LLC"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Also added the header to a set of files that were missing it.


/**
* Represents the Android-specific options that can be included in a {@link Message}.
* Instances of this class are thread-safe and immutable.
Copy link
Contributor

@schmidt-sebastian schmidt-sebastian Jan 30, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does this show up in the JavaDoc output? I wonder if we should add a newline (or <p>).

Copy link
Contributor Author

@hiranya911 hiranya911 Jan 30, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They show up as a single paragraph. We already have a quite a bit of similarly documented classes (See UserRecord class from auth for example)

this.ttl = String.format("%ds", seconds);
}
} else {
this.ttl = null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: This is not needed since it is the default (here and above)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is needed since it's final

* @return This builder.
*/
public Builder putData(@NonNull String key, @NonNull String value) {
this.data.put(key, value);
Copy link
Contributor

@schmidt-sebastian schmidt-sebastian Jan 30, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't override the existing value. Do you instead mean that the final AndroidConfig's data map will be overridden?

Copy link
Contributor Author

@hiranya911 hiranya911 Jan 30, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It overrides the data set using the Message.Builder class. It's an FCM mechanic executed at the backend. In other words AndroidConfig.data map gets priority over Message.data map when FCM sends notifications. I've slightly modified the text to make this clear.

this.body = builder.body;
this.icon = builder.icon;
if (builder.color != null) {
checkArgument(builder.color.matches("^#[0-9a-fA-F]{6}$"),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion (here and below):

this.color = checkArgument(...)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually checkArgument() does not return anything. Only checkNotNull() returns the input argument.

private final String threadId;

private Aps(Builder builder) {
int alerts = Booleans.countTrue(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about:

checkArgument(Strings.isNullOrEmpty(builder.alertString) || builder.alert == null, ...)

Copy link
Contributor Author

@hiranya911 hiranya911 Jan 30, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


/**
* 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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This reads a little weird since it is not possible to not specify this value using this API. Can you make it more clear that you mean "do not call this method"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

}

/**
* Sends the given {@link Message} via Firebase Cloud Messaging. If the {@code dryRun} option
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Insert new paragraph after the first sentence.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


// 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<String, String> ERROR_CODES = ImmutableMap.<String, String>builder()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move these to a single place so the Admin SDK can share these errors?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The backend error codes (keys of the map) are specific to the services we invoke at various places. I'm not sure there's a lot to be gained by sharing them across the package/SDK. What sort of sharing were you thinking of? Put them all in a shared FirebaseMessagingConstants class?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that's what I was leaning towards. It looks like https://github.com/firebase/firebase-admin-java/blob/master/src/main/java/com/google/firebase/iid/FirebaseInstanceId.java uses HTTP Response Codes though and not these same errors.

}
}
this.successCount = successCount;
this.failureCount = failureCount;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just errors.size().

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@hiranya911
Copy link
Contributor Author

Thanks @schmidt-sebastian and @sampson-chen for the initial round of feedback. I've addressed almost all the comments. Over to you for another look.

Copy link
Contributor

@avishalom avishalom left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM(nit and question)

Message message = Message.builder()
.setNotification(new Notification("Title", "Body"))
.setAndroidConfig(AndroidConfig.builder()
.setRestrictedPackageName("com.demoapps.hkj")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

name?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed


private String makeSendRequest(Message message,
boolean dryRun) throws FirebaseMessagingException {
ImmutableMap.Builder<String, Object> payload = ImmutableMap.<String, Object>builder()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider ImmutableMap.of(...)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have to use Builder here since we are conditionally putting some values.

FirebaseMessaging messaging = FirebaseMessaging.getInstance();
TopicManagementResponse results = messaging.subscribeToTopicAsync(
ImmutableList.of(TEST_REGISTRATION_TOKEN), "mock-topic").get();
assertEquals(1, results.getSuccessCount() + results.getFailureCount());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there no point in testing success and failure separately like in the next FirebaseMessagingTest.java?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no meaningful way to test success case since the IID tokens keep changing.

* 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.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/crete/create/

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

avishalom
avishalom approved these changes Feb 2, 2018
if (builder.ttl != null) {
checkArgument(builder.ttl >= 0, "ttl must not be negative");
long seconds = TimeUnit.MILLISECONDS.toSeconds(builder.ttl);
long subsecondNanos = TimeUnit.MILLISECONDS.toNanos(builder.ttl - seconds * 1000L);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be 1000 * 1000.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

builder.ttl is in millis. So I believe this is correct. We have a test case for this where 10ms is correctly transformed to 0.010000000s (where the decimal part is subsecond nanos).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I think I had nanoseconds in my head.


@Override
public void destroy() {
// NOTE: We don't explicitly tear down anything here, but public methods of StorageClient
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

StorageClient -> FirebaseMessaging

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

/**
* 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/one of token, topic or condition parameters/one token, topic or condition parameter/

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


// 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<String, String> ERROR_CODES = ImmutableMap.<String, String>builder()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that's what I was leaning towards. It looks like https://github.com/firebase/firebase-admin-java/blob/master/src/main/java/com/google/firebase/iid/FirebaseInstanceId.java uses HTTP Response Codes though and not these same errors.

Map<Message, Map<String, Object>> testMessages = buildTestMessages();

for (Map.Entry<Message, Map<String, Object>> entry : testMessages.entrySet()) {
response.setContent("{\"name\": \"mock-name\"}");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you remove the code duplication in this test its counterpart?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

MockLowLevelHttpResponse response = new MockLowLevelHttpResponse();
FirebaseMessaging messaging = initMessaging(response);
for (int code : HTTP_ERRORS) {
response.setStatusCode(code).setContent(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same for these tests. The code here could use some de-duping.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


ByteArrayOutputStream out = new ByteArrayOutputStream();
request.getContent().writeTo(out);
assertEquals("{\"to\":\"/topics/test-topic\",\"registration_tokens\":[\"id1\",\"id2\"]}",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems overly harsh as it relies on an unspecified order during the serialization.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed by parsing into a map, and then comparing entries.

assertEquals(TEST_IID_UNSUBSCRIBE_URL, request.getUrl().toString());
assertEquals("Bearer test-token", request.getHeaders().getAuthorization());
assertEquals("true", request.getHeaders().get("access_token_auth"));

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lines from 343-356 also seem duplicated to me.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

.build();
FirebaseApp.initializeApp(options);
FirebaseMessaging messaging = FirebaseMessaging.getInstance();
TestResponseInterceptor interceptor = new TestResponseInterceptor();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can 314-319 be done in the @before method?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to a helper method.

@hiranya911
Copy link
Contributor Author

Made the suggested changes.

@hiranya911 hiranya911 merged commit f83cdec into master Feb 13, 2018
@hiranya911 hiranya911 deleted the hkj-fcm-api branch February 13, 2018 02:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants