Skip to content

Instantly share code, notes, and snippets.

@judepereira
Created March 31, 2017 09:28
Show Gist options
  • Save judepereira/fd8dc0a5321179b699f5c5e54812770c to your computer and use it in GitHub Desktop.
Save judepereira/fd8dc0a5321179b699f5c5e54812770c to your computer and use it in GitHub Desktop.
XMPP/CCS client for FCM/GCM, using Smack 4.2
import com.mongodb.BasicDBObject;
import com.mongodb.util.JSON;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.StanzaListener;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.filter.StanzaTypeFilter;
import org.jivesoftware.smack.packet.ExtensionElement;
import org.jivesoftware.smack.packet.StandardExtensionElement;
import org.jivesoftware.smack.packet.Stanza;
import org.jivesoftware.smack.roster.Roster;
import org.jivesoftware.smack.tcp.XMPPTCPConnection;
import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration;
import org.jivesoftware.smack.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.SSLSocketFactory;
import java.io.IOException;
import java.util.HashMap;
import java.util.concurrent.ConcurrentHashMap;
public class CcsClient {
private static final String NAMESPACE_GOOGLE = "google:mobile:data";
private static final Logger logger = LoggerFactory.getLogger(CcsClient.class);
private final ConcurrentHashMap<String, ResultCallback> callbacks = new ConcurrentHashMap<>();
private static class MyXMPPTCPConnection extends XMPPTCPConnection {
public MyXMPPTCPConnection(XMPPTCPConnectionConfiguration config) {
super(config);
}
@Override
public void shutdown() {
super.shutdown();
}
}
/**
* To be used by those who are migrating from the HTTP protocol
* to the XMPP protocol.
* <p>
* <strong>Note:</strong> This does not cover all possible errors, just the most
* common ones, provided that the payload sent is valid.
*/
public static final HashMap<String, String> HTTP_PROTO_ERROR_MAP = new HashMap<>();
static {
HTTP_PROTO_ERROR_MAP.put("BAD_REGISTRATION", "InvalidRegistration"); // This one covers MismatchSenderId too
HTTP_PROTO_ERROR_MAP.put("DEVICE_UNREGISTERED", "NotRegistered");
HTTP_PROTO_ERROR_MAP.put("SERVICE_UNAVAILABLE", "Unavailable");
HTTP_PROTO_ERROR_MAP.put("INTERNAL_SERVER_ERROR", "InternalServerError");
}
public void shutdown() {
conn.shutdown();
}
public interface ResultCallback {
void run(final boolean success, final String canonicalRegistrationId, final String errorDesc);
}
private static final String HOST = "fcm-xmpp.googleapis.com";
private static final int PORT = 5235; // 5236 = dev gateway (dummy, not sent); 5235 = prod gateway
private final MyXMPPTCPConnection conn;
public CcsClient(String senderId, String serverKey) throws IOException {
Roster.setRosterLoadedAtLoginDefault(false);
final XMPPTCPConnectionConfiguration conf = XMPPTCPConnectionConfiguration.builder()
.setCompressionEnabled(false)
.setSendPresence(false)
.setConnectTimeout(10000)
.setHost(HOST)
.setDebuggerEnabled(false)
.setPort(PORT)
.setXmppDomain(HOST)
.setSocketFactory(SSLSocketFactory.getDefault())
.setUsernameAndPassword(senderId + "@gcm.googleapis.com", serverKey)
.build();
this.conn = new MyXMPPTCPConnection(conf);
try {
conn.connect();
conn.login();
} catch (XMPPException | InterruptedException | SmackException e) {
throw new IOException(e);
}
this.conn.addAsyncStanzaListener(new StanzaListener() {
@Override
public void processStanza(Stanza stanza) throws SmackException.NotConnectedException, InterruptedException {
try {
if (stanza == null || stanza.getExtensions() == null || stanza.getExtensions().size() > 1) {
logger.error("Unknown stanza received! {}", stanza);
return;
}
final ExtensionElement ee = stanza.getExtensions().get(0);
if (!(ee instanceof StandardExtensionElement)) {
logger.error("Unknown stanza extension element received! {}", stanza);
return;
}
final String json = ((StandardExtensionElement) ee).getText();
final BasicDBObject res = (BasicDBObject) JSON.parse(json);
final String messageType = res.getString("message_type");
final String messageId = res.getString("message_id");
final ResultCallback resultCallback = callbacks.remove(messageId);
if (resultCallback != null) {
resultCallback.run("ack".equals(messageType), res.getString("registration_id"), res.getString("error"));
}
} catch (Throwable t) {
logger.error("Failed to call callback", t);
}
}
}, StanzaTypeFilter.MESSAGE);
}
public void send(final BasicDBObject payload, final ResultCallback resultCallback)
throws SmackException.NotConnectedException, InterruptedException {
final String messageId = Thread.currentThread().getName() + System.nanoTime();
payload.put("message_id", messageId); // The NACK/ACK message from Google will send this id back
if (resultCallback != null) {
callbacks.put(messageId, resultCallback);
}
conn.sendStanza(new Stanza() {
@Override
public String toString() {
return toXML().toString();
}
@Override
public CharSequence toXML() {
return "<message><gcm xmlns=\"" + NAMESPACE_GOOGLE + "\">" + StringUtils.escapeForXml(payload.toString()) + "</gcm></message>";
}
});
}
}
@judepereira
Copy link
Author

Dependencies:

  • Smack 4.2
  • MongoDB java driver (only for JSON - some other one can be used easily)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment