Refactored application and server-side to use GCM instead of C2DM; removed and or deprecated C2DM code; removed unused code

This commit is contained in:
felipeal
2012-08-01 23:37:36 +00:00
parent 346348263d
commit 79adf622d5
23 changed files with 603 additions and 637 deletions

View File

@@ -4,5 +4,7 @@
<classpathentry kind="src" path="c2dm"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
<classpathentry kind="con" path="com.google.appengine.eclipse.core.GAE_CONTAINER"/>
<classpathentry kind="lib" path="war/WEB-INF/lib/gcm-server.jar"/>
<classpathentry kind="lib" path="war/WEB-INF/lib/json_simple-1.1.jar"/>
<classpathentry kind="output" path="war/WEB-INF/classes"/>
</classpath>

View File

@@ -35,11 +35,16 @@ import javax.jdo.PersistenceManagerFactory;
*/
public class C2DMConfigLoader {
private static final String TOKEN_FILE = "/dataMessagingToken.txt";
private static final String API_KEY_FILE = "/dataMessagingApiKey.txt";
private final PersistenceManagerFactory PMF;
private static final Logger log = Logger.getLogger(C2DMConfigLoader.class.getName());
private static final int C2DM_TOKEN = 1;
private static final int GCM_API_KEY = 2;
String currentToken;
String c2dmUrl;
String currentApiKey;
C2DMConfigLoader(PersistenceManagerFactory pmf) {
this.PMF = pmf;
@@ -56,7 +61,7 @@ public class C2DMConfigLoader {
currentToken = token;
PersistenceManager pm = PMF.getPersistenceManager();
try {
getDataMessagingConfig(pm).setAuthToken(token);
getDataMessagingConfig(pm, TOKEN_FILE, C2DM_TOKEN).setAuthToken(token);
} finally {
pm.close();
}
@@ -76,30 +81,37 @@ public class C2DMConfigLoader {
*/
public String getToken() {
if (currentToken == null) {
currentToken = getDataMessagingConfig().getAuthToken();
currentToken = getDataMessagingConfig(TOKEN_FILE, C2DM_TOKEN).getAuthToken();
}
return currentToken;
}
public String getGcmApiKey() {
if (currentApiKey == null) {
currentApiKey = getDataMessagingConfig(API_KEY_FILE, GCM_API_KEY).getAuthToken();
}
return currentApiKey;
}
public String getC2DMUrl() {
if (c2dmUrl == null) {
c2dmUrl = getDataMessagingConfig().getC2DMUrl();
c2dmUrl = getDataMessagingConfig(TOKEN_FILE, C2DM_TOKEN).getC2DMUrl();
}
return c2dmUrl;
}
public C2DMConfig getDataMessagingConfig() {
private C2DMConfig getDataMessagingConfig(String file, int index) {
PersistenceManager pm = PMF.getPersistenceManager();
try {
C2DMConfig dynamicConfig = getDataMessagingConfig(pm);
C2DMConfig dynamicConfig = getDataMessagingConfig(pm, file, index);
return dynamicConfig;
} finally {
pm.close();
}
}
public static C2DMConfig getDataMessagingConfig(PersistenceManager pm) {
Key key = KeyFactory.createKey(C2DMConfig.class.getSimpleName(), 1);
private C2DMConfig getDataMessagingConfig(PersistenceManager pm, String file, int index) {
Key key = KeyFactory.createKey(C2DMConfig.class.getSimpleName(), index);
C2DMConfig dmConfig = null;
try {
dmConfig = pm.getObjectById(C2DMConfig.class, key);
@@ -109,7 +121,7 @@ public class C2DMConfigLoader {
dmConfig.setKey(key);
// Must be in classpath, before sending. Do not checkin !
try {
InputStream is = C2DMConfigLoader.class.getClassLoader().getResourceAsStream(TOKEN_FILE);
InputStream is = C2DMConfigLoader.class.getClassLoader().getResourceAsStream(file);
String token;
if (is != null) {
BufferedReader reader = new BufferedReader(new InputStreamReader(is));

View File

@@ -1,87 +0,0 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.c2dm.server;
import java.io.IOException;
import java.util.Map;
import java.util.logging.Logger;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* A task that sends tickles to device clients. This will be invoked by
* AppEngine cron to retry failed requests.
*
* You must configure war/WEB-INF/queue.xml and the web.xml entries.
*/
@SuppressWarnings("serial")
public class C2DMRetryServlet extends HttpServlet {
private static final Logger log = Logger.getLogger(C2DMRetryServlet.class.getName());
public static final String URI = "/tasks/c2dm";
public static final String RETRY_COUNT = "X-AppEngine-TaskRetryCount";
static int MAX_RETRY = 3;
/**
* Only admin can make this request.
*/
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
String registrationId = req.getParameter(C2DMessaging.PARAM_REGISTRATION_ID);
String retryCount = req.getHeader(RETRY_COUNT);
if (retryCount != null) {
int retryCnt = Integer.parseInt(retryCount);
if (retryCnt > MAX_RETRY) {
log.severe("Too many retries, drop message for :" + registrationId);
resp.setStatus(200);
return; // will not try again.
}
}
@SuppressWarnings("unchecked")
Map<String, String[]> params = req.getParameterMap();
String collapse = req.getParameter(C2DMessaging.PARAM_COLLAPSE_KEY);
boolean delayWhenIdle =
null != req.getParameter(C2DMessaging.PARAM_DELAY_WHILE_IDLE);
try {
// Send doesn't retry !!
// We use the queue exponential backoff for retries.
boolean sentOk = C2DMessaging.get(getServletContext())
.sendNoRetry(registrationId, collapse, params, delayWhenIdle);
log.info("Retry result " + sentOk + " " + registrationId);
if (sentOk) {
resp.setStatus(200);
resp.getOutputStream().write("OK".getBytes());
} else {
resp.setStatus(500); // retry this task
}
} catch (IOException ex) {
resp.setStatus(200);
resp.getOutputStream().write(("Non-retriable error:" +
ex.toString()).getBytes());
}
}
}

View File

@@ -16,11 +16,14 @@
package com.google.android.c2dm.server;
import com.google.android.gcm.server.Message;
import com.google.android.gcm.server.Result;
import com.google.android.gcm.server.Sender;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
@@ -34,13 +37,6 @@ import javax.jdo.PersistenceManagerFactory;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletResponse;
import com.google.appengine.api.taskqueue.Queue;
import com.google.appengine.api.taskqueue.QueueFactory;
import com.google.appengine.api.taskqueue.TaskHandle;
import com.google.appengine.api.taskqueue.TaskOptions;
/**
*/
@SuppressWarnings("serial")
public class C2DMessaging {
private static final String UPDATE_CLIENT_AUTH = "Update-Client-Auth";
@@ -110,9 +106,14 @@ public class C2DMessaging {
pmfFactory);
}
return pmfFactory;
}
}
/**
* This method is deprecated because C2DM has been replaced by GCM, and it
* provides a library with similar functionality.
*/
@Deprecated
public boolean sendNoRetry(String registrationId,
String collapse,
Map<String, String[]> params,
@@ -219,52 +220,12 @@ public class C2DMessaging {
}
}
/**
* Helper method to send a message, with 2 parameters.
*
* Permanent errors will result in IOException.
* Retriable errors will cause the message to be scheduled for retry.
*/
public void sendWithRetry(String token, String collapseKey,
String name1, String value1, String name2, String value2,
String name3, String value3)
throws IOException {
Map<String, String[]> params = new HashMap<String, String[]>();
if (value1 != null) params.put("data." + name1, new String[] {value1});
if (value2 != null) params.put("data." + name2, new String[] {value2});
if (value3 != null) params.put("data." + name3, new String[] {value3});
boolean sentOk = sendNoRetry(token, collapseKey, params, true);
if (!sentOk) {
retry(token, collapseKey, params, true);
}
}
public boolean sendNoRetry(String token, String collapseKey,
String name1, String value1, String name2, String value2,
String name3, String value3) {
Map<String, String[]> params = new HashMap<String, String[]>();
if (value1 != null) params.put("data." + name1, new String[] {value1});
if (value2 != null) params.put("data." + name2, new String[] {value2});
if (value3 != null) params.put("data." + name3, new String[] {value3});
try {
return sendNoRetry(token, collapseKey, params, true);
} catch (IOException ex) {
return false;
}
}
private static final ThreadLocal<IOException> C2DM_EXCEPTION =
new ThreadLocal<IOException>();
public static IOException getC2dmException() {
return C2DM_EXCEPTION.get();
}
public boolean sendNoRetry(String token, String collapseKey,
/**
* This method is deprecated because C2DM has been replaced by GCM, and it
* provides a library with similar functionality.
*/
@Deprecated
public Object sendNoRetry(String token, String collapseKey,
String... nameValues) {
Map<String, String[]> params = new HashMap<String, String[]>();
@@ -281,42 +242,25 @@ public class C2DMessaging {
}
try {
C2DM_EXCEPTION.remove();
return sendNoRetry(token, collapseKey, params, true);
} catch (IOException ex) {
// save exception in a thread-local object so it can be cheked for
// unregistration later
C2DM_EXCEPTION.set(ex);
return false;
return ex;
}
}
private void retry(String token, String collapseKey,
Map<String, String[]> params, boolean delayWhileIdle) {
Queue dmQueue = QueueFactory.getQueue("c2dm");
try {
TaskOptions url =
TaskOptions.Builder.withUrl(C2DMRetryServlet.URI)
.param(C2DMessaging.PARAM_REGISTRATION_ID, token)
.param(C2DMessaging.PARAM_COLLAPSE_KEY, collapseKey);
if (delayWhileIdle) {
url.param(PARAM_DELAY_WHILE_IDLE, "1");
}
for (String key: params.keySet()) {
String[] values = params.get(key);
url.param(key, URLEncoder.encode(values[0], UTF8));
}
// Task queue implements the exponential backoff
long jitter = (int) Math.random() * DATAMESSAGING_MAX_JITTER_MSEC;
url.countdownMillis(jitter);
TaskHandle add = dmQueue.add(url);
} catch (UnsupportedEncodingException e) {
// Ignore - UTF8 should be supported
log.log(Level.SEVERE, "Unexpected error", e);
}
public Result send(Message message, String regId) {
String key = serverConfig.getGcmApiKey();
Sender sender = new Sender(key);
try {
// TODO: should use AppEngine's queue mechanism to retry, otherwise the
// request might time out
Result result = sender.send(message, regId, 2 /* number of tries */);
log.fine("Result: " + result);
return result;
} catch (IOException e) {
log.log(Level.SEVERE, "Error sending " + message + " to " + regId, e);
return null;
}
}
}

Binary file not shown.

Binary file not shown.

View File

@@ -23,7 +23,9 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.logging.Level;
import javax.jdo.JDOObjectNotFoundException;
import javax.jdo.PersistenceManager;
import javax.jdo.Query;
import javax.jdo.annotations.IdentityType;
@@ -173,4 +175,14 @@ public class DeviceInfo {
return result;
}
public static DeviceInfo getDeviceInfo(PersistenceManager pm, String regId) {
Query query = pm.newQuery(DeviceInfo.class);
query.setFilter("deviceRegistrationID == '" + regId + "'");
@SuppressWarnings("unchecked")
List<DeviceInfo> result = (List<DeviceInfo>) query.execute();
DeviceInfo deviceInfo = (result == null || result.isEmpty()) ? null : result.get(0);
query.closeAll();
return deviceInfo;
}
}

View File

@@ -237,4 +237,17 @@ public class RequestInfo {
}
}
public void updateRegistration(String regId, String canonicalRegId) {
if (ctx == null) {
return;
}
log.fine("Updating regId " + regId + " to canonical " + canonicalRegId);
PersistenceManager pm = C2DMessaging.getPMF(ctx).getPersistenceManager();
DeviceInfo device = DeviceInfo.getDeviceInfo(pm, regId);
device.setDeviceRegistrationID(canonicalRegId);
pm.currentTransaction().begin();
pm.makePersistent(device);
pm.currentTransaction().commit();
}
}

View File

@@ -17,6 +17,11 @@
package com.google.android.chrometophone.server;
import com.google.android.c2dm.server.C2DMessaging;
import com.google.android.gcm.server.Constants;
import com.google.android.gcm.server.Message;
import com.google.android.gcm.server.Message.Builder;
import com.google.android.gcm.server.Result;
import com.google.android.gcm.server.Sender;
import com.google.appengine.api.channel.ChannelMessage;
import com.google.appengine.api.channel.ChannelServiceFactory;
@@ -86,18 +91,18 @@ public class SendServlet extends HttpServlet {
// Send push message to phone
C2DMessaging push = C2DMessaging.get(getServletContext());
boolean res = false;
Object res = null;
String collapseKey = "" + url.hashCode();
boolean reqDebug = "1".equals(reqInfo.getParameter("debug"));
int ac2dmCnt = 0;
int deviceCount = 0;
Iterator<DeviceInfo> iterator = reqInfo.devices.iterator();
while (iterator.hasNext()) {
DeviceInfo deviceInfo = iterator.next();
if ("ac2dm".equals(deviceInfo.getType())) {
ac2dmCnt++;
if (!DeviceInfo.TYPE_CHROME.equals(deviceInfo.getType())) {
deviceCount++;
}
if (deviceNames != null) {
boolean found = false;
@@ -117,27 +122,56 @@ public class SendServlet extends HttpServlet {
if (deviceInfo.getType().equals(DeviceInfo.TYPE_CHROME)) {
res = doSendViaBrowserChannel(url, deviceInfo);
} else {
res = doSendViaC2dm(url, title, sel, push, collapseKey,
deviceInfo, reqDebug);
res = doSendViaGoogleCloud(url, title, sel, push, collapseKey,
deviceInfo, reqDebug, deviceInfo.getType());
}
if (res) {
if (res == null) {
log.info("Link sent to phone! collapse_key:" + collapseKey);
ok = true;
} else {
log.warning("Error: Unable to send link to device: " +
deviceInfo.getDeviceRegistrationID());
IOException ex = C2DMessaging.getC2dmException();
if (ex != null) {
if ("InvalidRegistration".equals(ex.getMessage())) {
// Prune device, it no longer works
reqInfo.deleteRegistration(deviceInfo.getDeviceRegistrationID(),
deviceInfo.getType());
iterator.remove();
ac2dmCnt--;
} else {
throw ex;
// C2DM error
if (res instanceof IOException) {
IOException ex = (IOException) res;
log.warning("Error: Unable to send link to device: " +
deviceInfo.getDeviceRegistrationID());
String error = "" + ex.getMessage();
if (error.equals(Constants.ERROR_NOT_REGISTERED) || error.equals(Constants.ERROR_INVALID_REGISTRATION)) {
// Prune device, it no longer works
reqInfo.deleteRegistration(deviceInfo.getDeviceRegistrationID(),
deviceInfo.getType());
iterator.remove();
deviceCount--;
} else {
throw ex;
}
}
// GCM result.
if (res instanceof Result) {
Result result = (Result) res;
String regId = deviceInfo.getDeviceRegistrationID();
if (result.getMessageId() != null) {
log.info("Link sent to phone! collapse_key:" + collapseKey);
ok = true;
String canonicalRegId = result.getCanonicalRegistrationId();
if (canonicalRegId != null) {
// same device has more than on registration id: update it
log.finest("canonicalRegId " + canonicalRegId);
reqInfo.updateRegistration(regId, canonicalRegId);
}
} else {
String error = result.getErrorCodeName();
if (error.equals(Constants.ERROR_NOT_REGISTERED) || error.equals(Constants.ERROR_INVALID_REGISTRATION)) {
// Prune device, it no longer works
reqInfo.deleteRegistration(regId, deviceInfo.getType());
iterator.remove();
deviceCount--;
} else {
log.severe("Error sending message to device " + regId
+ ": " + error);
throw new IOException(error);
}
}
}
}
}
@@ -149,7 +183,7 @@ public class SendServlet extends HttpServlet {
// Show the 'no devices' if only the browser is registered.
// We should also clarify that 'error status' mean no matching
// device found ( when the extension allow specifying the destination )
if (ac2dmCnt == 0 && "ac2dm".equals(deviceType)) {
if (deviceCount == 0 && !DeviceInfo.TYPE_CHROME.equals(deviceType)) {
log.warning("No device registered for " + reqInfo.userName);
return DEVICE_NOT_REGISTERED_STATUS;
} else {
@@ -158,8 +192,8 @@ public class SendServlet extends HttpServlet {
}
}
private boolean doSendViaC2dm(String url, String title, String sel, C2DMessaging push,
String collapseKey, DeviceInfo deviceInfo, boolean reqDebug) {
private Object doSendViaGoogleCloud(String url, String title, String sel, C2DMessaging push,
String collapseKey, DeviceInfo deviceInfo, boolean reqDebug, String deviceType) {
// Trim title, sel if needed.
if (url.length() + title.length() + sel.length() > 1000) {
@@ -177,13 +211,28 @@ public class SendServlet extends HttpServlet {
}
}
boolean res;
res = push.sendNoRetry(deviceInfo.getDeviceRegistrationID(),
collapseKey,
"url", url,
"title", title,
"sel", sel,
"debug", deviceInfo.getDebug() || reqDebug ? "1" : null);
String regId = deviceInfo.getDeviceRegistrationID();
String debug = (deviceInfo.getDebug()) || reqDebug ? "1" : null;
Object res;
if (deviceType.equals(DeviceInfo.TYPE_AC2DM)) {
res = push.sendNoRetry(regId,
collapseKey,
"url", url,
"title", title,
"sel", sel,
"debug", debug);
} else {
Builder builder = new Message.Builder()
.collapseKey(collapseKey)
.addData("url", url)
.addData("title", title)
.addData("sel", sel);
if (debug != null) {
builder.addData("debug", debug);
}
Message message = builder.build();
res = push.send(message, regId);
}
return res;
}

View File

@@ -43,7 +43,6 @@ public class UnregisterServlet extends HttpServlet {
return;
}
// TODO: make sure new app passes deviceType
String deviceType = reqInfo.getParameter("deviceType");
if (deviceType == null) {
deviceType = "ac2dm";

View File

@@ -55,13 +55,6 @@
</servlet-class>
</servlet>
<servlet>
<servlet-name>dataMessagingServlet</servlet-name>
<servlet-class>
com.google.android.c2dm.server.C2DMRetryServlet
</servlet-class>
</servlet>
<servlet>
<servlet-name>SenderServlet</servlet-name>
<servlet-class>com.google.android.chrometophone.server.SenderServlet
@@ -130,20 +123,6 @@
<url-pattern>/admin/convertStats</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>dataMessagingServlet</servlet-name>
<url-pattern>/tasks/c2dm</url-pattern>
</servlet-mapping>
<security-constraint>
<web-resource-collection>
<web-resource-name>tasks</web-resource-name>
<url-pattern>/tasks/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>admin</role-name>
</auth-constraint>
</security-constraint>
<security-constraint>
<web-resource-collection>
<web-resource-name>admin</web-resource-name>