Handle selection

This commit is contained in:
burke.davey
2010-05-22 22:00:55 +00:00
parent e66cc9579e
commit 4ad00c31d6
2 changed files with 64 additions and 56 deletions

View File

@@ -45,7 +45,7 @@ import com.google.appengine.api.labs.taskqueue.TaskOptions;
public class C2DMessaging { public class C2DMessaging {
private static final String UPDATE_CLIENT_AUTH = "Update-Client-Auth"; private static final String UPDATE_CLIENT_AUTH = "Update-Client-Auth";
public static final String DATAMESSAGING_SEND_ENDPOINT = public static final String DATAMESSAGING_SEND_ENDPOINT =
"https://android.clients.google.com/c2dm/send"; "https://android.clients.google.com/c2dm/send";
private static final Logger log = Logger.getLogger(C2DMessaging.class.getName()); private static final Logger log = Logger.getLogger(C2DMessaging.class.getName());
@@ -53,11 +53,11 @@ public class C2DMessaging {
public static final String PARAM_REGISTRATION_ID = "registration_id"; public static final String PARAM_REGISTRATION_ID = "registration_id";
public static final String PARAM_DELAY_WHILE_IDLE = "delay_while_idle"; public static final String PARAM_DELAY_WHILE_IDLE = "delay_while_idle";
public static final String PARAM_COLLAPSE_KEY = "collapse_key"; public static final String PARAM_COLLAPSE_KEY = "collapse_key";
private static final String UTF8 = "UTF-8"; private static final String UTF8 = "UTF-8";
/** /**
* Jitter - random interval to wait before retry. * Jitter - random interval to wait before retry.
*/ */
@@ -66,11 +66,11 @@ public class C2DMessaging {
static C2DMessaging singleton; static C2DMessaging singleton;
final C2DMConfigLoader serverConfig; final C2DMConfigLoader serverConfig;
private C2DMessaging(C2DMConfigLoader serverConfig) { private C2DMessaging(C2DMConfigLoader serverConfig) {
this.serverConfig = serverConfig; this.serverConfig = serverConfig;
} }
public synchronized static C2DMessaging get(ServletContext servletContext) { public synchronized static C2DMessaging get(ServletContext servletContext) {
if (singleton == null) { if (singleton == null) {
C2DMConfigLoader serverConfig = new C2DMConfigLoader(getPMF(servletContext)); C2DMConfigLoader serverConfig = new C2DMConfigLoader(getPMF(servletContext));
@@ -78,7 +78,7 @@ public class C2DMessaging {
} }
return singleton; return singleton;
} }
public synchronized static C2DMessaging get(PersistenceManagerFactory pmf) { public synchronized static C2DMessaging get(PersistenceManagerFactory pmf) {
if (singleton == null) { if (singleton == null) {
C2DMConfigLoader serverConfig = new C2DMConfigLoader(pmf); C2DMConfigLoader serverConfig = new C2DMConfigLoader(pmf);
@@ -86,18 +86,18 @@ public class C2DMessaging {
} }
return singleton; return singleton;
} }
C2DMConfigLoader getServerConfig() { C2DMConfigLoader getServerConfig() {
return serverConfig; return serverConfig;
} }
/** /**
* Initialize PMF - we use a context attribute, so other servlets can * Initialize PMF - we use a context attribute, so other servlets can
* be share the same instance. This is similar with a shared static * be share the same instance. This is similar with a shared static
* field, but avoids dependencies. * field, but avoids dependencies.
*/ */
public static PersistenceManagerFactory getPMF(ServletContext ctx) { public static PersistenceManagerFactory getPMF(ServletContext ctx) {
PersistenceManagerFactory pmfFactory = PersistenceManagerFactory pmfFactory =
(PersistenceManagerFactory) ctx.getAttribute( (PersistenceManagerFactory) ctx.getAttribute(
PersistenceManagerFactory.class.getName()); PersistenceManagerFactory.class.getName());
if (pmfFactory == null) { if (pmfFactory == null) {
@@ -110,13 +110,13 @@ public class C2DMessaging {
return pmfFactory; return pmfFactory;
} }
public boolean sendNoRetry(String registrationId, public boolean sendNoRetry(String registrationId,
String collapse, String collapse,
Map<String, String[]> params, Map<String, String[]> params,
boolean delayWhileIdle) boolean delayWhileIdle)
throws IOException { throws IOException {
// Send a sync message to this Android device. // Send a sync message to this Android device.
StringBuilder postDataBuilder = new StringBuilder(); StringBuilder postDataBuilder = new StringBuilder();
postDataBuilder.append(PARAM_REGISTRATION_ID). postDataBuilder.append(PARAM_REGISTRATION_ID).
@@ -126,13 +126,13 @@ public class C2DMessaging {
postDataBuilder.append("&") postDataBuilder.append("&")
.append(PARAM_DELAY_WHILE_IDLE).append("=1"); .append(PARAM_DELAY_WHILE_IDLE).append("=1");
} }
postDataBuilder.append("&").append(PARAM_COLLAPSE_KEY).append("="). postDataBuilder.append("&").append(PARAM_COLLAPSE_KEY).append("=").
append(collapse); append(collapse);
for (Object keyObj: params.keySet()) { for (Object keyObj: params.keySet()) {
String key = (String) keyObj; String key = (String) keyObj;
if (key.startsWith("data.")) { if (key.startsWith("data.")) {
String[] values = (String[]) params.get(key); String[] values = params.get(key);
postDataBuilder.append("&").append(key).append("="). postDataBuilder.append("&").append(key).append("=").
append(URLEncoder.encode(values[0], UTF8)); append(URLEncoder.encode(values[0], UTF8));
} }
@@ -142,7 +142,7 @@ public class C2DMessaging {
// Hit the dm URL. // Hit the dm URL.
URL url = new URL(DATAMESSAGING_SEND_ENDPOINT); URL url = new URL(DATAMESSAGING_SEND_ENDPOINT);
HttpURLConnection conn = (HttpURLConnection) url.openConnection(); HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setDoOutput(true); conn.setDoOutput(true);
conn.setUseCaches(false); conn.setUseCaches(false);
@@ -155,20 +155,20 @@ public class C2DMessaging {
OutputStream out = conn.getOutputStream(); OutputStream out = conn.getOutputStream();
out.write(postData); out.write(postData);
out.close(); out.close();
int responseCode = conn.getResponseCode(); int responseCode = conn.getResponseCode();
if (responseCode == HttpServletResponse.SC_UNAUTHORIZED || if (responseCode == HttpServletResponse.SC_UNAUTHORIZED ||
responseCode == HttpServletResponse.SC_FORBIDDEN) { responseCode == HttpServletResponse.SC_FORBIDDEN) {
// The token is too old - return false to retry later, will fetch the token // The token is too old - return false to retry later, will fetch the token
// from DB. This happens if the password is changed or token expires. Either admin // from DB. This happens if the password is changed or token expires. Either admin
// is updating the token, or Update-Client-Auth was received by another server, // is updating the token, or Update-Client-Auth was received by another server,
// and next retry will get the good one from database. // and next retry will get the good one from database.
log.warning("Unauthorized - need token"); log.warning("Unauthorized - need token");
serverConfig.invalidateCachedToken(); serverConfig.invalidateCachedToken();
return false; return false;
} }
// Check for updated token header // Check for updated token header
String updatedAuthToken = conn.getHeaderField(UPDATE_CLIENT_AUTH); String updatedAuthToken = conn.getHeaderField(UPDATE_CLIENT_AUTH);
if (updatedAuthToken != null && !authToken.equals(updatedAuthToken)) { if (updatedAuthToken != null && !authToken.equals(updatedAuthToken)) {
@@ -176,25 +176,25 @@ public class C2DMessaging {
updatedAuthToken); updatedAuthToken);
serverConfig.updateToken(updatedAuthToken); serverConfig.updateToken(updatedAuthToken);
} }
String responseLine = new BufferedReader(new InputStreamReader(conn.getInputStream())) String responseLine = new BufferedReader(new InputStreamReader(conn.getInputStream()))
.readLine(); .readLine();
// NOTE: You *MUST* use exponential backoff if you receive a 503 response code. // NOTE: You *MUST* use exponential backoff if you receive a 503 response code.
// Since App Engine's task queue mechanism automatically does this for tasks that // Since App Engine's task queue mechanism automatically does this for tasks that
// return non-success error codes, this is not explicitly implemented here. // return non-success error codes, this is not explicitly implemented here.
// If we weren't using App Engine, we'd need to manually implement this. // If we weren't using App Engine, we'd need to manually implement this.
log.info("Got " + responseCode + " response from Google datamessaging endpoint."); log.info("Got " + responseCode + " response from Google datamessaging endpoint.");
if (responseLine == null || responseLine.equals("")) { if (responseLine == null || responseLine.equals("")) {
throw new IOException("Got empty response from Google datamessaging endpoint."); throw new IOException("Got empty response from Google datamessaging endpoint.");
} }
String[] responseParts = responseLine.split("=", 2); String[] responseParts = responseLine.split("=", 2);
if (responseParts.length != 2) { if (responseParts.length != 2) {
log.warning("Invalid message from google: " + log.warning("Invalid message from google: " +
responseCode + " " + responseLine); responseCode + " " + responseLine);
throw new IOException("Invalid response from Google " + throw new IOException("Invalid response from Google " +
responseCode + " " + responseLine); responseCode + " " + responseLine);
} }
@@ -202,16 +202,16 @@ public class C2DMessaging {
log.info("Successfully sent data message to device: " + responseLine); log.info("Successfully sent data message to device: " + responseLine);
return true; return true;
} }
if (responseParts[0].equals("Error")) { if (responseParts[0].equals("Error")) {
String err = responseParts[1]; String err = responseParts[1];
log.warning("Got error response from Google datamessaging endpoint: " + err); log.warning("Got error response from Google datamessaging endpoint: " + err);
// No retry. // No retry.
// TODO(costin): show a nicer error to the user. // TODO(costin): show a nicer error to the user.
throw new IOException("Server error: " + err); throw new IOException("Server error: " + err);
} else { } else {
// 500 or unparseable response - server error, needs to retry // 500 or unparseable response - server error, needs to retry
log.warning("Invalid response from google " + responseLine + " " + log.warning("Invalid response from google " + responseLine + " " +
responseCode); responseCode);
return false; return false;
} }
@@ -219,44 +219,48 @@ public class C2DMessaging {
/** /**
* Helper method to send a message, with 2 parameters. * Helper method to send a message, with 2 parameters.
* *
* Permanent errors will result in IOException. * Permanent errors will result in IOException.
* Retriable errors will cause the message to be scheduled for retry. * Retriable errors will cause the message to be scheduled for retry.
*/ */
public void sendWithRetry(String token, String collapseKey, public void sendWithRetry(String token, String collapseKey,
String name1, String value1, String name2, String value2) String name1, String value1, String name2, String value2,
String name3, String value3)
throws IOException { throws IOException {
Map<String, String[]> params = new HashMap<String, String[]>(); Map<String, String[]> params = new HashMap<String, String[]>();
params.put("data." + name1, new String[] {value1}); if (value1 != null) params.put("data." + name1, new String[] {value1});
params.put("data." + name2, new String[] {value2}); if (value2 != null) params.put("data." + name2, new String[] {value2});
if (value3 != null) params.put("data." + name3, new String[] {value3});
boolean sentOk = sendNoRetry(token, collapseKey, params, true); boolean sentOk = sendNoRetry(token, collapseKey, params, true);
if (!sentOk) { if (!sentOk) {
retry(token, collapseKey, params, true); retry(token, collapseKey, params, true);
} }
} }
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)
throws IOException { throws IOException {
Map<String, String[]> params = new HashMap<String, String[]>(); Map<String, String[]> params = new HashMap<String, String[]>();
params.put("data." + name1, new String[] {value1}); if (value1 != null) params.put("data." + name1, new String[] {value1});
params.put("data." + name2, new String[] {value2}); if (value2 != null) params.put("data." + name2, new String[] {value2});
if (value3 != null) params.put("data." + name3, new String[] {value3});
try { try {
return sendNoRetry(token, collapseKey, params, true); return sendNoRetry(token, collapseKey, params, true);
} catch (IOException ex) { } catch (IOException ex) {
return false; return false;
} }
} }
private void retry(String token, String collapseKey, private void retry(String token, String collapseKey,
Map<String, String[]> params, boolean delayWhileIdle) { Map<String, String[]> params, boolean delayWhileIdle) {
Queue dmQueue = QueueFactory.getQueue("c2dm"); Queue dmQueue = QueueFactory.getQueue("c2dm");
try { try {
TaskOptions url = TaskOptions url =
TaskOptions.Builder.url(C2DMRetryServlet.URI) TaskOptions.Builder.url(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);
@@ -264,20 +268,20 @@ public class C2DMessaging {
url.param(PARAM_DELAY_WHILE_IDLE, "1"); url.param(PARAM_DELAY_WHILE_IDLE, "1");
} }
for (String key: params.keySet()) { for (String key: params.keySet()) {
String[] values = (String[]) params.get(key); String[] values = params.get(key);
url.param(key, URLEncoder.encode(values[0], UTF8)); url.param(key, URLEncoder.encode(values[0], UTF8));
} }
// Task queue implements the exponential backoff // Task queue implements the exponential backoff
long jitter = (int) Math.random() * DATAMESSAGING_MAX_JITTER_MSEC; long jitter = (int) Math.random() * DATAMESSAGING_MAX_JITTER_MSEC;
url.countdownMillis(jitter); url.countdownMillis(jitter);
TaskHandle add = dmQueue.add(url); TaskHandle add = dmQueue.add(url);
} catch (UnsupportedEncodingException e) { } catch (UnsupportedEncodingException e) {
// Ignore - UTF8 should be supported // Ignore - UTF8 should be supported
log.log(Level.SEVERE, "Unexpected error", e); log.log(Level.SEVERE, "Unexpected error", e);
} }
} }
} }

View File

@@ -40,15 +40,17 @@ public class SendServlet extends HttpServlet {
private static final String OK_STATUS = "OK"; private static final String OK_STATUS = "OK";
private static final String ERROR_STATUS = "ERROR"; private static final String ERROR_STATUS = "ERROR";
@Override
public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
doGet(req, resp); doGet(req, resp);
} }
@Override @Override
public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.setContentType("text/plain"); resp.setContentType("text/plain");
String url = req.getParameter("url"); String url = req.getParameter("url");
String title = req.getParameter("title"); String title = req.getParameter("title");
String sel = req.getParameter("sel"); // optional
if (url == null && title == null) { if (url == null && title == null) {
resp.setStatus(400); resp.setStatus(400);
resp.getWriter().println(ERROR_STATUS + " (Must specify url and title parameters)"); resp.getWriter().println(ERROR_STATUS + " (Must specify url and title parameters)");
@@ -58,17 +60,18 @@ public class SendServlet extends HttpServlet {
UserService userService = UserServiceFactory.getUserService(); UserService userService = UserServiceFactory.getUserService();
User user = userService.getCurrentUser(); User user = userService.getCurrentUser();
if (user != null) { if (user != null) {
doSendToPhone(url, title, user.getEmail(), resp); doSendToPhone(url, title, sel, user.getEmail(), resp);
} else { } else {
String followOnURL = req.getRequestURI() + "?title=" + String followOnURL = req.getRequestURI() + "?title=" +
URLEncoder.encode(req.getParameter("title"), "UTF-8") + URLEncoder.encode(req.getParameter("title"), "UTF-8") +
"&url=" + URLEncoder.encode(req.getParameter("url"), "UTF-8"); "&url=" + URLEncoder.encode(req.getParameter("url"), "UTF-8") +
"&sel=" + URLEncoder.encode(req.getParameter("sel"), "UTF-8");
resp.sendRedirect(userService.createLoginURL(followOnURL)); resp.sendRedirect(userService.createLoginURL(followOnURL));
} }
} }
private boolean doSendToPhone(String url, String title, String userAccount, private boolean doSendToPhone(String url, String title, String sel,
HttpServletResponse resp) throws IOException { String userAccount, HttpServletResponse resp) throws IOException {
// Get device info // Get device info
DeviceInfo deviceInfo = null; DeviceInfo deviceInfo = null;
// Shared PMF // Shared PMF
@@ -91,7 +94,8 @@ public class SendServlet extends HttpServlet {
// Send push message to phone // Send push message to phone
C2DMessaging push = C2DMessaging.get(getServletContext()); C2DMessaging push = C2DMessaging.get(getServletContext());
if (push.sendNoRetry(deviceInfo.getDeviceRegistrationID(), if (push.sendNoRetry(deviceInfo.getDeviceRegistrationID(),
"" + url.hashCode(), "url", url, "title", title)) { "" + url.hashCode(), "url", url, "title", title,
"sel", sel)) {
log.info("Link sent to phone!"); log.info("Link sent to phone!");
resp.getWriter().println(OK_STATUS); resp.getWriter().println(OK_STATUS);
return true; return true;