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="c2dm"/>
<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"/>
</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.params.HttpClientParams;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpParams;
@@ -46,6 +47,14 @@ import android.util.Log;
*/
public class AppEngineClient {
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_TOKEN_TYPE = "ah";
@@ -79,10 +88,15 @@ public class AppEngineClient {
authToken = getAuthToken(mContext, account);
}
// Get ACSID cookie
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;
URI uri = new URI(AUTH_URL + "?continue=" +
uri = new URI(AUTH_URL + "?continue=" +
URLEncoder.encode(continueURL, "UTF-8") +
"&auth=" + authToken);
HttpGet method = new HttpGet(uri);
@@ -90,14 +104,13 @@ public class AppEngineClient {
HttpClientParams.setRedirecting(getParams, false); // continue is not used
method.setParams(getParams);
HttpResponse res = client.execute(method);
res = client.execute(method);
Header[] headers = res.getHeaders("Set-Cookie");
if (res.getStatusLine().getStatusCode() != 302 ||
headers.length == 0) {
return res;
}
String ascidCookie = null;
for (Header header: headers) {
if (header.getValue().indexOf("ACSID=") >=0) {
// let's parse it
@@ -106,14 +119,22 @@ public class AppEngineClient {
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
uri = new URI(BASE_URL + urlPath);
uri = new URI(baseUrl + urlPath);
HttpPost post = new HttpPost(uri);
UrlEncodedFormEntity entity =
new UrlEncodedFormEntity(params, "UTF-8");
post.setEntity(entity);
if (ascidCookie != null) {
post.setHeader("Cookie", ascidCookie);
}
post.setHeader("X-Same-Domain", "1"); // XSRF
res = client.execute(post);
return res;

View File

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

View File

@@ -3,6 +3,6 @@
<classpathentry kind="src" path="src"/>
<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/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"/>
</classpath>

View File

@@ -10,11 +10,6 @@
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.google.appengine.eclipse.core.enhancerbuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.google.appengine.eclipse.core.projectValidator</name>
<arguments>
@@ -25,6 +20,11 @@
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.google.appengine.eclipse.core.enhancerbuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<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
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/**

View File

@@ -16,6 +16,9 @@
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.InputStream;
import java.io.InputStreamReader;
@@ -26,14 +29,12 @@ import javax.jdo.JDOObjectNotFoundException;
import javax.jdo.PersistenceManager;
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.
*
*/
public class C2DMConfigLoader {
private static final String TOKEN_FILE = "/dataMessagingToken.txt";
private final PersistenceManagerFactory PMF;
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
* only if the old token expired.
*
* @return
*/
public String getToken() {
if (currentToken == null) {
@@ -110,12 +109,19 @@ public class C2DMConfigLoader {
dmConfig.setKey(key);
// Must be in classpath, before sending. Do not checkin !
try {
InputStream is = C2DMConfigLoader.class.getClassLoader().getResourceAsStream("/dataMessagingToken.txt");
InputStream is = C2DMConfigLoader.class.getClassLoader().getResourceAsStream(TOKEN_FILE);
String token;
if (is != null) {
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
String token = reader.readLine();
dmConfig.setAuthToken(token);
token = reader.readLine();
} 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) {
log.log(Level.SEVERE,
"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.logging.Logger;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -48,7 +47,7 @@ public class C2DMRetryServlet extends HttpServlet {
*/
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
throws IOException {
String registrationId = req.getParameter(C2DMessaging.PARAM_REGISTRATION_ID);
String retryCount = req.getHeader(RETRY_COUNT);
@@ -61,6 +60,7 @@ public class C2DMRetryServlet extends HttpServlet {
}
}
@SuppressWarnings("unchecked")
Map<String, String[]> params = req.getParameterMap();
String collapse = req.getParameter(C2DMessaging.PARAM_COLLAPSE_KEY);
boolean delayWhenIdle =

View File

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

View File

@@ -2,8 +2,6 @@
*/
package com.google.android.chrometophone.server;
import java.io.IOException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -11,7 +9,7 @@ import javax.servlet.http.HttpServletResponse;
public class DebugServlet extends HttpServlet {
@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
// confirmation.
// TODO: use memcache to dynamically get statistics.

View File

@@ -16,9 +16,15 @@
package com.google.android.chrometophone.server;
import com.google.appengine.api.datastore.Key;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
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.Query;
@@ -27,8 +33,6 @@ import javax.jdo.annotations.PersistenceCapable;
import javax.jdo.annotations.Persistent;
import javax.jdo.annotations.PrimaryKey;
import com.google.appengine.api.datastore.Key;
/**
* Registration info.
*
@@ -39,8 +43,12 @@ import com.google.appengine.api.datastore.Key;
*/
@PersistenceCapable(identityType = IdentityType.APPLICATION)
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_CHROME = "chrome";
public static final String TYPE_GCM = "gcm";
/**
* User-email # device-id
@@ -147,11 +155,11 @@ public class DeviceInfo {
/**
* Helper function - will query all registrations for a user.
*/
@SuppressWarnings("unchecked")
public static List<DeviceInfo> getDeviceInfoForUser(PersistenceManager pm, String user) {
Query query = pm.newQuery(DeviceInfo.class);
query.setFilter("key >= '" +
user + "' && key < '" + user + "$'");
@SuppressWarnings("unchecked")
List<DeviceInfo> qresult = (List<DeviceInfo>) query.execute();
// Copy to array - we need to close the query
List<DeviceInfo> result = new ArrayList<DeviceInfo>();
@@ -161,4 +169,27 @@ public class DeviceInfo {
query.closeAll();
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.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.repackaged.org.json.JSONArray;
import com.google.appengine.repackaged.org.json.JSONException;
import com.google.appengine.repackaged.org.json.JSONObject;
import com.google.appengine.labs.repackaged.org.json.JSONArray;
import com.google.appengine.labs.repackaged.org.json.JSONException;
import com.google.appengine.labs.repackaged.org.json.JSONObject;
@SuppressWarnings("serial")
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.UserService;
import com.google.appengine.api.users.UserServiceFactory;
import com.google.appengine.repackaged.org.json.JSONException;
import com.google.appengine.repackaged.org.json.JSONObject;
import com.google.appengine.labs.repackaged.org.json.JSONException;
import com.google.appengine.labs.repackaged.org.json.JSONObject;
/**
* Common code and helpers to handle a request and manipulate device info.
@@ -112,7 +112,9 @@ public class RequestInfo {
return null;
}
} else {
ri.parameterMap = req.getParameterMap();
@SuppressWarnings("unchecked")
Map<String, String[]> castMap = req.getParameterMap();
ri.parameterMap = castMap;
}
ri.deviceRegistrationID = ri.getParameter("devregid");
@@ -130,6 +132,15 @@ public class RequestInfo {
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) {
ri.initDevices(ctx);
}

View File

@@ -16,6 +16,10 @@
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.util.logging.Logger;
@@ -23,10 +27,6 @@ import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
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")
public class SendServlet extends HttpServlet {
static final Logger log =
@@ -112,7 +112,6 @@ public class SendServlet extends HttpServlet {
continue; // user-specified device type
}
try {
if (deviceInfo.getType().equals(DeviceInfo.TYPE_CHROME)) {
res = doSendViaBrowserChannel(url, deviceInfo);
} else {
@@ -126,10 +125,9 @@ public class SendServlet extends HttpServlet {
} else {
log.warning("Error: Unable to send link to device: " +
deviceInfo.getDeviceRegistrationID());
}
} catch (IOException ex) {
if ("NotRegistered".equals(ex.getMessage()) ||
"InvalidRegistration".equals(ex.getMessage())) {
IOException ex = C2DMessaging.getC2dmException();
if (ex != null) {
if ("InvalidRegistration".equals(ex.getMessage())) {
// Prune device, it no longer works
reqInfo.deleteRegistration(deviceInfo.getDeviceRegistrationID());
reqInfo.devices.remove(deviceInfo);
@@ -139,6 +137,7 @@ public class SendServlet extends HttpServlet {
}
}
}
}
if (ok) {
// 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,
String collapseKey, DeviceInfo deviceInfo, boolean reqDebug) throws IOException {
String collapseKey, DeviceInfo deviceInfo, boolean reqDebug) {
// Trim title, sel if needed.
if (url.length() + title.length() + sel.length() > 1000) {
@@ -176,6 +175,7 @@ public class SendServlet extends HttpServlet {
}
boolean res;
System.out.println(">>>>> REG_ID: " + deviceInfo.getDeviceRegistrationID()); // TODO: tmp
res = push.sendNoRetry(deviceInfo.getDeviceRegistrationID(),
collapseKey,
"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">
<application>chrometophone</application>
<version>11</version>
<threadsafe>true</threadsafe>
<system-properties>
<property name="java.util.logging.config.file" value="WEB-INF/logging.properties"/>
</system-properties>

View File

@@ -62,6 +62,18 @@
</servlet-class>
</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-name>RegisterServlet</servlet-name>
<url-pattern>/register</url-pattern>
@@ -97,6 +109,16 @@
<url-pattern>/signout</url-pattern>
</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-name>dataMessagingServlet</servlet-name>
<url-pattern>/tasks/c2dm</url-pattern>
@@ -111,4 +133,13 @@
<role-name>admin</role-name>
</auth-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>