/* * 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 edu.dartmouth.cs.gcmdemo.gcm; import java.io.BufferedReader; import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.util.HashMap; import java.util.Map; import java.util.Random; import java.util.logging.Level; import java.util.logging.Logger; import com.google.appengine.api.log.InvalidRequestException; /** * Helper class to send messages to the GCM service using an API Key. */ public class Sender { private String GCM_SEND_URL = "https://android.googleapis.com/gcm/send"; /** * Initial delay before first retry, without jitter. */ protected static final int BACKOFF_INITIAL_DELAY = 1000; /** * Maximum delay before a retry. */ protected static final int MAX_BACKOFF_DELAY = 1024000; protected final Random random = new Random(); protected static final Logger logger = Logger.getLogger(Sender.class .getName()); private final String key; /** * Default constructor. * * @param key * API key obtained through the Google API Console. */ public Sender(String key) { this.key = nonNull(key); } /** * Sends a message to one device, retrying in case of unavailability. * *
* Note: this method uses exponential back-off to retry in * case of service unavailability and hence could block the calling thread * for many seconds. * * @param message * message to be sent, including the device's registration id. * @param registrationId * device where the message will be sent. * @param retries * number of retries in case of service unavailability errors. * * @return result of the request (see its javadoc for more details). * * @throws IllegalArgumentException * if registrationId is {@literal null}. * @throws InvalidRequestException * if GCM didn't returned a 200 or 5xx status. * @throws IOException * if message could not be sent. */ public Response send(Message message, int retries) throws IOException { Response result; int attempt = 0; int backoff = BACKOFF_INITIAL_DELAY; boolean tryAgain; do { attempt++; result = send(message); tryAgain = (result == null || result.mHttpStatus != 200) && attempt <= retries; if (tryAgain) { int sleepTime = backoff / 2 + random.nextInt(backoff); try { Thread.sleep(sleepTime); } catch (Exception ex) { } if (2 * backoff < MAX_BACKOFF_DELAY) { backoff *= 2; } } } while (tryAgain); if (result == null) { throw new IOException("Could not send message after " + attempt + " attempts"); } return result; } public Response send(Message message) throws IOException { Response result = new Response(); HttpURLConnection conn; try { conn = post(GCM_SEND_URL, "application/json", key, message.toString()); result.mHttpStatus = conn.getResponseCode(); } catch (IOException e) { logger.log(Level.FINE, "IOException posting to GCM", e); return result; } if (result.mHttpStatus / 100 == 5) { logger.fine("GCM service is unavailable (status " + result.mHttpStatus + ")"); return result; } if (result.mHttpStatus != 200 && result.mHttpStatus != 400) { try { result.mMessage = getAndClose(conn.getErrorStream()); logger.finest("Plain post error response: " + result.mMessage); } catch (IOException e) { result.mMessage = "N/A"; logger.log(Level.FINE, "Exception reading response: ", e); } } else { try { result.mMessage = getAndClose(conn.getInputStream()); } catch (IOException e) { logger.log(Level.WARNING, "Exception reading response: ", e); // return null so it can retry return null; } } return result; } private static void close(Closeable closeable) { if (closeable != null) { try { closeable.close(); } catch (IOException e) { // ignore error logger.log(Level.FINEST, "IOException closing stream", e); } } } /** * Makes an HTTP POST request to a given endpoint. * *
* Note: the returned connected should not be
* disconnected, otherwise it would kill persistent connections made using
* Keep-Alive.
*
* @param url
* endpoint to post the request.
* @param contentType
* type of request.
* @param body
* body of the request.
*
* @return the underlying connection.
*
* @throws IOException
* propagated from underlying methods.
*/
protected HttpURLConnection post(String url, String contentType,
String apiKey, String body) throws IOException {
if (url == null || body == null) {
throw new IllegalArgumentException("arguments cannot be null");
}
logger.fine("Sending POST to " + url);
logger.finest("POST body: " + body);
byte[] bytes = body.getBytes();
HttpURLConnection conn = getConnection(url);
conn.setDoOutput(true);
conn.setUseCaches(false);
conn.setFixedLengthStreamingMode(bytes.length);
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", contentType);
conn.setRequestProperty("Authorization", "key=" + key);
OutputStream out = conn.getOutputStream();
try {
out.write(bytes);
} finally {
close(out);
}
return conn;
}
/**
* Creates a map with just one key-value pair.
*/
protected static final Map
* If the stream ends in a newline character, it will be stripped.
*
* If the stream is {@literal null}, returns an empty string.
*/
protected static String getString(InputStream stream) throws IOException {
if (stream == null) {
return "";
}
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();
}
private static String getAndClose(InputStream stream) throws IOException {
try {
return getString(stream);
} finally {
if (stream != null) {
close(stream);
}
}
}
static