Few minor changes in preparation to the GCM migration:

- fixed warnings (like un-thrown exceptions)
- fixed code that unregister in case of NotRegistered error
- updated server code to use AppEngine 1.7
- changed code so it can be used in a local app engine server
- added a new device type called gcm
- created a servlet that displays how many devices are registered per type (ac2dm, chrome, gcm)
- created a servlet used to send messages without the need for the chrome plugin
This commit is contained in:
felipeal
2012-07-27 23:13:40 +00:00
parent e7950f7b5e
commit 9ccc0225c4
19 changed files with 462 additions and 107 deletions

View File

@@ -4,5 +4,6 @@
<classpathentry kind="src" path="gen"/> <classpathentry kind="src" path="gen"/>
<classpathentry kind="src" path="c2dm"/> <classpathentry kind="src" path="c2dm"/>
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/> <classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
<classpathentry kind="output" path="bin/classes"/> <classpathentry kind="output" path="bin/classes"/>
</classpath> </classpath>

View File

@@ -1,13 +0,0 @@
# This file is automatically generated by Android Tools.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must be checked in Version Control Systems.
#
# To customize properties used by the Ant build system use,
# "build.properties", and override values to adapt the script to your
# project structure.
# Indicates whether an apk should be generated for each density.
split.density=false
# Project target.
target=android-11

View File

@@ -29,6 +29,7 @@ import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.params.HttpClientParams; import org.apache.http.client.params.HttpClientParams;
import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.params.BasicHttpParams; import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpParams; import org.apache.http.params.HttpParams;
@@ -46,6 +47,14 @@ import android.util.Log;
*/ */
public class AppEngineClient { public class AppEngineClient {
static final String BASE_URL = "https://chrometophone.appspot.com"; static final String BASE_URL = "https://chrometophone.appspot.com";
/*
* When running AppEngine locally, set BASE_LOCAL_URL with your server's address.
* (make sure to start AppEngine passing the -a server_address flag, otherwise it will run on
* localhost and the device won't be able to connect.
*/
// static final String BASE_LOCAL_URL = null;
// TODO: tmp
static final String BASE_LOCAL_URL = "http://snpp.mtv.corp.google.com:8888";
private static final String AUTH_URL = BASE_URL + "/_ah/login"; private static final String AUTH_URL = BASE_URL + "/_ah/login";
private static final String AUTH_TOKEN_TYPE = "ah"; private static final String AUTH_TOKEN_TYPE = "ah";
@@ -79,10 +88,15 @@ public class AppEngineClient {
authToken = getAuthToken(mContext, account); authToken = getAuthToken(mContext, account);
} }
// Get ACSID cookie
DefaultHttpClient client = new DefaultHttpClient(); DefaultHttpClient client = new DefaultHttpClient();
String baseUrl;
URI uri;
String ascidCookie = null;
HttpResponse res;
if (BASE_LOCAL_URL == null) {
// Get ACSID cookie so it can be used to authenticate on AppEngine
String continueURL = BASE_URL; String continueURL = BASE_URL;
URI uri = new URI(AUTH_URL + "?continue=" + uri = new URI(AUTH_URL + "?continue=" +
URLEncoder.encode(continueURL, "UTF-8") + URLEncoder.encode(continueURL, "UTF-8") +
"&auth=" + authToken); "&auth=" + authToken);
HttpGet method = new HttpGet(uri); HttpGet method = new HttpGet(uri);
@@ -90,14 +104,13 @@ public class AppEngineClient {
HttpClientParams.setRedirecting(getParams, false); // continue is not used HttpClientParams.setRedirecting(getParams, false); // continue is not used
method.setParams(getParams); method.setParams(getParams);
HttpResponse res = client.execute(method); res = client.execute(method);
Header[] headers = res.getHeaders("Set-Cookie"); Header[] headers = res.getHeaders("Set-Cookie");
if (res.getStatusLine().getStatusCode() != 302 || if (res.getStatusLine().getStatusCode() != 302 ||
headers.length == 0) { headers.length == 0) {
return res; return res;
} }
String ascidCookie = null;
for (Header header: headers) { for (Header header: headers) {
if (header.getValue().indexOf("ACSID=") >=0) { if (header.getValue().indexOf("ACSID=") >=0) {
// let's parse it // let's parse it
@@ -106,14 +119,22 @@ public class AppEngineClient {
ascidCookie = pairs[0]; ascidCookie = pairs[0];
} }
} }
baseUrl = BASE_URL;
} else {
// local app server, pass user directly
baseUrl = BASE_LOCAL_URL;
params.add(new BasicNameValuePair("account", account.name));
}
// Make POST request // Make POST request
uri = new URI(BASE_URL + urlPath); uri = new URI(baseUrl + urlPath);
HttpPost post = new HttpPost(uri); HttpPost post = new HttpPost(uri);
UrlEncodedFormEntity entity = UrlEncodedFormEntity entity =
new UrlEncodedFormEntity(params, "UTF-8"); new UrlEncodedFormEntity(params, "UTF-8");
post.setEntity(entity); post.setEntity(entity);
if (ascidCookie != null) {
post.setHeader("Cookie", ascidCookie); post.setHeader("Cookie", ascidCookie);
}
post.setHeader("X-Same-Domain", "1"); // XSRF post.setHeader("X-Same-Domain", "1"); // XSRF
res = client.execute(post); res = client.execute(post);
return res; return res;

View File

@@ -48,6 +48,7 @@ public class DeviceRegistrar {
public static void registerWithServer(final Context context, public static void registerWithServer(final Context context,
final String deviceRegistrationID) { final String deviceRegistrationID) {
new Thread(new Runnable() { new Thread(new Runnable() {
@Override
public void run() { public void run() {
Intent updateUIIntent = new Intent("com.google.ctp.UPDATE_UI"); Intent updateUIIntent = new Intent("com.google.ctp.UPDATE_UI");
try { try {
@@ -83,6 +84,7 @@ public class DeviceRegistrar {
public static void unregisterWithServer(final Context context, public static void unregisterWithServer(final Context context,
final String deviceRegistrationID) { final String deviceRegistrationID) {
new Thread(new Runnable() { new Thread(new Runnable() {
@Override
public void run() { public void run() {
Intent updateUIIntent = new Intent("com.google.ctp.UPDATE_UI"); Intent updateUIIntent = new Intent("com.google.ctp.UPDATE_UI");
try { try {

View File

@@ -3,6 +3,6 @@
<classpathentry kind="src" path="src"/> <classpathentry kind="src" path="src"/>
<classpathentry kind="src" path="c2dm"/> <classpathentry kind="src" path="c2dm"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/> <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
<classpathentry kind="con" path="com.google.appengine.eclipse.core.GAE_CONTAINER/appengine-java-sdk-1.5.2"/> <classpathentry kind="con" path="com.google.appengine.eclipse.core.GAE_CONTAINER"/>
<classpathentry kind="output" path="war/WEB-INF/classes"/> <classpathentry kind="output" path="war/WEB-INF/classes"/>
</classpath> </classpath>

View File

@@ -10,11 +10,6 @@
<arguments> <arguments>
</arguments> </arguments>
</buildCommand> </buildCommand>
<buildCommand>
<name>com.google.appengine.eclipse.core.enhancerbuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand> <buildCommand>
<name>com.google.appengine.eclipse.core.projectValidator</name> <name>com.google.appengine.eclipse.core.projectValidator</name>
<arguments> <arguments>
@@ -25,6 +20,11 @@
<arguments> <arguments>
</arguments> </arguments>
</buildCommand> </buildCommand>
<buildCommand>
<name>com.google.appengine.eclipse.core.enhancerbuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec> </buildSpec>
<natures> <natures>
<nature>org.eclipse.jdt.core.javanature</nature> <nature>org.eclipse.jdt.core.javanature</nature>

View File

@@ -1,4 +1,3 @@
#Sun Aug 21 01:00:53 BST 2011
eclipse.preferences.version=1 eclipse.preferences.version=1
filesCopiedToWebInfLib=appengine-api-1.0-sdk-1.5.2.jar|appengine-api-labs-1.5.2.jar|appengine-jsr107cache-1.5.2.jar|jsr107cache-1.1.jar|datanucleus-appengine-1.0.9.final.jar|datanucleus-core-1.1.5.jar|datanucleus-jpa-1.1.5.jar|geronimo-jpa_3.0_spec-1.1.1.jar|geronimo-jta_1.1_spec-1.1.1.jar|jdo2-api-2.3-eb.jar filesCopiedToWebInfLib=appengine-api-labs.jar|appengine-endpoints.jar|appengine-jsr107cache-1.7.0.jar|jsr107cache-1.1.jar|appengine-api-1.0-sdk-1.7.0.jar|datanucleus-core-1.1.5.jar|jdo2-api-2.3-eb.jar|geronimo-jta_1.1_spec-1.1.1.jar|geronimo-jpa_3.0_spec-1.1.1.jar|datanucleus-jpa-1.1.5.jar|datanucleus-appengine-1.0.10.final.jar
ormEnhancementInclusions=src/**|c2dm/** ormEnhancementInclusions=src/**|c2dm/**

View File

@@ -16,6 +16,9 @@
package com.google.android.c2dm.server; package com.google.android.c2dm.server;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
@@ -26,14 +29,12 @@ import javax.jdo.JDOObjectNotFoundException;
import javax.jdo.PersistenceManager; import javax.jdo.PersistenceManager;
import javax.jdo.PersistenceManagerFactory; import javax.jdo.PersistenceManagerFactory;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
/** /**
* Stores config information related to data messaging. * Stores config information related to data messaging.
* *
*/ */
public class C2DMConfigLoader { public class C2DMConfigLoader {
private static final String TOKEN_FILE = "/dataMessagingToken.txt";
private final PersistenceManagerFactory PMF; private final PersistenceManagerFactory PMF;
private static final Logger log = Logger.getLogger(C2DMConfigLoader.class.getName()); private static final Logger log = Logger.getLogger(C2DMConfigLoader.class.getName());
@@ -72,8 +73,6 @@ public class C2DMConfigLoader {
/** /**
* Return the auth token from the database. Should be called * Return the auth token from the database. Should be called
* only if the old token expired. * only if the old token expired.
*
* @return
*/ */
public String getToken() { public String getToken() {
if (currentToken == null) { if (currentToken == null) {
@@ -110,12 +109,19 @@ public class C2DMConfigLoader {
dmConfig.setKey(key); dmConfig.setKey(key);
// Must be in classpath, before sending. Do not checkin ! // Must be in classpath, before sending. Do not checkin !
try { try {
InputStream is = C2DMConfigLoader.class.getClassLoader().getResourceAsStream("/dataMessagingToken.txt"); InputStream is = C2DMConfigLoader.class.getClassLoader().getResourceAsStream(TOKEN_FILE);
String token;
if (is != null) { if (is != null) {
BufferedReader reader = new BufferedReader(new InputStreamReader(is)); BufferedReader reader = new BufferedReader(new InputStreamReader(is));
String token = reader.readLine(); token = reader.readLine();
dmConfig.setAuthToken(token); } else {
// happens on developement: delete entity from viewer, change
// token below, and run it again
log.log(Level.WARNING, "File " + TOKEN_FILE +
" not found on classpath, using hardcoded token");
token = "please_change_me";
} }
dmConfig.setAuthToken(token);
} catch (Throwable t) { } catch (Throwable t) {
log.log(Level.SEVERE, log.log(Level.SEVERE,
"Can't load initial token, use admin console", t); "Can't load initial token, use admin console", t);

View File

@@ -20,7 +20,6 @@ import java.io.IOException;
import java.util.Map; import java.util.Map;
import java.util.logging.Logger; import java.util.logging.Logger;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
@@ -48,7 +47,7 @@ public class C2DMRetryServlet extends HttpServlet {
*/ */
@Override @Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException { throws IOException {
String registrationId = req.getParameter(C2DMessaging.PARAM_REGISTRATION_ID); String registrationId = req.getParameter(C2DMessaging.PARAM_REGISTRATION_ID);
String retryCount = req.getHeader(RETRY_COUNT); String retryCount = req.getHeader(RETRY_COUNT);
@@ -61,6 +60,7 @@ public class C2DMRetryServlet extends HttpServlet {
} }
} }
@SuppressWarnings("unchecked")
Map<String, String[]> params = req.getParameterMap(); Map<String, String[]> params = req.getParameterMap();
String collapse = req.getParameter(C2DMessaging.PARAM_COLLAPSE_KEY); String collapse = req.getParameter(C2DMessaging.PARAM_COLLAPSE_KEY);
boolean delayWhenIdle = boolean delayWhenIdle =

View File

@@ -34,10 +34,10 @@ import javax.jdo.PersistenceManagerFactory;
import javax.servlet.ServletContext; import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import com.google.appengine.api.labs.taskqueue.Queue; import com.google.appengine.api.taskqueue.Queue;
import com.google.appengine.api.labs.taskqueue.QueueFactory; import com.google.appengine.api.taskqueue.QueueFactory;
import com.google.appengine.api.labs.taskqueue.TaskHandle; import com.google.appengine.api.taskqueue.TaskHandle;
import com.google.appengine.api.labs.taskqueue.TaskOptions; import com.google.appengine.api.taskqueue.TaskOptions;
/** /**
*/ */
@@ -243,8 +243,7 @@ public class C2DMessaging {
public boolean sendNoRetry(String token, String collapseKey, public boolean sendNoRetry(String token, String collapseKey,
String name1, String value1, String name2, String value2, String name1, String value1, String name2, String value2,
String name3, String value3) String name3, String value3) {
throws IOException {
Map<String, String[]> params = new HashMap<String, String[]>(); Map<String, String[]> params = new HashMap<String, String[]>();
if (value1 != null) params.put("data." + name1, new String[] {value1}); if (value1 != null) params.put("data." + name1, new String[] {value1});
@@ -258,9 +257,15 @@ public class C2DMessaging {
} }
} }
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, public boolean sendNoRetry(String token, String collapseKey,
String... nameValues) String... nameValues) {
throws IOException {
Map<String, String[]> params = new HashMap<String, String[]>(); Map<String, String[]> params = new HashMap<String, String[]>();
int len = nameValues.length; int len = nameValues.length;
@@ -276,8 +281,12 @@ public class C2DMessaging {
} }
try { try {
C2DM_EXCEPTION.remove();
return sendNoRetry(token, collapseKey, params, true); return sendNoRetry(token, collapseKey, params, true);
} catch (IOException ex) { } 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 false;
} }
} }
@@ -287,7 +296,7 @@ public class C2DMessaging {
Queue dmQueue = QueueFactory.getQueue("c2dm"); Queue dmQueue = QueueFactory.getQueue("c2dm");
try { try {
TaskOptions url = TaskOptions url =
TaskOptions.Builder.url(C2DMRetryServlet.URI) TaskOptions.Builder.withUrl(C2DMRetryServlet.URI)
.param(C2DMessaging.PARAM_REGISTRATION_ID, token) .param(C2DMessaging.PARAM_REGISTRATION_ID, token)
.param(C2DMessaging.PARAM_COLLAPSE_KEY, collapseKey); .param(C2DMessaging.PARAM_COLLAPSE_KEY, collapseKey);
if (delayWhileIdle) { if (delayWhileIdle) {

View File

@@ -2,8 +2,6 @@
*/ */
package com.google.android.chrometophone.server; package com.google.android.chrometophone.server;
import java.io.IOException;
import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
@@ -11,7 +9,7 @@ import javax.servlet.http.HttpServletResponse;
public class DebugServlet extends HttpServlet { public class DebugServlet extends HttpServlet {
@Override @Override
public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { public void doGet(HttpServletRequest req, HttpServletResponse resp) {
// Nothing, we're just looking for logs to find response times and delivery // Nothing, we're just looking for logs to find response times and delivery
// confirmation. // confirmation.
// TODO: use memcache to dynamically get statistics. // TODO: use memcache to dynamically get statistics.

View File

@@ -16,9 +16,15 @@
package com.google.android.chrometophone.server; package com.google.android.chrometophone.server;
import com.google.appengine.api.datastore.Key;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.jdo.PersistenceManager; import javax.jdo.PersistenceManager;
import javax.jdo.Query; import javax.jdo.Query;
@@ -27,8 +33,6 @@ import javax.jdo.annotations.PersistenceCapable;
import javax.jdo.annotations.Persistent; import javax.jdo.annotations.Persistent;
import javax.jdo.annotations.PrimaryKey; import javax.jdo.annotations.PrimaryKey;
import com.google.appengine.api.datastore.Key;
/** /**
* Registration info. * Registration info.
* *
@@ -39,8 +43,12 @@ import com.google.appengine.api.datastore.Key;
*/ */
@PersistenceCapable(identityType = IdentityType.APPLICATION) @PersistenceCapable(identityType = IdentityType.APPLICATION)
public class DeviceInfo { public class DeviceInfo {
private static final Logger log =
Logger.getLogger(DeviceInfo.class.getName());
public static final String TYPE_AC2DM = "ac2dm"; public static final String TYPE_AC2DM = "ac2dm";
public static final String TYPE_CHROME = "chrome"; public static final String TYPE_CHROME = "chrome";
public static final String TYPE_GCM = "gcm";
/** /**
* User-email # device-id * User-email # device-id
@@ -147,11 +155,11 @@ public class DeviceInfo {
/** /**
* Helper function - will query all registrations for a user. * Helper function - will query all registrations for a user.
*/ */
@SuppressWarnings("unchecked")
public static List<DeviceInfo> getDeviceInfoForUser(PersistenceManager pm, String user) { public static List<DeviceInfo> getDeviceInfoForUser(PersistenceManager pm, String user) {
Query query = pm.newQuery(DeviceInfo.class); Query query = pm.newQuery(DeviceInfo.class);
query.setFilter("key >= '" + query.setFilter("key >= '" +
user + "' && key < '" + user + "$'"); user + "' && key < '" + user + "$'");
@SuppressWarnings("unchecked")
List<DeviceInfo> qresult = (List<DeviceInfo>) query.execute(); List<DeviceInfo> qresult = (List<DeviceInfo>) query.execute();
// Copy to array - we need to close the query // Copy to array - we need to close the query
List<DeviceInfo> result = new ArrayList<DeviceInfo>(); List<DeviceInfo> result = new ArrayList<DeviceInfo>();
@@ -161,4 +169,27 @@ public class DeviceInfo {
query.closeAll(); query.closeAll();
return result; return result;
} }
/**
* Helper function - get number of devices registered by type.
*/
public static Map<String, Integer> getDevicesUsage(PersistenceManager pm) {
Map<String, Integer> result = new HashMap<String, Integer>();
Query query = pm.newQuery(DeviceInfo.class);
addStats(query, result, TYPE_AC2DM);
addStats(query, result, TYPE_GCM);
addStats(query, result, TYPE_CHROME);
query.closeAll();
return result;
}
private static void addStats(Query query, Map<String, Integer> result, String type) {
query.setResult("count(this)");
query.setFilter("type == '" + type + "'");
Integer total = (Integer) query.execute();
log.log(Level.INFO, "Number of records of type {0}: {1}",
new Object[] {type, total});
result.put(type, total);
}
} }

View File

@@ -33,9 +33,9 @@ import com.google.android.c2dm.server.C2DMessaging;
import com.google.appengine.api.channel.ChannelServiceFactory; import com.google.appengine.api.channel.ChannelServiceFactory;
import com.google.appengine.api.datastore.Key; import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory; import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.repackaged.org.json.JSONArray; import com.google.appengine.labs.repackaged.org.json.JSONArray;
import com.google.appengine.repackaged.org.json.JSONException; import com.google.appengine.labs.repackaged.org.json.JSONException;
import com.google.appengine.repackaged.org.json.JSONObject; import com.google.appengine.labs.repackaged.org.json.JSONObject;
@SuppressWarnings("serial") @SuppressWarnings("serial")
public class RegisterServlet extends HttpServlet { public class RegisterServlet extends HttpServlet {

View File

@@ -23,8 +23,8 @@ import com.google.appengine.api.oauth.OAuthServiceFactory;
import com.google.appengine.api.users.User; import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserService; import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory; import com.google.appengine.api.users.UserServiceFactory;
import com.google.appengine.repackaged.org.json.JSONException; import com.google.appengine.labs.repackaged.org.json.JSONException;
import com.google.appengine.repackaged.org.json.JSONObject; import com.google.appengine.labs.repackaged.org.json.JSONObject;
/** /**
* Common code and helpers to handle a request and manipulate device info. * Common code and helpers to handle a request and manipulate device info.
@@ -112,7 +112,9 @@ public class RequestInfo {
return null; return null;
} }
} else { } else {
ri.parameterMap = req.getParameterMap(); @SuppressWarnings("unchecked")
Map<String, String[]> castMap = req.getParameterMap();
ri.parameterMap = castMap;
} }
ri.deviceRegistrationID = ri.getParameter("devregid"); ri.deviceRegistrationID = ri.getParameter("devregid");
@@ -130,6 +132,15 @@ public class RequestInfo {
return null; return null;
} }
// check if account was really set on development environment
if (ri.userName.endsWith("@example.com")) {
String account = req.getParameter("account");
if (account != null) {
log.log(Level.INFO, "Using " + account + " instead of " + ri.userName);
ri.userName = account;
}
}
if (ctx != null) { if (ctx != null) {
ri.initDevices(ctx); ri.initDevices(ctx);
} }

View File

@@ -16,6 +16,10 @@
package com.google.android.chrometophone.server; package com.google.android.chrometophone.server;
import com.google.android.c2dm.server.C2DMessaging;
import com.google.appengine.api.channel.ChannelMessage;
import com.google.appengine.api.channel.ChannelServiceFactory;
import java.io.IOException; import java.io.IOException;
import java.util.logging.Logger; import java.util.logging.Logger;
@@ -23,10 +27,6 @@ import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import com.google.android.c2dm.server.C2DMessaging;
import com.google.appengine.api.channel.ChannelMessage;
import com.google.appengine.api.channel.ChannelServiceFactory;
@SuppressWarnings("serial") @SuppressWarnings("serial")
public class SendServlet extends HttpServlet { public class SendServlet extends HttpServlet {
static final Logger log = static final Logger log =
@@ -112,7 +112,6 @@ public class SendServlet extends HttpServlet {
continue; // user-specified device type continue; // user-specified device type
} }
try {
if (deviceInfo.getType().equals(DeviceInfo.TYPE_CHROME)) { if (deviceInfo.getType().equals(DeviceInfo.TYPE_CHROME)) {
res = doSendViaBrowserChannel(url, deviceInfo); res = doSendViaBrowserChannel(url, deviceInfo);
} else { } else {
@@ -126,10 +125,9 @@ public class SendServlet extends HttpServlet {
} else { } else {
log.warning("Error: Unable to send link to device: " + log.warning("Error: Unable to send link to device: " +
deviceInfo.getDeviceRegistrationID()); deviceInfo.getDeviceRegistrationID());
} IOException ex = C2DMessaging.getC2dmException();
} catch (IOException ex) { if (ex != null) {
if ("NotRegistered".equals(ex.getMessage()) || if ("InvalidRegistration".equals(ex.getMessage())) {
"InvalidRegistration".equals(ex.getMessage())) {
// Prune device, it no longer works // Prune device, it no longer works
reqInfo.deleteRegistration(deviceInfo.getDeviceRegistrationID()); reqInfo.deleteRegistration(deviceInfo.getDeviceRegistrationID());
reqInfo.devices.remove(deviceInfo); reqInfo.devices.remove(deviceInfo);
@@ -139,6 +137,7 @@ public class SendServlet extends HttpServlet {
} }
} }
} }
}
if (ok) { if (ok) {
// TODO: return a count of devices we sent to, maybe names as well // TODO: return a count of devices we sent to, maybe names as well
@@ -157,7 +156,7 @@ public class SendServlet extends HttpServlet {
} }
private boolean doSendViaC2dm(String url, String title, String sel, C2DMessaging push, private boolean doSendViaC2dm(String url, String title, String sel, C2DMessaging push,
String collapseKey, DeviceInfo deviceInfo, boolean reqDebug) throws IOException { String collapseKey, DeviceInfo deviceInfo, boolean reqDebug) {
// Trim title, sel if needed. // Trim title, sel if needed.
if (url.length() + title.length() + sel.length() > 1000) { if (url.length() + title.length() + sel.length() > 1000) {
@@ -176,6 +175,7 @@ public class SendServlet extends HttpServlet {
} }
boolean res; boolean res;
System.out.println(">>>>> REG_ID: " + deviceInfo.getDeviceRegistrationID()); // TODO: tmp
res = push.sendNoRetry(deviceInfo.getDeviceRegistrationID(), res = push.sendNoRetry(deviceInfo.getDeviceRegistrationID(),
collapseKey, collapseKey,
"url", url, "url", url,

View File

@@ -0,0 +1,175 @@
/*
* Copyright 2012 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.chrometophone.server;
import com.google.android.c2dm.server.C2DMessaging;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.jdo.PersistenceManager;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Sends a link to a device without using the Chrome extension.
*
* Useful for debugging purposes.
*/
public class SenderServlet extends HttpServlet {
private static final Logger logger = Logger.getLogger(SenderServlet.class.getName());
private static final String ATTR_LOG = "log";
private static final String PARAM_ACCOUNT = "account";
private static final String PARAM_URL = "url";
@Override
public void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
String account = getParameter(req, PARAM_ACCOUNT);
String url = getParameter(req, PARAM_URL);
String log = (String) req.getAttribute(ATTR_LOG);
resp.setContentType("text/html");
PrintWriter out = resp.getWriter();
out.println("<html>");
out.println("<head>");
out.println("<title>Sender</title>");
out.println("</head>");
out.println("<body>");
out.println("<h1>Send a link to the phone</h1>");
if (log != null) {
out.println("<h3>Log from previous request</h3>");
out.println(log);
out.println("<br/>");
}
out.write("<form method='POST'>");
out.println("<table>");
out.println("<tr><td align='right'>Account:</td><td>" +
"<input type='text' name='" + PARAM_ACCOUNT + "' value='" + account + "' size='40'>" +
"</td></tr>");
out.println("<tr><td align='right'>Link:</td><td>" +
"<input type='text' name='" + PARAM_URL + "' value='" + url + "' size='80'>" +
"</td></tr>");
out.println("<tr><td colspan='2' align='center'>" +
"<input type='submit' value='Send'/></td></tr>");
out.println("</table>");
out.println("</form>");
out.println("</body></html>");
resp.setStatus(HttpServletResponse.SC_OK);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
String account = getParameter(req, PARAM_ACCOUNT);
String url = getParameter(req, PARAM_URL);
ServletContext ctx = getServletContext();
PersistenceManager pm = C2DMessaging.getPMF(ctx).getPersistenceManager();
StringBuilder log = new StringBuilder();
log.append("Getting devices for account ").append(account).append("<br>");
List<DeviceInfo> devices = DeviceInfo.getDeviceInfoForUser(pm, account);
pm.close();
log.append("Number of devices found: ").append(devices.size()).append("<br>");
for (DeviceInfo device : devices) {
String type = device.getType();
String regId = device.getDeviceRegistrationID();
log.append("Sending ").append(url).append(" to device of type ")
.append(type).append("<br/>");
sendLink(req, account, regId, type, url, log);
}
req.setAttribute(ATTR_LOG, log.toString());
doGet(req, resp);
}
private void sendLink(HttpServletRequest req, String account, String regId, String type,
String url, StringBuilder log) throws IOException {
// use the /send servlet to send the link - we could use C2DMessaging
// directly, but the whole idea of this servlet is to emulate the
// Chrome plugin work
String sendServletUrl = req.getScheme() + "://" + req.getServerName() + ":" + req.getServerPort() +
"/" + req.getContextPath() + "send";
String postUrl = sendServletUrl + "?url=" + URLEncoder.encode(url, "UTF-8") +
"&deviceType=" + type + "&devregid=" + URLEncoder.encode(regId, "UTF-8") +
"&account=" + URLEncoder.encode(account, "UTF-8");
HttpURLConnection conn = (HttpURLConnection) new URL(postUrl).openConnection();
conn.setDoOutput(true);
conn.setUseCaches(false);
conn.setFixedLengthStreamingMode(0); // no content, just parameters
conn.setRequestMethod("POST");
conn.setRequestProperty("X-Same-Domain", "1");
OutputStream out = null;
String status = null;
String responseBody = null;
try {
out = conn.getOutputStream();
out.close();
int statusCode = conn.getResponseCode();
status = Integer.toString(statusCode);
try {
InputStream stream = (statusCode == 200) ? conn.getInputStream() : conn.getErrorStream();
responseBody = getString(stream);
log.append("\tPOST status: ").append(statusCode).append(" Body: ").append(responseBody).append("<br/>");
} catch (Exception e) {
logger.log(Level.SEVERE, "Exception posting to " + postUrl, e);
log.append("POST threw exception: ").append(e);
}
} catch (Exception e) {
logger.log(Level.SEVERE, "Exception posting to " + postUrl, e);
log.append("POST threw exception: ").append(e);
}
}
private String getParameter(HttpServletRequest req, String name) {
String value = req.getParameter(name);
return (value == null || value.trim().length() == 0) ? "" : value.trim();
}
private static String getString(InputStream stream) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
StringBuilder content = new StringBuilder();
String newLine;
do {
newLine = reader.readLine();
if (newLine != null) {
content.append(newLine).append('\n');
}
} while (newLine != null);
if (content.length() > 0) {
// strip last newline
content.setLength(content.length() - 1);
}
return content.toString();
}
}

View File

@@ -0,0 +1,83 @@
/*
* Copyright 2012 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.chrometophone.server;
import com.google.android.c2dm.server.C2DMessaging;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Map;
import java.util.Map.Entry;
import javax.jdo.PersistenceManager;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Shows the statistics of how many devices use C2DM or GCM.
*
* Useful for debugging purposes.
*/
public class StatsServlet extends HttpServlet {
@Override
public void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException{
ServletContext ctx = getServletContext();
PersistenceManager pm = C2DMessaging.getPMF(ctx).getPersistenceManager();
Map<String, Integer> stats = DeviceInfo.getDevicesUsage(pm);
pm.close();
resp.setContentType("text/html");
PrintWriter out = resp.getWriter();
out.println("<html><body>");
out.println("<head>");
out.println("<title>Device stats</title>");
out.println("</head>");
out.println("<body>");
out.println("<h3>Device stats</h3>");
if (stats.isEmpty()) {
out.println("<p>No devices registered yet!</p>");
} else {
int total = 0;
for (Integer count : stats.values()) {
total += count;
}
out.println("<table cellspacing='2' cellpadding='2'><tr><th>Type</th>" +
"<th>Count</th><th>Share</th></tr>");
for (Entry<String, Integer> entry : stats.entrySet()) {
String type = entry.getKey();
int count = entry.getValue();
float share = (100*count) / total;
out.println("<tr>" +
"<td align='right'>" + type + "</td>" +
"<td align='right'>" + count + "</td>" +
"<td align='right'>" + share + "%</td></tr>");
}
out.println("<tr>" +
"<td align='right'>Total</td>" +
"<td align='right'>" + total+ "</td>" +
"<td align='right'>100.0%</td></tr>");
out.println("</table>");
}
out.println("</body></html>");
resp.setStatus(HttpServletResponse.SC_OK);
}
}

View File

@@ -17,6 +17,7 @@
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0"> <appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<application>chrometophone</application> <application>chrometophone</application>
<version>11</version> <version>11</version>
<threadsafe>true</threadsafe>
<system-properties> <system-properties>
<property name="java.util.logging.config.file" value="WEB-INF/logging.properties"/> <property name="java.util.logging.config.file" value="WEB-INF/logging.properties"/>
</system-properties> </system-properties>

View File

@@ -62,6 +62,18 @@
</servlet-class> </servlet-class>
</servlet> </servlet>
<servlet>
<servlet-name>SenderServlet</servlet-name>
<servlet-class>com.google.android.chrometophone.server.SenderServlet
</servlet-class>
</servlet>
<servlet>
<servlet-name>StatsServlet</servlet-name>
<servlet-class>com.google.android.chrometophone.server.StatsServlet
</servlet-class>
</servlet>
<servlet-mapping> <servlet-mapping>
<servlet-name>RegisterServlet</servlet-name> <servlet-name>RegisterServlet</servlet-name>
<url-pattern>/register</url-pattern> <url-pattern>/register</url-pattern>
@@ -97,6 +109,16 @@
<url-pattern>/signout</url-pattern> <url-pattern>/signout</url-pattern>
</servlet-mapping> </servlet-mapping>
<servlet-mapping>
<servlet-name>SenderServlet</servlet-name>
<url-pattern>/admin/sender</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>StatsServlet</servlet-name>
<url-pattern>/admin/stats</url-pattern>
</servlet-mapping>
<servlet-mapping> <servlet-mapping>
<servlet-name>dataMessagingServlet</servlet-name> <servlet-name>dataMessagingServlet</servlet-name>
<url-pattern>/tasks/c2dm</url-pattern> <url-pattern>/tasks/c2dm</url-pattern>
@@ -111,4 +133,13 @@
<role-name>admin</role-name> <role-name>admin</role-name>
</auth-constraint> </auth-constraint>
</security-constraint> </security-constraint>
<security-constraint>
<web-resource-collection>
<web-resource-name>admin</web-resource-name>
<url-pattern>/admin/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>admin</role-name>
</auth-constraint>
</security-constraint>
</web-app> </web-app>