<style>
/* Changing the layout to use less space for mobiles */
@media screen and (max-device-width: 480px), screen and (-webkit-min-device-pixel-ratio: 2) {
#email-body { min-width: 30em !important; }
#email-page { padding: 8px !important; }
#email-banner { padding: 8px 8px 0 8px !important; }
#email-avatar { margin: 1px 8px 8px 0 !important; padding: 0 !important; }
#email-fields { padding: 0 8px 8px 8px !important; }
#email-gutter { width: 0 !important; }
}
</style>
<div id="email-body">
<table id="email-wrap" align="center" border="0" cellpadding="0" cellspacing="0" style="background-color:#f0f0f0;color:#000000;width:100%;">
<tr valign="top">
<td id="email-page" style="padding:16px !important;">
<table align="center" border="0" cellpadding="0" cellspacing="0" style="background-color:#ffffff;border:1px solid #bbbbbb;color:#000000;width:100%;">
<tr valign="top">
<td bgcolor="#3b4d64" style="background-color:#3b4d64;color:#ffffff;font-family:Arial,FreeSans,Helvetica,sans-serif;font-size:12px;line-height:1;"><img src="https://issues.jboss.org/s/en_USbeh5ai-1988229788/6109/58/_/jira-logo-scaled.png" alt="" style="vertical-align:top;" /></td>
</tr><tr valign="top">
<td id="email-banner" style="padding:32px 32px 0 32px;">
<table align="left" border="0" cellpadding="0" cellspacing="0" width="100%" style="width:100%;">
<tr valign="top">
<td style="color:#505050;font-family:Arial,FreeSans,Helvetica,sans-serif;padding:0;">
<img id="email-avatar" src="https://community.jboss.org/people/mwessendorf/avatar/16.png" alt="" height="48" width="48" border="0" align="left" style="padding:0;margin: 0 16px 16px 0;" />
<div id="email-action" style="padding: 0 0 8px 0;font-size:12px;line-height:18px;">
<a class="user-hover" rel="mwessendorf" id="email_mwessendorf" href="https://issues.jboss.org/secure/ViewProfile.jspa?name=mwessendorf" style="color:#3b73af;">Matthias Wessendorf</a>
commented on <img src="https://issues.jboss.org/images/icons/issuetypes/newfeature.png" height="16" width="16" border="0" align="absmiddle" alt="Feature Request"> <a style='color:#3b73af;text-decoration:none;' href='https://issues.jboss.org/browse/AGPUSH-36'>AGPUSH-36</a>
</div>
<div id="email-summary" style="font-size:16px;line-height:20px;padding:2px 0 16px 0;">
<a style='color:#3b73af;text-decoration:none;' href='https://issues.jboss.org/browse/AGPUSH-36'><strong>Push: GCM: Persistent Connections</strong></a>
</div>
</td>
</tr>
</table>
</td>
</tr>
<tr valign="top">
<td id="email-fields" style="padding:0 32px 32px 32px;">
<table border="0" cellpadding="0" cellspacing="0" style="padding:0;text-align:left;width:100%;" width="100%">
<tr valign="top">
<td id="email-gutter" style="width:64px;white-space:nowrap;"></td>
<td>
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<tr valign="top">
<td colspan="2" style="color:#000000;font-family:Arial,FreeSans,Helvetica,sans-serif;font-size:12px;padding:0 0 16px 0;width:100%;">
<div class="comment-block" style="background-color:#edf5ff;border:1px solid #dddddd;color:#000000;padding:12px;"><p>The persistent connection is called <em>GCM Cloud Connection Server (CCS)</em> . CCS itself is XMPP based</p>
<p>Google added an example on how to use CCS:<br/>
<a href="https://developer.android.com/google/gcm/ccs.html#implement" class="external-link" rel="nofollow">https://developer.android.com/google/gcm/ccs.html#implement</a></p>
<p>The example uses Jive's Smack (an Open Source XMPP (Jabber) library):</p>
<div class="code panel" style="border-width: 1px;"><div class="codeContent panelContent">
<pre class="code-java"><span class="code-keyword">import</span> org.jivesoftware.smack.ConnectionConfiguration;
<span class="code-keyword">import</span> org.jivesoftware.smack.ConnectionConfiguration.SecurityMode;
<span class="code-keyword">import</span> org.jivesoftware.smack.ConnectionListener;
<span class="code-keyword">import</span> org.jivesoftware.smack.PacketInterceptor;
<span class="code-keyword">import</span> org.jivesoftware.smack.PacketListener;
<span class="code-keyword">import</span> org.jivesoftware.smack.XMPPConnection;
<span class="code-keyword">import</span> org.jivesoftware.smack.XMPPException;
<span class="code-keyword">import</span> org.jivesoftware.smack.filter.PacketTypeFilter;
<span class="code-keyword">import</span> org.jivesoftware.smack.packet.DefaultPacketExtension;
<span class="code-keyword">import</span> org.jivesoftware.smack.packet.Message;
<span class="code-keyword">import</span> org.jivesoftware.smack.packet.Packet;
<span class="code-keyword">import</span> org.jivesoftware.smack.packet.PacketExtension;
<span class="code-keyword">import</span> org.jivesoftware.smack.provider.PacketExtensionProvider;
<span class="code-keyword">import</span> org.jivesoftware.smack.provider.ProviderManager;
<span class="code-keyword">import</span> org.jivesoftware.smack.util.StringUtils;
<span class="code-keyword">import</span> org.json.simple.JSONValue;
<span class="code-keyword">import</span> org.json.simple.parser.ParseException;
<span class="code-keyword">import</span> org.xmlpull.v1.XmlPullParser;
<span class="code-keyword">import</span> java.util.HashMap;
<span class="code-keyword">import</span> java.util.Map;
<span class="code-keyword">import</span> java.util.Random;
<span class="code-keyword">import</span> java.util.logging.Level;
<span class="code-keyword">import</span> java.util.logging.Logger;
<span class="code-keyword">import</span> javax.net.ssl.SSLSocketFactory;
/**
* Sample Smack implementation of a client <span class="code-keyword">for</span> GCM Cloud Connection Server.
*
* <p>For illustration purposes only.
*/
<span class="code-keyword">public</span> class SmackCcsClient {
Logger logger = Logger.getLogger(<span class="code-quote">"SmackCcsClient"</span>);
<span class="code-keyword">public</span> <span class="code-keyword">static</span> <span class="code-keyword">final</span> <span class="code-object">String</span> GCM_SERVER = <span class="code-quote">"gcm.googleapis.com"</span>;
<span class="code-keyword">public</span> <span class="code-keyword">static</span> <span class="code-keyword">final</span> <span class="code-object">int</span> GCM_PORT = 5235;
<span class="code-keyword">public</span> <span class="code-keyword">static</span> <span class="code-keyword">final</span> <span class="code-object">String</span> GCM_ELEMENT_NAME = <span class="code-quote">"gcm"</span>;
<span class="code-keyword">public</span> <span class="code-keyword">static</span> <span class="code-keyword">final</span> <span class="code-object">String</span> GCM_NAMESPACE = <span class="code-quote">"google:mobile:data"</span>;
<span class="code-keyword">static</span> Random random = <span class="code-keyword">new</span> Random();
XMPPConnection connection;
ConnectionConfiguration config;
/**
* XMPP Packet Extension <span class="code-keyword">for</span> GCM Cloud Connection Server.
*/
class GcmPacketExtension <span class="code-keyword">extends</span> DefaultPacketExtension {
<span class="code-object">String</span> json;
<span class="code-keyword">public</span> GcmPacketExtension(<span class="code-object">String</span> json) {
<span class="code-keyword">super</span>(GCM_ELEMENT_NAME, GCM_NAMESPACE);
<span class="code-keyword">this</span>.json = json;
}
<span class="code-keyword">public</span> <span class="code-object">String</span> getJson() {
<span class="code-keyword">return</span> json;
}
@Override
<span class="code-keyword">public</span> <span class="code-object">String</span> toXML() {
<span class="code-keyword">return</span> <span class="code-object">String</span>.format(<span class="code-quote">"<%s xmlns=\"</span>%s\<span class="code-quote">">%s</%s>"</span>, GCM_ELEMENT_NAME,
GCM_NAMESPACE, json, GCM_ELEMENT_NAME);
}
@SuppressWarnings(<span class="code-quote">"unused"</span>)
<span class="code-keyword">public</span> Packet toPacket() {
<span class="code-keyword">return</span> <span class="code-keyword">new</span> Message() {
<span class="code-comment">// Must override toXML() because it includes a <body>
</span> @Override
<span class="code-keyword">public</span> <span class="code-object">String</span> toXML() {
StringBuilder buf = <span class="code-keyword">new</span> StringBuilder();
buf.append(<span class="code-quote">"<message"</span>);
<span class="code-keyword">if</span> (getXmlns() != <span class="code-keyword">null</span>) {
buf.append(<span class="code-quote">" xmlns=\"</span><span class="code-quote">").append(getXmlns()).append("</span>\"");
}
<span class="code-keyword">if</span> (getLanguage() != <span class="code-keyword">null</span>) {
buf.append(<span class="code-quote">" xml:lang=\"</span><span class="code-quote">").append(getLanguage()).append("</span>\"");
}
<span class="code-keyword">if</span> (getPacketID() != <span class="code-keyword">null</span>) {
buf.append(<span class="code-quote">" id=\"</span><span class="code-quote">").append(getPacketID()).append("</span>\"");
}
<span class="code-keyword">if</span> (getTo() != <span class="code-keyword">null</span>) {
buf.append(<span class="code-quote">" to=\"</span><span class="code-quote">").append(StringUtils.escapeForXML(getTo())).append("</span>\"");
}
<span class="code-keyword">if</span> (getFrom() != <span class="code-keyword">null</span>) {
buf.append(<span class="code-quote">" from=\"</span><span class="code-quote">").append(StringUtils.escapeForXML(getFrom())).append("</span>\"");
}
buf.append(<span class="code-quote">">"</span>);
buf.append(GcmPacketExtension.<span class="code-keyword">this</span>.toXML());
buf.append(<span class="code-quote">"</message>"</span>);
<span class="code-keyword">return</span> buf.toString();
}
};
}
}
<span class="code-keyword">public</span> SmackCcsClient() {
<span class="code-comment">// Add GcmPacketExtension
</span> ProviderManager.getInstance().addExtensionProvider(GCM_ELEMENT_NAME,
GCM_NAMESPACE, <span class="code-keyword">new</span> PacketExtensionProvider() {
@Override
<span class="code-keyword">public</span> PacketExtension parseExtension(XmlPullParser parser)
<span class="code-keyword">throws</span> Exception {
<span class="code-object">String</span> json = parser.nextText();
GcmPacketExtension packet = <span class="code-keyword">new</span> GcmPacketExtension(json);
<span class="code-keyword">return</span> packet;
}
});
}
/**
* Returns a random message id to uniquely identify a message.
*
* <p>Note:
* This is generated by a pseudo random number generator <span class="code-keyword">for</span> illustration purpose,
* and is not guaranteed to be unique.
*
*/
<span class="code-keyword">public</span> <span class="code-object">String</span> getRandomMessageId() {
<span class="code-keyword">return</span> <span class="code-quote">"m-"</span> + <span class="code-object">Long</span>.toString(random.nextLong());
}
/**
* Sends a downstream GCM message.
*/
<span class="code-keyword">public</span> void send(<span class="code-object">String</span> jsonRequest) {
Packet request = <span class="code-keyword">new</span> 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 <span class="code-keyword">this</span> method to process an upstream message.
*/
<span class="code-keyword">public</span> void handleIncomingDataMessage(Map<<span class="code-object">String</span>, <span class="code-object">Object</span>> jsonObject) {
<span class="code-object">String</span> from = jsonObject.get(<span class="code-quote">"from"</span>).toString();
<span class="code-comment">// PackageName of the application that sent <span class="code-keyword">this</span> message.
</span> <span class="code-object">String</span> category = jsonObject.get(<span class="code-quote">"category"</span>).toString();
<span class="code-comment">// Use the packageName as the collapseKey in the echo packet
</span> <span class="code-object">String</span> collapseKey = <span class="code-quote">"echo:CollapseKey"</span>;
@SuppressWarnings(<span class="code-quote">"unchecked"</span>)
Map<<span class="code-object">String</span>, <span class="code-object">String</span>> payload = (Map<<span class="code-object">String</span>, <span class="code-object">String</span>>) jsonObject.get(<span class="code-quote">"data"</span>);
payload.put(<span class="code-quote">"ECHO"</span>, <span class="code-quote">"Application: "</span> + category);
<span class="code-comment">// Send an ECHO response back
</span> <span class="code-object">String</span> echo = createJsonMessage(from, getRandomMessageId(), payload, collapseKey, <span class="code-keyword">null</span>, <span class="code-keyword">false</span>);
send(echo);
}
/**
* Handles an ACK.
*
* <p>By <span class="code-keyword">default</span>, it only logs a INFO message, but subclasses could override it to
* properly handle ACKS.
*/
<span class="code-keyword">public</span> void handleAckReceipt(Map<<span class="code-object">String</span>, <span class="code-object">Object</span>> jsonObject) {
<span class="code-object">String</span> messageId = jsonObject.get(<span class="code-quote">"message_id"</span>).toString();
<span class="code-object">String</span> from = jsonObject.get(<span class="code-quote">"from"</span>).toString();
logger.log(Level.INFO, <span class="code-quote">"handleAckReceipt() from: "</span> + from + <span class="code-quote">", messageId: "</span> + messageId);
}
/**
* Handles a NACK.
*
* <p>By <span class="code-keyword">default</span>, it only logs a INFO message, but subclasses could override it to
* properly handle NACKS.
*/
<span class="code-keyword">public</span> void handleNackReceipt(Map<<span class="code-object">String</span>, <span class="code-object">Object</span>> jsonObject) {
<span class="code-object">String</span> messageId = jsonObject.get(<span class="code-quote">"message_id"</span>).toString();
<span class="code-object">String</span> from = jsonObject.get(<span class="code-quote">"from"</span>).toString();
logger.log(Level.INFO, <span class="code-quote">"handleNackReceipt() from: "</span> + from + <span class="code-quote">", messageId: "</span> + messageId);
}
/**
* Creates a JSON encoded GCM message.
*
* @param to RegistrationId of the target device (Required).
* @param messageId Unique messageId <span class="code-keyword">for</span> which CCS will send an <span class="code-quote">"ack/nack"</span> (Required).
* @param payload Message content intended <span class="code-keyword">for</span> 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).
* @<span class="code-keyword">return</span> JSON encoded GCM message.
*/
<span class="code-keyword">public</span> <span class="code-keyword">static</span> <span class="code-object">String</span> createJsonMessage(<span class="code-object">String</span> to, <span class="code-object">String</span> messageId, Map<<span class="code-object">String</span>, <span class="code-object">String</span>> payload,
<span class="code-object">String</span> collapseKey, <span class="code-object">Long</span> timeToLive, <span class="code-object">Boolean</span> delayWhileIdle) {
Map<<span class="code-object">String</span>, <span class="code-object">Object</span>> message = <span class="code-keyword">new</span> HashMap<<span class="code-object">String</span>, <span class="code-object">Object</span>>();
message.put(<span class="code-quote">"to"</span>, to);
<span class="code-keyword">if</span> (collapseKey != <span class="code-keyword">null</span>) {
message.put(<span class="code-quote">"collapse_key"</span>, collapseKey);
}
<span class="code-keyword">if</span> (timeToLive != <span class="code-keyword">null</span>) {
message.put(<span class="code-quote">"time_to_live"</span>, timeToLive);
}
<span class="code-keyword">if</span> (delayWhileIdle != <span class="code-keyword">null</span> && delayWhileIdle) {
message.put(<span class="code-quote">"delay_while_idle"</span>, <span class="code-keyword">true</span>);
}
message.put(<span class="code-quote">"message_id"</span>, messageId);
message.put(<span class="code-quote">"data"</span>, payload);
<span class="code-keyword">return</span> JSONValue.toJSONString(message);
}
/**
* Creates a JSON encoded ACK message <span class="code-keyword">for</span> 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.
* @<span class="code-keyword">return</span> JSON encoded ack.
*/
<span class="code-keyword">public</span> <span class="code-keyword">static</span> <span class="code-object">String</span> createJsonAck(<span class="code-object">String</span> to, <span class="code-object">String</span> messageId) {
Map<<span class="code-object">String</span>, <span class="code-object">Object</span>> message = <span class="code-keyword">new</span> HashMap<<span class="code-object">String</span>, <span class="code-object">Object</span>>();
message.put(<span class="code-quote">"message_type"</span>, <span class="code-quote">"ack"</span>);
message.put(<span class="code-quote">"to"</span>, to);
message.put(<span class="code-quote">"message_id"</span>, messageId);
<span class="code-keyword">return</span> JSONValue.toJSONString(message);
}
/**
* Connects to GCM Cloud Connection Server using the supplied credentials.
*
* @param username GCM_SENDER_ID@gcm.googleapis.com
* @param password API Key
* @<span class="code-keyword">throws</span> XMPPException
*/
<span class="code-keyword">public</span> void connect(<span class="code-object">String</span> username, <span class="code-object">String</span> password) <span class="code-keyword">throws</span> XMPPException {
config = <span class="code-keyword">new</span> ConnectionConfiguration(GCM_SERVER, GCM_PORT);
config.setSecurityMode(SecurityMode.enabled);
config.setReconnectionAllowed(<span class="code-keyword">true</span>);
config.setRosterLoadedAtLogin(<span class="code-keyword">false</span>);
config.setSendPresence(<span class="code-keyword">false</span>);
config.setSocketFactory(SSLSocketFactory.getDefault());
<span class="code-comment">// NOTE: Set to <span class="code-keyword">true</span> to launch a window with information about packets sent and received
</span> config.setDebuggerEnabled(<span class="code-keyword">true</span>);
<span class="code-comment">// -Dsmack.debugEnabled=<span class="code-keyword">true</span>
</span> XMPPConnection.DEBUG_ENABLED = <span class="code-keyword">true</span>;
connection = <span class="code-keyword">new</span> XMPPConnection(config);
connection.connect();
connection.addConnectionListener(<span class="code-keyword">new</span> ConnectionListener() {
@Override
<span class="code-keyword">public</span> void reconnectionSuccessful() {
logger.info(<span class="code-quote">"Reconnecting.."</span>);
}
@Override
<span class="code-keyword">public</span> void reconnectionFailed(Exception e) {
logger.log(Level.INFO, <span class="code-quote">"Reconnection failed.. "</span>, e);
}
@Override
<span class="code-keyword">public</span> void reconnectingIn(<span class="code-object">int</span> seconds) {
logger.log(Level.INFO, <span class="code-quote">"Reconnecting in %d secs"</span>, seconds);
}
@Override
<span class="code-keyword">public</span> void connectionClosedOnError(Exception e) {
logger.log(Level.INFO, <span class="code-quote">"Connection closed on error."</span>);
}
@Override
<span class="code-keyword">public</span> void connectionClosed() {
logger.info(<span class="code-quote">"Connection closed."</span>);
}
});
<span class="code-comment">// Handle incoming packets
</span> connection.addPacketListener(<span class="code-keyword">new</span> PacketListener() {
@Override
<span class="code-keyword">public</span> void processPacket(Packet packet) {
logger.log(Level.INFO, <span class="code-quote">"Received: "</span> + packet.toXML());
Message incomingMessage = (Message) packet;
GcmPacketExtension gcmPacket =
(GcmPacketExtension) incomingMessage.getExtension(GCM_NAMESPACE);
<span class="code-object">String</span> json = gcmPacket.getJson();
<span class="code-keyword">try</span> {
@SuppressWarnings(<span class="code-quote">"unchecked"</span>)
Map<<span class="code-object">String</span>, <span class="code-object">Object</span>> jsonObject =
(Map<<span class="code-object">String</span>, <span class="code-object">Object</span>>) JSONValue.parseWithException(json);
<span class="code-comment">// present <span class="code-keyword">for</span> <span class="code-quote">"ack"</span>/<span class="code-quote">"nack"</span>, <span class="code-keyword">null</span> otherwise
</span> <span class="code-object">Object</span> messageType = jsonObject.get(<span class="code-quote">"message_type"</span>);
<span class="code-keyword">if</span> (messageType == <span class="code-keyword">null</span>) {
<span class="code-comment">// Normal upstream data message
</span> handleIncomingDataMessage(jsonObject);
<span class="code-comment">// Send ACK to CCS
</span> <span class="code-object">String</span> messageId = jsonObject.get(<span class="code-quote">"message_id"</span>).toString();
<span class="code-object">String</span> from = jsonObject.get(<span class="code-quote">"from"</span>).toString();
<span class="code-object">String</span> ack = createJsonAck(from, messageId);
send(ack);
} <span class="code-keyword">else</span> <span class="code-keyword">if</span> (<span class="code-quote">"ack"</span>.equals(messageType.toString())) {
<span class="code-comment">// <span class="code-object">Process</span> Ack
</span> handleAckReceipt(jsonObject);
} <span class="code-keyword">else</span> <span class="code-keyword">if</span> (<span class="code-quote">"nack"</span>.equals(messageType.toString())) {
<span class="code-comment">// <span class="code-object">Process</span> Nack
</span> handleNackReceipt(jsonObject);
} <span class="code-keyword">else</span> {
logger.log(Level.WARNING, <span class="code-quote">"Unrecognized message type (%s)"</span>,
messageType.toString());
}
} <span class="code-keyword">catch</span> (ParseException e) {
logger.log(Level.SEVERE, <span class="code-quote">"Error parsing JSON "</span> + json, e);
} <span class="code-keyword">catch</span> (Exception e) {
logger.log(Level.SEVERE, <span class="code-quote">"Couldn't send echo."</span>, e);
}
}
}, <span class="code-keyword">new</span> PacketTypeFilter(Message.class));
<span class="code-comment">// Log all outgoing packets
</span> connection.addPacketInterceptor(<span class="code-keyword">new</span> PacketInterceptor() {
@Override
<span class="code-keyword">public</span> void interceptPacket(Packet packet) {
logger.log(Level.INFO, <span class="code-quote">"Sent: {0}"</span>, packet.toXML());
}
}, <span class="code-keyword">new</span> PacketTypeFilter(Message.class));
connection.login(username, password);
}
<span class="code-keyword">public</span> <span class="code-keyword">static</span> void main(<span class="code-object">String</span> [] args) {
<span class="code-keyword">final</span> <span class="code-object">String</span> userName = <span class="code-quote">"Your GCM Sender Id"</span> + <span class="code-quote">"@gcm.googleapis.com"</span>;
<span class="code-keyword">final</span> <span class="code-object">String</span> password = <span class="code-quote">"API Key"</span>;
SmackCcsClient ccsClient = <span class="code-keyword">new</span> SmackCcsClient();
<span class="code-keyword">try</span> {
ccsClient.connect(userName, password);
} <span class="code-keyword">catch</span> (XMPPException e) {
e.printStackTrace();
}
<span class="code-comment">// Send a sample hello downstream message to a device.
</span> <span class="code-object">String</span> toRegId = <span class="code-quote">"RegistrationIdOfTheTargetDevice"</span>;
<span class="code-object">String</span> messageId = ccsClient.getRandomMessageId();
Map<<span class="code-object">String</span>, <span class="code-object">String</span>> payload = <span class="code-keyword">new</span> HashMap<<span class="code-object">String</span>, <span class="code-object">String</span>>();
payload.put(<span class="code-quote">"Hello"</span>, <span class="code-quote">"World"</span>);
payload.put(<span class="code-quote">"CCS"</span>, <span class="code-quote">"Dummy Message"</span>);
payload.put(<span class="code-quote">"EmbeddedMessageId"</span>, messageId);
<span class="code-object">String</span> collapseKey = <span class="code-quote">"sample"</span>;
<span class="code-object">Long</span> timeToLive = 10000L;
<span class="code-object">Boolean</span> delayWhileIdle = <span class="code-keyword">true</span>;
ccsClient.send(createJsonMessage(toRegId, messageId, payload, collapseKey,
timeToLive, delayWhileIdle));
}
}
</pre>
</div></div></div>
<div style="color:#505050;padding:4px 0 0 0;"> </div>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td><!-- End #email-page -->
</tr>
<tr valign="top">
<td style="color:#505050;font-family:Arial,FreeSans,Helvetica,sans-serif;font-size:10px;line-height:14px;padding: 0 16px 16px 16px;text-align:center;">
This message is automatically generated by JIRA.<br />
If you think it was sent incorrectly, please contact your JIRA administrators<br />
For more information on JIRA, see: <a style='color:#3b73af;' href='http://www.atlassian.com/software/jira'>http://www.atlassian.com/software/jira</a>
</td>
</tr>
</table><!-- End #email-wrap -->
</div><!-- End #email-body -->