package org.jboss.aerogear.android.impl.simplepush; import android.util.Log; import com.google.common.base.Optional; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.gson.JsonArray; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.JsonPrimitive; import com.google.gson.JsonSyntaxException; import java.net.URI; import java.nio.channels.NotYetConnectedException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.http.HttpStatus; import org.java_websocket.client.WebSocketClient; import org.java_websocket.drafts.Draft_17; import org.java_websocket.handshake.ServerHandshake; import org.jboss.aerogear.android.Callback; import org.jboss.aerogear.android.http.HttpException; public class SimplePushWebsocketClient extends WebSocketClient { private static final String TAG = SimplePushWebsocketClient.class.getSimpleName(); private static final String MESSAGE_TYPE = "messageType"; private static final String CHANNEL_IDS = "channelIDs"; private static final String CHANNEL_ID = "channelID"; private static final String PUSH_ENDPOINT = "pushEndpoint"; private static final String UAID = "uaid"; private static final String STATUS = "status"; private String uaid = null; private List<String> channelIDs = ImmutableList.of(); private Map<String, Callback<PushChannel>> registrationMap = new HashMap<String, Callback<PushChannel>>(); private long lastMessage = -1; private CountDownLatch connectionLatch; public void connect(CountDownLatch connectionLatch) { this.connectionLatch = connectionLatch; new Thread(new Runnable() { @Override public void run() { connect(); } }).start(); } private enum MessageType { HELLO, REGISTER } public SimplePushWebsocketClient(URI uri) { super(uri, new Draft_17()); } @Override public void onOpen(ServerHandshake sh) { JsonObject message = new JsonObject(); Log.d(TAG, "Channel open"); connectionLatch.countDown(); message.addProperty(MESSAGE_TYPE, MessageType.HELLO.name().toLowerCase()); message.addProperty(UAID, Strings.nullToEmpty(uaid)); if (!channelIDs.isEmpty()) { List<String> channels = channelIDs; JsonArray channelIdsArray = new JsonArray(); for (String channel : channels) { channelIdsArray.add(new JsonPrimitive(channel)); } message.add(CHANNEL_IDS, channelIdsArray); } this.send(message.toString()); } @Override public void onMessage(String string) { Log.d(TAG, string); lastMessage = now(); try { JsonObject response = new JsonParser().parse(string).getAsJsonObject(); if (!response.has(MESSAGE_TYPE)) { return; } MessageType messageType = MessageType.valueOf(response.get(MESSAGE_TYPE).getAsString().toUpperCase()); switch (messageType) { case HELLO: uaid = response.get(UAID).getAsString(); connectionLatch.countDown(); break; case REGISTER: int status = response.get(STATUS).getAsInt(); String channelID = response.get(CHANNEL_ID).getAsString(); String pushEndpoint = response.get(PUSH_ENDPOINT).getAsString(); Callback<PushChannel> callback = registrationMap.get(channelID); switch (status) { case HttpStatus.SC_OK: if (callback == null) { //do Nothing but do not fail } else { registrationMap.remove(channelID); callback.onSuccess(new PushChannel(pushEndpoint, channelID)); } break; case HttpStatus.SC_CONFLICT: if (callback == null) { //do Nothing but do not fail } else { //retry with new UUID registrationMap.remove(channelID); registerChannel(callback); } break; case HttpStatus.SC_INTERNAL_SERVER_ERROR: if (callback == null) { Log.e(TAG, "The server returned a 500 error for channel:" + channelID); } else { //retry with new UUID registrationMap.remove(channelID); callback.onFailure(new HttpException(string.getBytes(), status)); } break; default: if (callback == null) { Log.e(TAG, "The server returned a " + status + " error for channel:" + channelID); } else { //retry with new UUID registrationMap.remove(channelID); callback.onFailure(new HttpException(string.getBytes(), status)); } } break; default: throw new AssertionError(messageType.name()); } } catch (JsonSyntaxException ex) { Log.e(TAG, ex.getMessage(), ex); } catch (NullPointerException ex) { Log.e(TAG, ex.getMessage(), ex); } } @Override public void onClose(int i, String string, boolean bln) { throw new UnsupportedOperationException("Not supported yet."); } @Override public void onError(Exception excptn) { Log.e(TAG, excptn.getMessage(), excptn); throw new UnsupportedOperationException("Not supported yet."); } /** * A globally unique UserAgent ID. Used by the PushServer to associate * channelIDs with a client. Stored by the UserAgent, but opaque to it. * * @return an optional UAID. */ public Optional<String> getUAID() { return Optional.fromNullable(uaid); } /** * A globally unique UserAgent ID. Used by the PushServer to associate * channelIDs with a client. Stored by the UserAgent, but opaque to it. * * @param uaid a new UAID, may be null. */ public void setUAID(String uaid) { this.uaid = uaid; } /** * Unique identifier for a Channel. Generated by UserAgent for a particular * application. Opaque identifier for both UserAgent and PushServer. This * MUST NOT be exposed to an application. * * @return a copied, mutable List of channelIDs */ public List<String> getChannelIDs() { return new ArrayList<String>(channelIDs); } /** * Unique identifier for a Channel. Generated by UserAgent for a particular * application. Opaque identifier for both UserAgent and PushServer. This * MUST NOT be exposed to an application. * * @param newChannelID a new Channel ID to add */ public void addChannelID(String newChannelID) { channelIDs = ImmutableList.<String>builder().addAll(channelIDs).add(newChannelID).build(); } /** * Unique identifier for a Channel. Generated by UserAgent for a particular * application. Opaque identifier for both UserAgent and PushServer. This * MUST NOT be exposed to an application. * * @param newChannelID a channelID to remove */ public void removeChannelID(String newChannelID) { List<String> tempIDs = new ArrayList<String>(channelIDs); tempIDs.remove(newChannelID); channelIDs = ImmutableList.<String>builder().addAll(tempIDs).build(); } /** * Unique identifier for a Channel. Generated by UserAgent for a particular * application. Opaque identifier for both UserAgent and PushServer. This * MUST NOT be exposed to an application. * * This method clears the set ChannelIDs */ public void clearChannelIDs() { channelIDs = ImmutableList.of(); } public void registerChannel(final Callback<PushChannel> callback) { JsonObject message = new JsonObject(); message.addProperty(MESSAGE_TYPE, MessageType.REGISTER.name().toLowerCase()); String channelID = UUID.randomUUID().toString(); message.addProperty(CHANNEL_ID, channelID); registrationMap.put(channelID, callback); send(message.toString()); } @Override public void send(final String text) throws NotYetConnectedException { new Thread(new Runnable() { @Override public void run() { try { connectionLatch.await(9000, TimeUnit.SECONDS); SimplePushWebsocketClient.super.send(text); } catch (InterruptedException ex) { Logger.getLogger(SimplePushWebsocketClient.class.getName()).log(Level.SEVERE, null, ex); } } }).start(); } public void ping() { send("{}"); } /** * * It is useful to know when the last message was received in order to * performing ping maintenance. * * @return the timestamp of when the last message was received. */ public long lastMessageTimestamp() { return lastMessage; } private long now() { return System.currentTimeMillis(); } }