2

I am trying to get Google Cloud Messaging (GCM) to work on Android with local device groups as described on https://developers.google.com/cloud-messaging/android/client-device-group .

I have enabled the following services in the web console ( https://console.developers.google.com ):

  • Google Cloud Messaging for Android
  • Google Cloud Pub/Sub
  • Google+ API

I have also created the following credentials:

  • API key (with specified package name and SHA-1 certificate keyprint)
  • OAuth 2.0 client IDs (Web application)

My Android code looks as follows (error handling etc. has been stripped):

String account = ACCOUNT_NAME; // E.g. "johndoe@google.com"
String scope = "audience:server:client_id:" + WEB_APPLICATION_CLIENT_ID;
String token = GoogleAuthUtil.getToken(context, account, scope);

// Token is successfully found here :-)

URL url = new URL("https://android.googleapis.com/gcm/notification");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("POST");
connection.setDoOutput(true);
connection.setDoInput(true);
connection.setRequestProperty("project_id", NUMERICAL_PROJECT_ID);
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("Accept", "application/json");
connection.connect();

JSONObject requestBody = new JSONObject();
requestBody.put("operation", "add");
requestBody.put("notification_key_name", "foobar");
requestBody.put("registration_ids", 
  new JSONArray(Arrays.asList(new String[]{"42", "44"})));
requestBody.put("id_token", token);

OutputStream os = connection.getOutputStream();
os.write(requestBody.toString().getBytes("UTF-8"));
os.close();

// connection.getResponseCode() is now 401

The submitted JSON to https://android.googleapis.com/gcm/notification looks something like this:

{
  "operation": "add",
  "notification_key_name": "foobar",
  "registration_ids": ["42","44"],
  "id_token": "[veeeeeery long string]"
}

The response contents is:

<HTML>
<HEAD>
<TITLE>Unauthorized</TITLE>
</HEAD>
<BODY BGCOLOR="#FFFFFF" TEXT="#000000">
<H1>Unauthorized</H1>
<H2>Error 401</H2>
</BODY>
</HTML>

What am I doing wrong?

Morten
  • 684
  • 1
  • 5
  • 15
  • 1
    Just to be sure, have run the Part 4 sample from https://developers.google.com/cloud-messaging/android/start – g90 Dec 04 '15 at 23:33
  • 1
    It's clear that you have made a thorough effort to get this working. I also tried and failed. There are two other SO questions for the same issue, [one recent](http://stackoverflow.com/questions/33895099/gcm-client-based-device-group-management) and [the other](http://stackoverflow.com/questions/26242452/google-cloud-messaging-401-unauthorized-is-returned-when-creating-notification) from a year ago. Neither have a solution. The [instructions](https://developers.google.com/cloud-messaging/android/client-device-group) are clearly wrong or incomplete. Very frustrating! – Bob Snyder Dec 05 '15 at 21:22
  • Yes. I started out from the examples and have attempted to move it to local device groups, since I don't want to have an unnecessary app server in the solution architecture. The examples assume that you have an app server available. – Morten Dec 06 '15 at 11:38

3 Answers3

2

Found the trick: you are using a google account to take the id_token, you need to use EXACTLY the email as notification_key_name. So if you are using foo@gmail.com, you need to use this address as notification_key_name.

greywolf82
  • 21,813
  • 18
  • 54
  • 108
  • Spamming the same answer across multiple answers is not the appropriate course to take. If you can copy and paste an answer, the questions are likely duplicates. Vote to close them as such. – Andy Dec 15 '15 at 19:59
  • 1
    I tried but I can't, I don't know why I can't found the response to mark as duplicate. Sorry. Since there are a LOT of people looking for a response is better to spam it on all related questions. If a moderator wants to delete them is ok for me. I just tried to help other developers. Edit: it seems you can't mark it as duplicate because the original question must have an accepted answer. – greywolf82 Dec 15 '15 at 20:14
  • You are absolutely right. This did the trick. Thank you very much! -- Just a curious question: How did you ever figure that out? – Morten Dec 15 '15 at 20:39
  • Side note: I have notified Google that they should update the documentation with this info. :-) – Morten Dec 15 '15 at 21:13
2

Here's the recap, based on the correct answer by greywolf82. The correct code should follow these principles (error handling etc. has been stripped):

///////////////////////////////////////////////////////////////////////////
// Working example on how to create a locally (client-side) managed      //
// device group for Google Cloud Messaging.                              //
//                                                                       //
// Thanks to greywolf82 for adding the final piece.                      //
// Please vote on his answer. Thank you!                                 //
///////////////////////////////////////////////////////////////////////////

// Get token:
String account = ACCOUNT_NAME; // E.g. "johndoe@gmail.com"
String scope = "audience:server:client_id:" + WEB_APPLICATION_CLIENT_ID;
String idToken = GoogleAuthUtil.getToken(context, account, scope);

// Get registration id:
InstanceID instanceID = InstanceID.getInstance(this);
String registration_id = instanceID.getToken(
        getString(R.string.gcm_defaultSenderId),
        GoogleCloudMessaging.INSTANCE_ID_SCOPE, null);

// Set up HTTP connection:
URL url = new URL("https://android.googleapis.com/gcm/googlenotification");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("POST");
connection.setDoOutput(true);
connection.setDoInput(true);
connection.setRequestProperty("project_id", NUMERICAL_PROJECT_ID);
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("Accept", "application/json");
connection.connect();

JSONObject requestBody = new JSONObject();
requestBody.put("operation", "add");
requestBody.put("notification_key_name", ACCOUNT_NAME); // You *must* use the email!
requestBody.put("registration_ids",
    new JSONArray(Arrays.asList(new String[]{registration_id})));
requestBody.put("id_token", idToken);

// Submit request body
OutputStream os = connection.getOutputStream();
os.write(requestBody.toString().getBytes("UTF-8"));
os.close();

// connection.getResponseCode() is now 200  :-)
// Now read the server response contents from connection.getInputStream()

The submitted JSON to https://android.googleapis.com/gcm/notification looks something like this:

{
  "operation": "add",
  "notification_key_name": "johndoe@gmail.com",
  "registration_ids": ["very long string here"],
  "id_token": "another very long string"
}

The response contents is:

{
  "notification_key": "your notification key is here --- voilá!"
}
Morten
  • 684
  • 1
  • 5
  • 15
0

This is how it is done using the SERVER_API_KEY (not best practice, but the best I have arrived at yet):

First, get an application instance id:

InstanceID instanceID = InstanceID.getInstance(this);
String token = instanceID.getToken(getString(R.string.gcm_defaultSenderId),
    GoogleCloudMessaging.INSTANCE_ID_SCOPE, null);

// Variable "token" now has the instance id. :-)

Next, get the messaging id token:

String account = ACCOUNT_NAME; // E.g. "johndoe@google.com"
String scope = "audience:server:client_id:" + WEB_APPLICATION_CLIENT_ID;
String idToken = GoogleAuthUtil.getToken(context, account, scope);

// Variable "idToken" now hods the messaging id token. :-)

Now to the magic part. Create a new device group with the instance Id as a member:

URL url = new URL("https://gcm-http.googleapis.com/gcm/notification"); // <-- Use this URL!
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("POST");
connection.setDoOutput(true);
connection.setDoInput(true);
connection.setRequestProperty("Authorization", "key=" + SERVER_API_KEY); // <--- Auth!
connection.setRequestProperty("project_id", NUMERICAL_PROJECT_ID);
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("Accept", "application/json");
connection.connect();

JSONObject requestBody = new JSONObject();
requestBody.put("operation", "create");               // <--- Not "add"
requestBody.put("notification_key_name", "foobar");
requestBody.put("registration_ids", 
    new JSONArray(Arrays.asList(new String[]{token}))); // <--- Instance Id token here!
requestBody.put("id_token", idToken);

OutputStream os = connection.getOutputStream();
os.write(requestBody.toString().getBytes("UTF-8"));
os.close();

InputStream is = connection.getInputStream();
String responseString = new Scanner(is, "UTF-8").useDelimiter("\\A").next();
is.close();

JSONObject response = new JSONObject(responseString);
Log.d(TAG, "Server response:\n" + response.toString(4));
String notificationKey = response.getString("notification_key");

At this point, the variable "notificationKey" holds the notification key which is the recipient that must be used when sending messages to this device group.

Send a message like this (use the "notificationKey" value as parameter as receipient from above):

private void sendMessage(String recipient) throws IOException, JSONException {

    Log.i(TAG, "Sending message to: " + recipient);
    URL url = new URL("https://gcm-http.googleapis.com/gcm/send");
    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
    connection.setRequestMethod("POST");
    connection.setDoOutput(true);
    connection.setDoInput(true);

    Log.d(TAG, "Opening connection to " + url.toString());

    connection.setRequestProperty("Authorization", "key=" + SERVER_API_KEY);
    connection.setRequestProperty("Content-Type", "application/json");
    connection.setRequestProperty("Accept", "application/json");
    connection.connect();

    JSONObject requestBody = new JSONObject();
    requestBody.put("to", recipient);
    JSONObject requestData = new JSONObject();
    requestData.put("hello", "Hello World!");
    requestData.put("hellodata", "42");
    requestBody.put("data", requestData);

    Log.d(TAG, "Request body:\n" + requestBody.toString(4));

    OutputStream os = connection.getOutputStream();
    os.write(requestBody.toString().getBytes("UTF-8"));
    os.close();

    Log.d(TAG, "Response code: " + connection.getResponseCode());

    InputStream is = connection.getInputStream();
    String responseString = new Scanner(is, "UTF-8").useDelimiter("\\A").next();
    is.close();
}

I have not succeeded in doing this without using the server API key. It might be worthwhile to investigate with different Authorization header values on the https://http.googleapis.com/gcm/googlenotification endpoint.

Hopefully this makes sense. Have fun. :-)

Morten
  • 684
  • 1
  • 5
  • 15
  • 1
    Well--good work, but this doesn't really answer the question that you originally posed. The instructions linked in your question are for client-side management of a device group. Your solution is using processing intended to run on a secure server to create the group. Because that processing uses an API Key, putting it in a client device is not secure. A hacker could decompile the code to get the key. (continued) – Bob Snyder Dec 06 '15 at 23:31
  • 2
    The instructions linked in your question state: "Managing device groups on the client is useful for cases where a server is unavailable. ... Note that the process for creating a notification key on the client is significantly different from the server-side process described in [Device Group Messaging](https://developers.google.com/cloud-messaging/notifications)". My understanding of the docs is that it should be possible for clients to create a group and send messages using only an OAuth web application client ID -- no API key. – Bob Snyder Dec 06 '15 at 23:32
  • At the end you are emulating the server. It could work but it's not the same thing as client side solution. I have tried using different authorization keys (even if it's not needed reading the docs), I tried to change the token, I tried with different accounts, nothing, nothing, nothing. It seems completely broken. – greywolf82 Dec 12 '15 at 14:42
  • Agree. It seems broken. -- I'm eagerly awaiting an email response from GCM developer support, who has responded a couple of times to my questions (referring to this exact thread). Now, I've kindly asked for a working example, but I haven't heard anything yet. We can only hope.... :-/ – Morten Dec 13 '15 at 21:03
  • @MortenE.Rasmussen Is there an email I can use to send help requests? – greywolf82 Dec 14 '15 at 10:35
  • I just received this email answer from Google: "Thanks for your patience and apologies for not getting back to you sooner. We're looking into this and will get back to you with any updates on our end." -- I received the answer from `gcm-dev-support@google.com`. – Morten Dec 14 '15 at 19:46