diff --git a/android/.classpath b/android/.classpath index abb5e98..4b6b103 100644 --- a/android/.classpath +++ b/android/.classpath @@ -4,5 +4,6 @@ + diff --git a/android/default.properties b/android/default.properties deleted file mode 100644 index 0d9db12..0000000 --- a/android/default.properties +++ /dev/null @@ -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 diff --git a/android/src/com/google/android/apps/chrometophone/AppEngineClient.java b/android/src/com/google/android/apps/chrometophone/AppEngineClient.java index 808afb1..d720b89 100644 --- a/android/src/com/google/android/apps/chrometophone/AppEngineClient.java +++ b/android/src/com/google/android/apps/chrometophone/AppEngineClient.java @@ -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,41 +88,53 @@ public class AppEngineClient { authToken = getAuthToken(mContext, account); } - // Get ACSID cookie DefaultHttpClient client = new DefaultHttpClient(); - String continueURL = BASE_URL; - URI uri = new URI(AUTH_URL + "?continue=" + - URLEncoder.encode(continueURL, "UTF-8") + - "&auth=" + authToken); - HttpGet method = new HttpGet(uri); - final HttpParams getParams = new BasicHttpParams(); - HttpClientParams.setRedirecting(getParams, false); // continue is not used - method.setParams(getParams); - - HttpResponse res = client.execute(method); - Header[] headers = res.getHeaders("Set-Cookie"); - if (res.getStatusLine().getStatusCode() != 302 || - headers.length == 0) { - return res; - } - + String baseUrl; + URI uri; String ascidCookie = null; - for (Header header: headers) { - if (header.getValue().indexOf("ACSID=") >=0) { - // let's parse it - String value = header.getValue(); - String[] pairs = value.split(";"); - ascidCookie = pairs[0]; - } + HttpResponse res; + if (BASE_LOCAL_URL == null) { + // Get ACSID cookie so it can be used to authenticate on AppEngine + String continueURL = BASE_URL; + uri = new URI(AUTH_URL + "?continue=" + + URLEncoder.encode(continueURL, "UTF-8") + + "&auth=" + authToken); + HttpGet method = new HttpGet(uri); + final HttpParams getParams = new BasicHttpParams(); + HttpClientParams.setRedirecting(getParams, false); // continue is not used + method.setParams(getParams); + + res = client.execute(method); + Header[] headers = res.getHeaders("Set-Cookie"); + if (res.getStatusLine().getStatusCode() != 302 || + headers.length == 0) { + return res; + } + + for (Header header: headers) { + if (header.getValue().indexOf("ACSID=") >=0) { + // let's parse it + String value = header.getValue(); + String[] pairs = value.split(";"); + 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); - post.setHeader("Cookie", ascidCookie); + if (ascidCookie != null) { + post.setHeader("Cookie", ascidCookie); + } post.setHeader("X-Same-Domain", "1"); // XSRF res = client.execute(post); return res; diff --git a/android/src/com/google/android/apps/chrometophone/DeviceRegistrar.java b/android/src/com/google/android/apps/chrometophone/DeviceRegistrar.java index 032b8af..c1c7aa4 100644 --- a/android/src/com/google/android/apps/chrometophone/DeviceRegistrar.java +++ b/android/src/com/google/android/apps/chrometophone/DeviceRegistrar.java @@ -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 { diff --git a/appengine/.classpath b/appengine/.classpath index 3b83848..7cc2b56 100644 --- a/appengine/.classpath +++ b/appengine/.classpath @@ -3,6 +3,6 @@ - + diff --git a/appengine/.project b/appengine/.project index c405444..276fdc9 100644 --- a/appengine/.project +++ b/appengine/.project @@ -10,11 +10,6 @@ - - com.google.appengine.eclipse.core.enhancerbuilder - - - com.google.appengine.eclipse.core.projectValidator @@ -25,6 +20,11 @@ + + com.google.appengine.eclipse.core.enhancerbuilder + + + org.eclipse.jdt.core.javanature diff --git a/appengine/.settings/com.google.appengine.eclipse.core.prefs b/appengine/.settings/com.google.appengine.eclipse.core.prefs index 829a840..fe2ffd0 100644 --- a/appengine/.settings/com.google.appengine.eclipse.core.prefs +++ b/appengine/.settings/com.google.appengine.eclipse.core.prefs @@ -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/** diff --git a/appengine/c2dm/com/google/android/c2dm/server/C2DMConfigLoader.java b/appengine/c2dm/com/google/android/c2dm/server/C2DMConfigLoader.java index 65122f4..df84ab6 100644 --- a/appengine/c2dm/com/google/android/c2dm/server/C2DMConfigLoader.java +++ b/appengine/c2dm/com/google/android/c2dm/server/C2DMConfigLoader.java @@ -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); diff --git a/appengine/c2dm/com/google/android/c2dm/server/C2DMRetryServlet.java b/appengine/c2dm/com/google/android/c2dm/server/C2DMRetryServlet.java index 3acad97..6638336 100644 --- a/appengine/c2dm/com/google/android/c2dm/server/C2DMRetryServlet.java +++ b/appengine/c2dm/com/google/android/c2dm/server/C2DMRetryServlet.java @@ -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 params = req.getParameterMap(); String collapse = req.getParameter(C2DMessaging.PARAM_COLLAPSE_KEY); boolean delayWhenIdle = diff --git a/appengine/c2dm/com/google/android/c2dm/server/C2DMessaging.java b/appengine/c2dm/com/google/android/c2dm/server/C2DMessaging.java index b6f3842..0066205 100644 --- a/appengine/c2dm/com/google/android/c2dm/server/C2DMessaging.java +++ b/appengine/c2dm/com/google/android/c2dm/server/C2DMessaging.java @@ -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 params = new HashMap(); if (value1 != null) params.put("data." + name1, new String[] {value1}); @@ -257,10 +256,16 @@ public class C2DMessaging { return false; } } + + private static final ThreadLocal C2DM_EXCEPTION = + new ThreadLocal(); + + public static IOException getC2dmException() { + return C2DM_EXCEPTION.get(); + } public boolean sendNoRetry(String token, String collapseKey, - String... nameValues) - throws IOException { + String... nameValues) { Map params = new HashMap(); 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) { diff --git a/appengine/src/com/google/android/chrometophone/server/DebugServlet.java b/appengine/src/com/google/android/chrometophone/server/DebugServlet.java index e380eea..77418e0 100644 --- a/appengine/src/com/google/android/chrometophone/server/DebugServlet.java +++ b/appengine/src/com/google/android/chrometophone/server/DebugServlet.java @@ -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. diff --git a/appengine/src/com/google/android/chrometophone/server/DeviceInfo.java b/appengine/src/com/google/android/chrometophone/server/DeviceInfo.java index 55dd7f5..7a3ad09 100644 --- a/appengine/src/com/google/android/chrometophone/server/DeviceInfo.java +++ b/appengine/src/com/google/android/chrometophone/server/DeviceInfo.java @@ -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 getDeviceInfoForUser(PersistenceManager pm, String user) { Query query = pm.newQuery(DeviceInfo.class); query.setFilter("key >= '" + user + "' && key < '" + user + "$'"); + @SuppressWarnings("unchecked") List qresult = (List) query.execute(); // Copy to array - we need to close the query List result = new ArrayList(); @@ -161,4 +169,27 @@ public class DeviceInfo { query.closeAll(); return result; } + + /** + * Helper function - get number of devices registered by type. + */ + public static Map getDevicesUsage(PersistenceManager pm) { + Map result = new HashMap(); + 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 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); + } + } diff --git a/appengine/src/com/google/android/chrometophone/server/RegisterServlet.java b/appengine/src/com/google/android/chrometophone/server/RegisterServlet.java index bc787cc..25e77c5 100644 --- a/appengine/src/com/google/android/chrometophone/server/RegisterServlet.java +++ b/appengine/src/com/google/android/chrometophone/server/RegisterServlet.java @@ -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 { diff --git a/appengine/src/com/google/android/chrometophone/server/RequestInfo.java b/appengine/src/com/google/android/chrometophone/server/RequestInfo.java index c5a958e..05ac4d6 100644 --- a/appengine/src/com/google/android/chrometophone/server/RequestInfo.java +++ b/appengine/src/com/google/android/chrometophone/server/RequestInfo.java @@ -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 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); } diff --git a/appengine/src/com/google/android/chrometophone/server/SendServlet.java b/appengine/src/com/google/android/chrometophone/server/SendServlet.java index e5be1af..b8e8ef7 100644 --- a/appengine/src/com/google/android/chrometophone/server/SendServlet.java +++ b/appengine/src/com/google/android/chrometophone/server/SendServlet.java @@ -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 = @@ -75,7 +75,7 @@ public class SendServlet extends HttpServlet { } resp.getWriter().println(id); } - + protected String doSendToDevice(String url, String title, String sel, RequestInfo reqInfo, String deviceNames[], String deviceType) throws IOException { @@ -112,30 +112,29 @@ public class SendServlet extends HttpServlet { continue; // user-specified device type } - try { - if (deviceInfo.getType().equals(DeviceInfo.TYPE_CHROME)) { - res = doSendViaBrowserChannel(url, deviceInfo); - } else { - res = doSendViaC2dm(url, title, sel, push, collapseKey, - deviceInfo, reqDebug); - } + if (deviceInfo.getType().equals(DeviceInfo.TYPE_CHROME)) { + res = doSendViaBrowserChannel(url, deviceInfo); + } else { + res = doSendViaC2dm(url, title, sel, push, collapseKey, + deviceInfo, reqDebug); + } - if (res) { - log.info("Link sent to phone! collapse_key:" + collapseKey); - ok = true; - } else { - log.warning("Error: Unable to send link to device: " + - deviceInfo.getDeviceRegistrationID()); - } - } catch (IOException ex) { - if ("NotRegistered".equals(ex.getMessage()) || - "InvalidRegistration".equals(ex.getMessage())) { - // Prune device, it no longer works - reqInfo.deleteRegistration(deviceInfo.getDeviceRegistrationID()); - reqInfo.devices.remove(deviceInfo); - ac2dmCnt--; - } else { - throw ex; + if (res) { + 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()); + reqInfo.devices.remove(deviceInfo); + ac2dmCnt--; + } else { + throw ex; + } } } } @@ -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, diff --git a/appengine/src/com/google/android/chrometophone/server/SenderServlet.java b/appengine/src/com/google/android/chrometophone/server/SenderServlet.java new file mode 100644 index 0000000..ca136e2 --- /dev/null +++ b/appengine/src/com/google/android/chrometophone/server/SenderServlet.java @@ -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(""); + out.println(""); + out.println("Sender"); + out.println(""); + out.println(""); + out.println("

Send a link to the phone

"); + if (log != null) { + out.println("

Log from previous request

"); + out.println(log); + out.println("
"); + } + out.write("
"); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println("
Account:" + + "" + + "
Link:" + + "" + + "
" + + "
"); + out.println("
"); + + out.println(""); + 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("
"); + List devices = DeviceInfo.getDeviceInfoForUser(pm, account); + pm.close(); + log.append("Number of devices found: ").append(devices.size()).append("
"); + 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("
"); + 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("
"); + } 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(); + } +} diff --git a/appengine/src/com/google/android/chrometophone/server/StatsServlet.java b/appengine/src/com/google/android/chrometophone/server/StatsServlet.java new file mode 100644 index 0000000..ea3fea9 --- /dev/null +++ b/appengine/src/com/google/android/chrometophone/server/StatsServlet.java @@ -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 stats = DeviceInfo.getDevicesUsage(pm); + pm.close(); + + resp.setContentType("text/html"); + PrintWriter out = resp.getWriter(); + out.println(""); + out.println(""); + out.println("Device stats"); + out.println(""); + out.println(""); + out.println("

Device stats

"); + if (stats.isEmpty()) { + out.println("

No devices registered yet!

"); + } else { + int total = 0; + for (Integer count : stats.values()) { + total += count; + } + + out.println("" + + ""); + for (Entry entry : stats.entrySet()) { + String type = entry.getKey(); + int count = entry.getValue(); + float share = (100*count) / total; + out.println("" + + "" + + "" + + ""); + } + out.println("" + + "" + + "" + + ""); + out.println("
TypeCountShare
" + type + "" + count + "" + share + "%
Total" + total+ "100.0%
"); + } + + out.println(""); + resp.setStatus(HttpServletResponse.SC_OK); + } +} diff --git a/appengine/war/WEB-INF/appengine-web.xml b/appengine/war/WEB-INF/appengine-web.xml index da28fd3..99e3164 100644 --- a/appengine/war/WEB-INF/appengine-web.xml +++ b/appengine/war/WEB-INF/appengine-web.xml @@ -17,6 +17,7 @@ chrometophone 11 + true diff --git a/appengine/war/WEB-INF/web.xml b/appengine/war/WEB-INF/web.xml index 4db965d..a7cff56 100644 --- a/appengine/war/WEB-INF/web.xml +++ b/appengine/war/WEB-INF/web.xml @@ -62,6 +62,18 @@ + + SenderServlet + com.google.android.chrometophone.server.SenderServlet + + + + + StatsServlet + com.google.android.chrometophone.server.StatsServlet + + + RegisterServlet /register @@ -97,6 +109,16 @@ /signout + + SenderServlet + /admin/sender + + + + StatsServlet + /admin/stats + + dataMessagingServlet /tasks/c2dm @@ -111,4 +133,13 @@ admin + + + admin + /admin/* + + + admin + +