Refactoring (phone-to-chrome now uses same code paths as chrome-to-phone). Bumped version number.

This commit is contained in:
burke.davey
2010-09-12 21:56:06 +00:00
parent 3671d70eff
commit 44a2ab3f9d
7 changed files with 126 additions and 169 deletions

View File

@@ -1,67 +0,0 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.chrometophone.server;
import java.io.IOException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.google.appengine.api.channel.ChannelMessage;
import com.google.appengine.api.channel.ChannelService;
import com.google.appengine.api.channel.ChannelServiceFactory;
import com.google.appengine.api.users.User;
@SuppressWarnings("serial")
public class BrowserChannelServlet extends HttpServlet {
private static final String OK_STATUS = "OK";
private static final String LOGIN_REQUIRED_STATUS = "LOGIN_REQUIRED";
private static final String ERROR_STATUS = "ERROR";
@Override
public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.setContentType("text/plain");
// Basic XSRF protection
if (req.getHeader("X-Same-Domain") == null) {
resp.setStatus(400);
resp.getWriter().println(ERROR_STATUS);
return;
}
User user = RegisterServlet.checkUser(req, resp, false);
if (user == null) {
resp.setStatus(400);
resp.getWriter().println(LOGIN_REQUIRED_STATUS);
} else {
String channelToken = String.valueOf(user.hashCode()); // channel per user
String data = req.getParameter("data");
if (data != null) { // send data
getChannelService().sendMessage(new ChannelMessage(channelToken, data));
resp.getWriter().print(OK_STATUS);
} else { // setup channel
String channelId = getChannelService().createChannel(channelToken);
resp.getWriter().print(channelId);
}
}
}
private ChannelService getChannelService() {
return ChannelServiceFactory.getChannelService();
}
}

View File

@@ -31,20 +31,23 @@ import com.google.appengine.api.datastore.Key;
/**
* Registration info.
*
*
* An account may be associated with multiple phones,
* and a phone may be associated with multiple accounts.
*
*
* registrations lists different phones registered to that account.
*/
@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class DeviceInfo {
public static final String TYPE_AC2DM = "ac2dm";
public static final String TYPE_CHROME = "chrome";
/**
* User-email # device-id
*
*
* Device-id can be specified by device, default is hash of abs(registration
* id).
*
*
* user@example.com#1234
*/
@PrimaryKey
@@ -54,39 +57,42 @@ public class DeviceInfo {
@Persistent
private String deviceRegistrationID;
/**
* Each device should provide a stable ID. It can be the
/**
* Each device should provide a stable ID. It can be the
* hash of the first registration, the phone ID, etc.
* Using the name seems error-prone, users may use the default
* which may be the same in identical phones, they may change name, etc.
* Using the name seems error-prone, users may use the default
* which may be the same in identical phones, they may change name, etc.
*/
@Persistent
private String id;
/**
* Current supported types:
* (default) - ac2dm, regular froyo+ devices using C2DM protocol
*
* New types may be defined - for example for sending to chrome.
*
* New types may be defined - for example for sending to chrome.
*/
@Persistent
private String type;
/**
/**
* Friendly name for the device. May be edited by the user.
*/
@Persistent
private String name;
/**
* For statistics - and to provide hints to the user.
*/
@Persistent
private Date registrationTimestamp;
@Persistent
private Boolean debug;
@Persistent
private Boolean phoneToChromeExperimentEnabled;
public DeviceInfo(Key key, String deviceRegistrationID) {
this.key = key;
this.deviceRegistrationID = deviceRegistrationID;
@@ -98,13 +104,22 @@ public class DeviceInfo {
}
public boolean getDebug() {
return debug != null ? debug.booleanValue() : false;
return (debug != null ? debug.booleanValue() : false);
}
public void setDebug(boolean debug) {
this.debug = new Boolean(debug);
}
public boolean getPhoneToChromeExperimentEnabled() {
return (phoneToChromeExperimentEnabled != null ?
phoneToChromeExperimentEnabled.booleanValue() : false);
}
public void setPhoneToChromeExperimentEnabled(boolean phoneToChromeExperimentEnabled) {
this.phoneToChromeExperimentEnabled = new Boolean(phoneToChromeExperimentEnabled);
}
public Key getKey() {
return key;
}
@@ -121,7 +136,7 @@ public class DeviceInfo {
this.deviceRegistrationID = deviceRegistrationID;
}
public void setId(String id) {
this.id = id;
}
@@ -153,10 +168,11 @@ public class DeviceInfo {
public Date getRegistrationTimestamp() {
return registrationTimestamp;
}
/**
* 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 >= '" +
@@ -170,5 +186,4 @@ public class DeviceInfo {
query.closeAll();
return result;
}
}

View File

@@ -21,12 +21,14 @@ import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.jdo.JDOObjectNotFoundException;
import javax.jdo.PersistenceManager;
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.ChannelServiceFactory;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.oauth.OAuthService;
@@ -40,10 +42,12 @@ public class RegisterServlet extends HttpServlet {
private static final Logger log =
Logger.getLogger(RegisterServlet.class.getName());
private static final String OK_STATUS = "OK";
private static final String NOT_ENABLED_STATUS = "NOT_ENABLED";
private static final String LOGIN_REQUIRED_STATUS = "LOGIN_REQUIRED";
private static final String ERROR_STATUS = "ERROR";
private static int MAX_DEVICES = 3;
private static int MAX_DEVICES = 5;
/**
* Get the user using the UserService.
*
@@ -73,7 +77,7 @@ public class RegisterServlet extends HttpServlet {
// TODO: redirect to OAuth/user service login, or send the URL
// TODO: 401 instead of 400
resp.setStatus(400);
resp.getWriter().println(ERROR_STATUS + " (Not authorized)");
resp.getWriter().println(LOGIN_REQUIRED_STATUS);
}
return user;
}
@@ -90,8 +94,8 @@ public class RegisterServlet extends HttpServlet {
return;
}
String deviceRegistrationID = req.getParameter("devregid");
if (deviceRegistrationID == null) {
String deviceRegistrationId = req.getParameter("devregid");
if (deviceRegistrationId == null) {
resp.setStatus(400);
resp.getWriter().println(ERROR_STATUS + "(Must specify devregid)");
return;
@@ -101,17 +105,17 @@ public class RegisterServlet extends HttpServlet {
if (deviceName == null) {
deviceName = "Phone";
}
String deviceId = req.getParameter("deviceId");
if (deviceId == null) {
deviceId = Long.toHexString(Math.abs(deviceRegistrationID.hashCode()));
deviceId = Long.toHexString(Math.abs(deviceRegistrationId.hashCode()));
}
String deviceType = req.getParameter("deviceType");
if (deviceType == null) {
deviceType = "ac2dm";
deviceType = "ac2dm";
}
User user = checkUser(req, resp, true);
if (user != null) {
// Context-shared PMF.
@@ -138,19 +142,38 @@ public class RegisterServlet extends HttpServlet {
}
pm.deletePersistent(oldest);
}
// TODO: dup ? update
String id = Long.toHexString(Math.abs(deviceRegistrationID.hashCode()));
Key key = KeyFactory.createKey(DeviceInfo.class.getSimpleName(),
// TODO: dup ? update
String id = Long.toHexString(Math.abs(deviceRegistrationId.hashCode()));
Key key = KeyFactory.createKey(DeviceInfo.class.getSimpleName(),
user.getEmail() + "#" + id);
DeviceInfo device = new DeviceInfo(key, deviceRegistrationID);
device.setId(deviceId);
device.setName(deviceName);
pm.makePersistent(device);
resp.getWriter().println(OK_STATUS);
// Get device if it already exists, else create
DeviceInfo device = null;
try {
device = pm.getObjectById(DeviceInfo.class, key);
} catch (JDOObjectNotFoundException e) { }
if (device == null) {
device = new DeviceInfo(key, deviceRegistrationId);
device.setId(deviceId);
device.setName(deviceName);
device.setType(deviceType);
pm.makePersistent(device);
}
if (device.getType().equals(DeviceInfo.TYPE_CHROME)) {
if (device.getPhoneToChromeExperimentEnabled()) {
String channelId =
ChannelServiceFactory.getChannelService().createChannel(deviceRegistrationId);
resp.getWriter().println(OK_STATUS + " " + channelId);
} else {
resp.setStatus(400);
resp.getWriter().println(NOT_ENABLED_STATUS);
}
} else {
resp.getWriter().println(OK_STATUS);
}
} catch (Exception e) {
resp.setStatus(500);
resp.getWriter().println(ERROR_STATUS + " (Error registering device)");

View File

@@ -20,15 +20,14 @@ import java.io.IOException;
import java.util.List;
import java.util.logging.Logger;
import javax.jdo.JDOObjectNotFoundException;
import javax.jdo.PersistenceManager;
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.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.channel.ChannelMessage;
import com.google.appengine.api.channel.ChannelServiceFactory;
import com.google.appengine.api.users.User;
@SuppressWarnings("serial")
@@ -41,23 +40,11 @@ public class SendServlet extends HttpServlet {
private static final String ERROR_STATUS = "ERROR";
// GET not supported
@Override
public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.setContentType("text/plain");
// Check API version
String apiVersionString = req.getParameter("ver");
if (apiVersionString == null) apiVersionString = "1";
int apiVersion = Integer.parseInt(apiVersionString);
if (apiVersion < 3) {
resp.setStatus(400);
resp.getWriter().println(ERROR_STATUS +
" (Please remove old Chrome extension and install latest)");
log.warning("Old extension version not supported: " + apiVersion);
return;
}
// Basic XSRF protection (TODO: remove X-Extension in a future release for consistency)
if (req.getHeader("X-Same-Domain") == null && req.getHeader("X-Extension") == null) {
resp.setStatus(400);
@@ -69,45 +56,45 @@ public class SendServlet extends HttpServlet {
String sel = req.getParameter("sel");
if (sel == null) sel = ""; // optional
String url = req.getParameter("url");
String title = req.getParameter("title");
if (title == null) title = ""; // optional
String url = req.getParameter("url");
if (url == null) {
resp.setStatus(400);
resp.getWriter().println(ERROR_STATUS + " (Must specify url and title parameters)");
resp.getWriter().println(ERROR_STATUS + " (Must specify url parameter)");
return;
}
if (title == null) {
title = "";
}
String deviceId = req.getParameter("deviceId");
String deviceName = req.getParameter("deviceName");
String deviceType = req.getParameter("deviceType");
if (deviceType == null) deviceType = DeviceInfo.TYPE_AC2DM;
User user = RegisterServlet.checkUser(req, resp, false);
if (user != null) {
doSendToPhone(url, title, sel, user.getEmail(), deviceId, deviceName,
resp);
doSendToDevice(url, title, sel, user.getEmail(),
deviceId, deviceName, deviceType, resp);
} else {
resp.getWriter().println(LOGIN_REQUIRED_STATUS);
}
}
private boolean doSendToPhone(String url, String title, String sel,
String userAccount, String deviceId,
String deviceName, HttpServletResponse resp) throws IOException {
private boolean doSendToDevice(String url, String title, String sel, String userAccount,
String deviceId, String deviceName, String deviceType, HttpServletResponse resp) throws IOException {
// ok = we sent to at least one phone.
boolean ok = false;
// Send push message to phone
C2DMessaging push = C2DMessaging.get(getServletContext());
boolean res = false;
String collapseKey = "" + url.hashCode();
PersistenceManager pm =
C2DMessaging.getPMF(getServletContext()).getPersistenceManager();
List<DeviceInfo> registrations = null;
List<DeviceInfo> registrations = null;
try {
registrations = DeviceInfo.getDeviceInfoForUser(C2DMessaging.getPMF(getServletContext())
.getPersistenceManager(), userAccount);
@@ -115,24 +102,30 @@ public class SendServlet extends HttpServlet {
pm.close();
}
if (registrations.size() == 0) {
log.warning("Device not registered");
resp.getWriter().println(DEVICE_NOT_REGISTERED_STATUS);
return false;
}
for (DeviceInfo deviceInfo: registrations) {
for (DeviceInfo deviceInfo : registrations) {
if (deviceId != null && !deviceId.equals(deviceInfo.getId())) {
continue; // user-specified device
continue; // user-specified device id
}
if (deviceName != null && !deviceName.equals(deviceInfo.getName())) {
continue; // user-specified device
continue; // user-specified device name
}
if (deviceType != null && !deviceType.equals(deviceInfo.getType())) {
continue; // user-specified device type
}
// if name or value are null - they'll be skipped
try {
res = doSend(url, title, sel, push, collapseKey, deviceInfo);
if (deviceInfo.getType().equals(DeviceInfo.TYPE_CHROME)) {
res = doSendViaBrowserChannel(url, deviceInfo);
} else {
res = doSendViaC2dm(url, title, sel, push, collapseKey, deviceInfo);
}
if (res) {
log.info("Link sent to phone! collapse_key:" + collapseKey);
@@ -151,46 +144,52 @@ public class SendServlet extends HttpServlet {
}
}
}
if (ok) {
resp.getWriter().println(OK_STATUS);
return true;
return true;
} else {
resp.setStatus(500);
resp.getWriter().println(ERROR_STATUS + " (Unable to send link)");
return false;
return false;
}
}
boolean doSend(String url, String title, String sel, C2DMessaging push,
boolean doSendViaC2dm(String url, String title, String sel, C2DMessaging push,
String collapseKey, DeviceInfo deviceInfo) throws IOException {
// Trim title, sel if needed.
if (url.length() + title.length() + sel.length() > 1000) {
// Shorten the title - C2DM has a 1024 limit, some padding for keys
if (title.length() > 16) {
title = title.substring(0, 16);
title = title.substring(0, 16);
}
// still not enough ?
if (title.length() + url.length() + sel.length() > 1000) {
// how much space we have for sel ?
// how much space we have for sel ?
int space = 1000 - url.length() - title.length();
if (space > 0 && sel.length() > space) {
sel = sel.substring(0, space);
} // else: we'll get an error sending
}
// TODO: when we have history, save the url/title/sel in the history
// TODO: when we have history, save the url/title/sel in the history
// and send a pointer, have device fetch it.
}
boolean res;
res = push.sendNoRetry(deviceInfo.getDeviceRegistrationID(),
collapseKey,
"url", url,
collapseKey,
"url", url,
"title", title,
"sel", sel,
"debug", deviceInfo.getDebug() ? "1" : null);
return res;
}
boolean doSendViaBrowserChannel(String url, DeviceInfo deviceInfo) {
String channelToken = deviceInfo.getDeviceRegistrationID();
ChannelServiceFactory.getChannelService().sendMessage(
new ChannelMessage(channelToken, url));
return true;
}
}

View File

@@ -27,8 +27,6 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.google.android.c2dm.server.C2DMessaging;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;
@@ -57,7 +55,7 @@ public class UnregisterServlet extends HttpServlet {
resp.getWriter().println(ERROR_STATUS + " (Must specify devregid)");
return;
}
// Authorize & store device info
UserService userService = UserServiceFactory.getUserService();
User user = userService.getCurrentUser();
@@ -74,7 +72,7 @@ public class UnregisterServlet extends HttpServlet {
break;
}
}
resp.getWriter().println(OK_STATUS);
} catch (JDOObjectNotFoundException e) {
resp.setStatus(400);

View File

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

View File

@@ -42,12 +42,6 @@
</servlet-class>
</servlet>
<servlet>
<servlet-name>BrowserChannelServlet</servlet-name>
<servlet-class>com.google.android.chrometophone.server.BrowserChannelServlet
</servlet-class>
</servlet>
<servlet>
<servlet-name>dataMessagingServlet</servlet-name>
<servlet-class>
@@ -80,11 +74,6 @@
<url-pattern>/signout</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>BrowserChannelServlet</servlet-name>
<url-pattern>/browserchannel</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>dataMessagingServlet</servlet-name>
<url-pattern>/tasks/c2dm</url-pattern>