Created
March 31, 2017 09:28
-
-
Save judepereira/fd8dc0a5321179b699f5c5e54812770c to your computer and use it in GitHub Desktop.
XMPP/CCS client for FCM/GCM, using Smack 4.2
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>"; | |
} | |
}); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Dependencies: