Google GCM XMPP code example
So Google used to have a nice page where they provided code examples on how to implement a GCM Xmpp server on that page:
https://developers.google.com/cloud-messaging/ccs
Whenever I want to see the implementation, I’ve bookmarked this answer
Confused about Google GCM XMPP
But everything linked in that answer is gone. Google removed them.
But now, they removed everything and added a page to explain what it does. Sure, but sample Java code would be nice. I tried to access the sample site on github that they linked to
https://github.com/google/gcm
However, it only provides an example of the HTTP GCM server version.
Can someone point me to a valid code sample? Or is there a library that can be used with C#? If not, I would also choose the Java version.
Thank you.
Solution
This is the
one I wrote for the class example (removing some non-XMPP parts to make it small enough for me to publish code, but the full server is in http://people.eku.edu/styere/GcmPushServer.java ):
// derived from https://developer.android.com/google/gcm/ccs.html
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.JScrollPane;
import javax.swing.SwingUtilities;
import org.jivesoftware.smack.ConnectionConfiguration;
import org.jivesoftware.smack.ConnectionConfiguration.SecurityMode;
import org.jivesoftware.smack.ConnectionListener;
import org.jivesoftware.smack.PacketInterceptor;
import org.jivesoftware.smack.PacketListener;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.SmackException.NotConnectedException;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.filter.PacketTypeFilter;
import org.jivesoftware.smack.packet.DefaultPacketExtension;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.PacketExtension;
import org.jivesoftware.smack.provider.PacketExtensionProvider;
import org.jivesoftware.smack.provider.ProviderManager;
import org.jivesoftware.smack.tcp.XMPPTCPConnection;
import org.jivesoftware.smack.util.StringUtils;
import org.json.simple.JSONValue;
import org.json.simple.JSONObject;
import org.json.simple.parser.ParseException;
import org.xmlpull.v1.XmlPullParser;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Random;
import java.net.*;
import java.io.*;
import java.util.*;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.HttpsURLConnection;
list of all users and associated devices
class UserList {
(--deleted--)
constructor/read existing data from file
public UserList() {
(--deleted--)
}
add a new device to an existing user (called from networking thread)
duplicate IDs are quietly accepted
public synchronized void addDevice( String uname, String newRegToken ) {
(--deleted--)
}
generate a group address/notification key
// !!!!!!!!!!!!!!! W A R N I N G !!!!!!!!!!!!!!!
If the group addr/notification key is lost, it currently CANNOT be recovered/rebuilt
public String createNotificationKey( String nKeyName, String addr ) {
String[] idset = new String[1];
idset[0] = addr;
String newGroupAddr;
try {
create a https connection to create the notification_key
URL url = new URL( "https://android.googleapis.com/gcm/notification" );
HttpsURLConnection conn = (HttpsURLConnection)url.openConnection();
send as a POST request
conn.setRequestMethod("POST");
add request parameters
conn.addRequestProperty( "project_id", ""+GcmPushServer.senderId );
conn.addRequestProperty( "Authorization", "key="+GcmPushServer.password );
use "set" since this may already exist
conn.setRequestProperty( "Content-Type", "application/json" );
create data to send with request
Map<String,Object> sendData = new HashMap<>();
create a new notification key
sendData.put( "operation", "create" );
sendData.put( "notification_key_name", (--deleted--) );
sendData.put( "registration_ids", (--deleted--) );
String strData = JSONValue.toJSONString(sendData);
Send post request
conn.setDoOutput(true);
DataOutputStream wr = new DataOutputStream(conn.getOutputStream());
wr.writeBytes( strData );
wr.flush();
wr.close();
successful?
int rc = conn.getResponseCode();
System.out.println("ResponseCode = " + rc );
read the response
Scanner input = new Scanner( conn.getInputStream() );
String resp="";
read/display lines
while( input.hasNextLine() ) {
String s = input.nextLine();
resp += s;
}
System.out.println("Response content: " + resp);
if ( rc == HttpsURLConnection.HTTP_OK ) {
JSONObject obj = (JSONObject) JSONValue.parse(resp);
newGroupAddr = (String) obj.get( "notification_key" );
System.out.println(" new notification_key: " + newGroupAddr );
return newGroupAddr;
} else {
}
} catch ( Exception e ) {
System.out.println("-- Exception: " + e.getMessage() );
}
return null;
}
add/delete a single address to/from a notification key
(add if doAdd is true, delete otherwise)
removing final address will quietly delete the key
public static void modifyNotificationKey( String nKeyName, String nKey,
String addr, boolean doAdd ) {
String[] idset = new String[1];
idset[0] = addr;
try {
create a https connection to create the notification_key
URL url = new URL( "https://android.googleapis.com/gcm/notification" );
HttpsURLConnection conn = (HttpsURLConnection)url.openConnection();
send as a POST request
conn.setRequestMethod("POST");
add request parameters
conn.addRequestProperty( "project_id", ""+GcmPushServer.senderId );
conn.addRequestProperty( "Authorization", "key="+GcmPushServer.password );
use "set" since this may already exist
conn.setRequestProperty( "Content-Type", "application/json" );
create data to send with request
Map<String,Object> sendData = new HashMap<>();
create a new notification key
sendData.put( "operation", doAdd? "add" : "remove" ); add or delete key?
sendData.put( "notification_key_name", (--deleted--) );
sendData.put( "notification_key", (--deleted--) );
sendData.put( "registration_ids", (--deleted--) );
String strData = JSONValue.toJSONString(sendData);
System.out.println("genGroupAddress POST data: " + strData );
Send post request
conn.setDoOutput(true);
DataOutputStream wr = new DataOutputStream(conn.getOutputStream());
wr.writeBytes( strData );
wr.flush();
wr.close();
conn.openConnection();
successful?
int rc = conn.getResponseCode();
System.out.println("ResponseCode = " + rc );
Scanner input = new Scanner( conn.getInputStream() );
read/display lines
while( input.hasNextLine() ) {
System.out.println("Response content:");
String s = input.nextLine();
System.out.println( s );
}
} catch ( Exception e ) {
do nothing
}
}
}
text area for log messages
class LogTextArea extends JTextArea {
update the "log" with the specified message
public void logMessage( String msg ) {
(--deleted--)
}
}
class CcsServer {
private static final String GCM_SERVER = "gcm.googleapis.com";
private static final int GCM_PORT = 5235;
private static final String GCM_ELEMENT_NAME = "gcm";
private static final String GCM_NAMESPACE = "google:mobile:data";
display/log area
LogTextArea logArea;
UserList users;
CcsServer( LogTextArea lta, UserList u ) {
logArea = lta;
users = u;
}
static {
ProviderManager.addExtensionProvider(GCM_ELEMENT_NAME, GCM_NAMESPACE,
new PacketExtensionProvider() {
@Override
public PacketExtension parseExtension(XmlPullParser parser) throws
Exception {
String json = parser.nextText();
return new GcmPacketExtension(json);
}
});
}
private XMPPConnection connection;
/**
* Indicates whether the connection is in draining state, which means that it
* will not accept any new downstream messages.
*/
protected volatile boolean connectionDraining = false;
/**
* Sends a downstream message to GCM.
*
* @return true if the message has been successfully sent.
*/
public boolean sendDownstreamMessage(String jsonRequest) throws
NotConnectedException {
if (!connectionDraining) {
send(jsonRequest);
return true;
}
logArea.logMessage("Dropping downstream message since the connection is draining");
return false;
}
/**
* Returns a random message id to uniquely identify a message.
*
* <p>Note: This is generated by a pseudo random number generator for
* illustration purpose, and is not guaranteed to be unique.
*/
public String nextMessageId() {
return "m-" + UUID.randomUUID().toString();
}
/**
* Sends a packet with contents provided.
*/
protected void send(String jsonRequest) throws NotConnectedException {
Packet request = new GcmPacketExtension(jsonRequest).toPacket();
connection.sendPacket(request);
}
/**
* Handles an upstream data message from a device application.
*
* <p>This sample echo server sends an echo message back to the device.
* Subclasses should override this method to properly process upstream messages.
*/
protected void handleUpstreamMessage(Map<String, Object> jsonObject) {
PackageName of the application that sent this message.
String category = (String) jsonObject.get("category");
String from = (String) jsonObject.get("from");
@SuppressWarnings("unchecked")
Map<String, String> payload = (Map<String, String>) jsonObject.get("data");
what type of upstream message is this?
if ( ! payload.containsKey( "my_action" ) )
{
Hmmm - this shouldn't happen!
logArea.logMessage( "handleUpstreamMessage - incoming message is missing my_action" );
just ignore the message
return;
}
what action do they want?
String my_action = (String) payload.get( "my_action" );
if ( my_action.equals( "edu.eku.styere.gcmpushclient.REGISTER" ) ) {
registration request
String username = (String) payload.get( "username" );
logArea.logMessage( "Registration request: user=" + username + ", ID/Token=" + from );
save the information
users.addDevice( username, from );
return;
} else {
take default action of echoing the message
payload.put("ECHO", "Application: " + category);
Send an ECHO response back
String echo = createJsonMessage(from, nextMessageId(), payload,
"echo:CollapseKey", null, false);
try {
sendDownstreamMessage(echo);
} catch (NotConnectedException e) {
logArea.logMessage( "Not connected anymore, echo message is not sent: " + e.getMessage() );
}
}
}
/**
* Handles an ACK.
*
* <p>Logs a INFO message, but subclasses could override it to
* properly handle ACKs.
*/
protected void handleAckReceipt(Map<String, Object> jsonObject) {
String messageId = (String) jsonObject.get("message_id");
String from = (String) jsonObject.get("from");
logArea.logMessage( "handleAckReceipt() from: " + from + ", messageId: " + messageId );
}
/**
* Handles a NACK.
*
* <p>Logs a INFO message, but subclasses could override it to
* properly handle NACKs.
*/
protected void handleNackReceipt(Map<String, Object> jsonObject) {
String messageId = (String) jsonObject.get("message_id");
String from = (String) jsonObject.get("from");
logArea.logMessage( "handleNackReceipt() from: " + from + ", messageId: " + messageId );
}
protected void handleControlMessage(Map<String, Object> jsonObject) {
logArea.logMessage( "handleControlMessage(): " + jsonObject );
String controlType = (String) jsonObject.get("control_type");
if ("CONNECTION_DRAINING".equals(controlType)) {
connectionDraining = true;
} else {
logArea.logMessage( "Unrecognised control type: " + controlType + ". This could "+
"happen if new features are " + "added to the CCS protocol." );
}
}
/**
* Creates a JSON encoded GCM message.
*
* @param to RegistrationId of the target device (Required).
* @param messageId Unique messageId for which CCS sends an
* "ack/nack" (Required).
* @param payload Message content intended for the application. (Optional).
* @param collapseKey GCM collapse_key parameter (Optional).
* @param timeToLive GCM time_to_live parameter (Optional).
* @param delayWhileIdle GCM delay_while_idle parameter (Optional).
* @return JSON encoded GCM message.
*/
public static String createJsonMessage(String to, String messageId,
Map<String, String> payload, String collapseKey, Long timeToLive,
Boolean delayWhileIdle) {
Map<String, Object> message = new HashMap<String, Object>();
message.put("to", to);
if (collapseKey != null) {
message.put("collapse_key", collapseKey);
}
if (timeToLive != null) {
message.put("time_to_live", timeToLive);
}
if (delayWhileIdle != null && delayWhileIdle) {
message.put("delay_while_idle", true);
}
message.put("message_id", messageId);
message.put("data", payload);
return JSONValue.toJSONString(message);
}
/**
* Creates a JSON encoded ACK message for an upstream message received
* from an application.
*
* @param to RegistrationId of the device who sent the upstream message.
* @param messageId messageId of the upstream message to be acknowledged to CCS.
* @return JSON encoded ack.
*/
protected static String createJsonAck(String to, String messageId) {
Map<String, Object> message = new HashMap<String, Object>();
message.put("message_type", "ack");
message.put("to", to);
message.put("message_id", messageId);
return JSONValue.toJSONString(message);
}
/**
* Connects to GCM Cloud Connection Server using the supplied credentials.
*
* @param senderId Your GCM project number
* @param apiKey API Key of your project
*/
public void connect(long senderId, String apiKey)
throws XMPPException, IOException, SmackException {
ConnectionConfiguration config =
new ConnectionConfiguration(GCM_SERVER, GCM_PORT);
config.setSecurityMode(SecurityMode.enabled);
config.setReconnectionAllowed(true);
config.setRosterLoadedAtLogin(false);
config.setSendPresence(false);
config.setSocketFactory(SSLSocketFactory.getDefault());
connection = new XMPPTCPConnection(config);
connection.connect();
connection.addConnectionListener( new LoggingConnectionListener() );
Handle incoming packets
connection.addPacketListener(new PacketListener() {
@Override
public void processPacket(Packet packet) {
logArea.logMessage( "Received: " + packet.toXML() );
Message incomingMessage = (Message) packet;
GcmPacketExtension gcmPacket =
(GcmPacketExtension) incomingMessage.
getExtension(GCM_NAMESPACE);
String json = gcmPacket.getJson();
try {
@SuppressWarnings("unchecked")
Map<String, Object> jsonObject =
(Map<String, Object>) JSONValue.
parseWithException(json);
present for "ack"/"nack", null otherwise
Object messageType = jsonObject.get("message_type");
if (messageType == null) {
Normal upstream data message
handleUpstreamMessage(jsonObject);
Send ACK to CCS
String messageId = (String) jsonObject.get("message_id");
String from = (String) jsonObject.get("from");
String ack = createJsonAck(from, messageId);
send(ack);
} else if ("ack".equals(messageType.toString())) {
Process Ack
handleAckReceipt(jsonObject);
} else if ("nack".equals(messageType.toString())) {
Process Nack
handleNackReceipt(jsonObject);
} else if ("control".equals(messageType.toString())) {
Process control message
handleControlMessage(jsonObject);
} else {
logArea.logMessage( "Unrecognised message type: " +
messageType.toString() );
}
} catch (ParseException e) {
logArea.logMessage( "Error parsing JSON " + json );
} catch (Exception e) {
logArea.logMessage( "Failed to process packet" );
}
}
}, new PacketTypeFilter(Message.class));
Log all outgoing packets
connection.addPacketInterceptor(new PacketInterceptor() {
@Override
public void interceptPacket(Packet packet) {
logArea.logMessage( "Sent: " + packet.toXML());
}
}, new PacketTypeFilter(Message.class));
connection.login(senderId + "@gcm.googleapis.com", apiKey);
}
---------- support classes ----------
/**
* XMPP Packet Extension for GCM Cloud Connection Server.
*/
private static class GcmPacketExtension extends DefaultPacketExtension {
private final String json;
public GcmPacketExtension(String json) {
super( GCM_ELEMENT_NAME, GCM_NAMESPACE);
this.json = json;
}
public String getJson() {
return json;
}
@Override
public String toXML() {
return String.format("<%s xmlns=\"%s\">%s</%s>",
GCM_ELEMENT_NAME, GCM_NAMESPACE,
StringUtils.escapeForXML(json), GCM_ELEMENT_NAME);
}
public Packet toPacket() {
Message message = new Message();
message.addExtension(this);
return message;
}
}
class LoggingConnectionListener implements ConnectionListener {
@Override
public void connected(XMPPConnection xmppConnection) {
logArea.logMessage( "Connected." );
}
@Override
public void authenticated(XMPPConnection xmppConnection) {
logArea.logMessage( "Authenticated." );
}
@Override
public void reconnectionSuccessful() {
logArea.logMessage( "Reconnecting.." );
}
@Override
public void reconnectionFailed(Exception e) {
logArea.logMessage( "Reconnection failed.. " + e.getMessage() );
}
@Override
public void reconnectingIn(int seconds) {
logArea.logMessage( "Reconnecting in " + seconds + " secs" );
}
@Override
public void connectionClosedOnError(Exception e) {
logArea.logMessage( "Connection closed on error." );
}
@Override
public void connectionClosed() {
logArea.logMessage( "Connection closed." );
}
}
}
a non-editable combo (drop-down) box for destination addresses
class DestComboBox extends JComboBox<String> {
(deleted to save space)
}
screen panel for sending a message
class SendMessagePanel extends JPanel implements ActionListener, ItemListener {
Message Types
protected final int MSGTYPE_NOTIFICATION = 1;
protected final int MSGTYPE_NOTIFICATION_DATA = 2;
protected final int MSGTYPE_DATA_COLLAPSE = 3;
protected final int MSGTYPE_DATA_NONCOLLAPSE = 4;
log window
LogTextArea msgs;
server class
CcsServer ccsServer;
constructor
SendMessagePanel( UserList u, LogTextArea m, CcsServer c ) {
(deleted to save space)
}
respond to the button
@Override
public void actionPerformed(ActionEvent e) {
String toAddr = "zzzzzz"; destination address
may be device, notification_key, or topic address
ttl = 2419200; time-to-live in seconds
what type of message?
int msgTypeIndex = (--deleted--);
create the message
Map<String, Object> message = new HashMap<String, Object>();
message.put("to", toAddr);
if ( msgTypeIndex == MSGTYPE_NOTIFICATION ||
msgTypeIndex == MSGTYPE_NOTIFICATION_DATA ||
msgTypeIndex == MSGTYPE_DATA_COLLAPSE ) {
create a collapse key
message.put("collapse_key", "ck"+msgTypeIndex );
}
message.put("time_to_live", ttl);
message.put("message_id", ccsServer.nextMessageId());
notification included?
if ( msgTypeIndex == MSGTYPE_NOTIFICATION ||
msgTypeIndex == MSGTYPE_NOTIFICATION_DATA ) {
create the notification payload
HashMap<String, String> notePayload = new HashMap<>();
notePayload.put( "title", "Gcm Push Message Example" );
notePayload.put( "body", (--deleted--) );
identify which notifications should replace older versions
notePayload.put( "tag", "ntag" + msgTypeIndex );
notePayload.put( "icon", "@drawable/new_picture" ); notification icon
additional stuff if we also have data
if ( msgTypeIndex == MSGTYPE_NOTIFICATION_DATA ) {
what to do when the user opens the notification
notePayload.put( "click_action", "OPEN_MAIN_ACTIVITY" );
}
message.put( "notification", notePayload );
}
data included?
if ( msgTypeIndex == MSGTYPE_NOTIFICATION_DATA ||
msgTypeIndex == MSGTYPE_DATA_COLLAPSE ||
msgTypeIndex == MSGTYPE_DATA_NONCOLLAPSE ) {
HashMap<String, String> dataPayload = new HashMap<>();
dataPayload.put( "contents", (--deleted--) );
message.put("data", dataPayload);
}
actually send the message
try {
ccsServer.sendDownstreamMessage( JSONValue.toJSONString(message) );
} catch (NotConnectedException enc ) {
msgs.logMessage( "Not connected anymore, echo message is not sent: " + enc.getMessage() );
}
}
}
class BorderPanel extends JPanel {
(--deleted--)
}
public class GcmPushServer
{
(--deleted--)
}