diff --git a/.gitignore b/.gitignore index afbdab3..d4a56c1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ .gradle -/local.properties -/.idea/workspace.xml +.idea /.idea/libraries +local.properties .DS_Store /build diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 217af47..96cc43e 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -1,7 +1,6 @@ - - - + \ No newline at end of file diff --git a/.idea/dictionaries/tylerroach.xml b/.idea/dictionaries/fburdy.xml similarity index 60% rename from .idea/dictionaries/tylerroach.xml rename to .idea/dictionaries/fburdy.xml index 67c3042..ed70e77 100644 --- a/.idea/dictionaries/tylerroach.xml +++ b/.idea/dictionaries/fburdy.xml @@ -1,3 +1,3 @@ - + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml deleted file mode 100644 index e206d70..0000000 --- a/.idea/encodings.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 696fee6..9dc4371 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -3,17 +3,17 @@ - - + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index d7b2b96..7be4039 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,11 +1,39 @@ - - - + + + + + + + @@ -16,7 +44,7 @@ - Abstraction issues + Android @@ -32,87 +60,11 @@ - + - - - - - - - - - - - - - - - - - - - - - localhost - 5050 - + + @@ -122,7 +74,7 @@ @@ -133,7 +85,7 @@ @@ -141,5 +93,4 @@ - - + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml index 7d7c4cf..ea0fd12 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -6,5 +6,4 @@ - - + \ No newline at end of file diff --git a/.idea/scopes/scope_settings.xml b/.idea/scopes/scope_settings.xml deleted file mode 100644 index 922003b..0000000 --- a/.idea/scopes/scope_settings.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml index def6a6a..35eb1dd 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -1,7 +1,6 @@ - + - - + \ No newline at end of file diff --git a/README.md b/README.md index c1ee7e6..bb8dc8f 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ # eventsource-android An Android EventSource (SSE) Library -This is a Java implementation of the EventSource - a client for Server-Sent Events. The implementation is based on Netty +This is a Java implementation of the EventSource - a client for Server-Sent Events. The implementation is based on Netty. + This project is based of off EventSource-Java: https://github.com/aslakhellesoy/eventsource-java @@ -21,6 +22,7 @@ Example implementation: public void run() { try { eventSource = new EventSource(Uri, new SSEHandler(), extraHeaderParameters); + /* eventSource = new EventSource(UriProxy, UriApi, new SSEHandler(), extraHeaderParameters)*/ eventSource.connect(); } catch(URISyntaxException e) { Log.v("Error starting eventsource", "True"); diff --git a/eventsource-android.iml b/eventsource-android.iml index 2a02201..ab41ce8 100644 --- a/eventsource-android.iml +++ b/eventsource-android.iml @@ -1,15 +1,14 @@ - + - - - + @@ -17,5 +16,4 @@ - - + \ No newline at end of file diff --git a/eventsource_android/eventsource_android.iml b/eventsource_android/eventsource_android.iml index cf8db6e..431f96a 100644 --- a/eventsource_android/eventsource_android.iml +++ b/eventsource_android/eventsource_android.iml @@ -1,5 +1,5 @@ - + @@ -9,21 +9,22 @@ - + @@ -60,22 +61,15 @@ + - - - - - - - - @@ -85,9 +79,8 @@ - + - - + \ No newline at end of file diff --git a/eventsource_android/src/main/AndroidManifest.xml b/eventsource_android/src/main/AndroidManifest.xml index 971aff4..4803636 100644 --- a/eventsource_android/src/main/AndroidManifest.xml +++ b/eventsource_android/src/main/AndroidManifest.xml @@ -1,8 +1,17 @@ + + package="tylerjroach.com.eventsource_android" > - + + + + + + + diff --git a/eventsource_android/src/main/java/tylerjroach/com/eventsource_android/EventSource.java b/eventsource_android/src/main/java/tylerjroach/com/eventsource_android/EventSource.java index da28d1b..b829d1a 100644 --- a/eventsource_android/src/main/java/tylerjroach/com/eventsource_android/EventSource.java +++ b/eventsource_android/src/main/java/tylerjroach/com/eventsource_android/EventSource.java @@ -1,13 +1,13 @@ package tylerjroach.com.eventsource_android; +import android.util.Log; + import org.jboss.netty.bootstrap.ClientBootstrap; import org.jboss.netty.channel.ChannelFuture; import org.jboss.netty.channel.ChannelPipeline; import org.jboss.netty.channel.ChannelPipelineFactory; import org.jboss.netty.channel.Channels; import org.jboss.netty.channel.socket.nio.NioClientSocketChannelFactory; -import org.jboss.netty.handler.codec.frame.DelimiterBasedFrameDecoder; -import org.jboss.netty.handler.codec.frame.Delimiters; import org.jboss.netty.handler.codec.http.HttpRequestEncoder; import org.jboss.netty.handler.codec.string.StringDecoder; import org.jboss.netty.handler.ssl.SslHandler; @@ -17,6 +17,7 @@ import java.util.Map; import java.util.concurrent.Executor; import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; import javax.net.ssl.SSLEngine; @@ -30,81 +31,104 @@ public class EventSource implements EventSourceHandler { public static final int OPEN = 1; public static final int CLOSED = 2; - private final ClientBootstrap bootstrap; - private final EventSourceChannelHandler clientHandler; - private final EventSourceHandler eventSourceHandler; + private static AtomicInteger count = new AtomicInteger(1); + private ClientBootstrap bootstrap; + private EventSourceChannelHandler clientHandler; + private EventSourceHandler eventSourceHandler; - private URI uri; + private URI uri, requestUri; private Map headers; private int readyState; /** - * Creates a new EventSource client. The client will reconnect on - * lost connections automatically, unless the connection is closed explicitly by a call to + * Creates a new EventSource client. The client will reconnect on + * lost connections automatically, unless the connection is closed explicitly by a call to * {@link tylerjroach.com.eventsource_android.EventSource#close()}. - * + *

* For sample usage, see examples at GitHub. - * - * @param executor the executor that will receive events + * + * @param executor the executor that will receive events * @param reconnectionTimeMillis delay before a reconnect is made - in the event of a lost connection - * @param pURI where to connect - * @param eventSourceHandler receives events - * @param headers Map of additional headers, such as passing auth tokens + * @param pURI where to connect + * @param eventSourceHandler receives events + * @param headers Map of additional headers, such as passing auth tokens * @see #close() */ + public EventSource(Executor executor, long reconnectionTimeMillis, final URI pURI, URI requestUri, EventSourceHandler eventSourceHandler, Map headers) { + this(executor, reconnectionTimeMillis, pURI, requestUri, null, eventSourceHandler, headers); + } + + public EventSource(Executor executor, long reconnectionTimeMillis, final URI pURI, EventSourceHandler eventSourceHandler, Map headers) { this(executor, reconnectionTimeMillis, pURI, null, eventSourceHandler, headers); } - public EventSource(Executor executor, long reconnectionTimeMillis, final URI pURI, SSLEngineFactory fSSLEngine, EventSourceHandler eventSourceHandler, Map headers) { + + public EventSource(Executor executor, long reconnectionTimeMillis, final URI pURI, URI requestUri, SSLEngineFactory fSSLEngine, EventSourceHandler eventSourceHandler, Map headers) { this.eventSourceHandler = eventSourceHandler; - bootstrap = new ClientBootstrap( new NioClientSocketChannelFactory( Executors.newSingleThreadExecutor(), Executors.newSingleThreadExecutor())); if (pURI.getScheme().equals("https") && fSSLEngine == null) { - fSSLEngine = new SSLEngineFactory(); + fSSLEngine = new SSLEngineFactory(); } else { - //If we don't do this then the pipeline still attempts to use SSL - fSSLEngine = null; + //If we don't do this then the pipeline still attempts to use SSL + fSSLEngine = null; } final SSLEngineFactory SSLFactory = fSSLEngine; - + uri = pURI; int port = uri.getPort(); - if (port==-1) - { - port = (uri.getScheme().equals("https"))?443:80; + // handle default port values + if (port == -1) { + port = (uri.getScheme().equals("https")) ? 443 : 80; } + bootstrap.setOption("remoteAddress", new InetSocketAddress(uri.getHost(), port)); - // add this class as the event source handler so the connect() call can be intercepted + // add this class as the event source handler so the connect() call can be intercepted. AsyncEventSourceHandler asyncHandler = new AsyncEventSourceHandler(executor, this); - clientHandler = new EventSourceChannelHandler(asyncHandler, reconnectionTimeMillis, bootstrap, uri, headers); + clientHandler = new EventSourceChannelHandler(asyncHandler, reconnectionTimeMillis, bootstrap, uri, requestUri, headers); bootstrap.setPipelineFactory(new ChannelPipelineFactory() { public ChannelPipeline getPipeline() throws Exception { ChannelPipeline pipeline = Channels.pipeline(); if (SSLFactory != null) { - SSLEngine sslEngine = SSLFactory.GetNewSSLEngine(); - sslEngine.setUseClientMode(true); + SSLEngine sslEngine = SSLFactory.GetNewSSLEngine(); + sslEngine.setUseClientMode(true); + // add handling of https connection pipeline.addLast("ssl", new SslHandler(sslEngine)); } - pipeline.addLast("line", new DelimiterBasedFrameDecoder(Integer.MAX_VALUE, Delimiters.lineDelimiter())); + // simply decode flow as string pipeline.addLast("string", new StringDecoder()); - + // simple encode request as HTTP request pipeline.addLast("encoder", new HttpRequestEncoder()); + // add our own event source handler pipeline.addLast("es-handler", clientHandler); return pipeline; } }); + connect(); } + public EventSource(URI proxyPrefixUri, URI requestUri, EventSourceHandler eventSourceHandler, Map headers) { + this(URI.create(proxyPrefixUri.toString() + requestUri.toString()), requestUri, null, eventSourceHandler, headers); + } + + public EventSource(URI uri, URI requestUri, SSLEngineFactory sslEngineFactory, EventSourceHandler eventSourceHandler) { + this(Executors.newSingleThreadExecutor(), DEFAULT_RECONNECTION_TIME_MILLIS, uri, requestUri, sslEngineFactory, eventSourceHandler, null); + } + + public EventSource(URI uri, URI requestUri, SSLEngineFactory sslEngineFactory, EventSourceHandler eventSourceHandler, Map headers) { + this(Executors.newSingleThreadExecutor(), DEFAULT_RECONNECTION_TIME_MILLIS, uri, requestUri, sslEngineFactory, eventSourceHandler, headers); + } + + public EventSource(String uri, EventSourceHandler eventSourceHandler) { this(uri, null, eventSourceHandler); } @@ -117,28 +141,22 @@ public EventSource(URI uri, EventSourceHandler eventSourceHandler) { this(uri, null, eventSourceHandler); } - public EventSource(URI uri, EventSourceHandler eventSourceHandler, Map headers) { - this(uri, null, eventSourceHandler, headers); - } - - public EventSource(URI uri, SSLEngineFactory sslEngineFactory, EventSourceHandler eventSourceHandler) { - this(Executors.newSingleThreadExecutor(), DEFAULT_RECONNECTION_TIME_MILLIS, uri, sslEngineFactory, eventSourceHandler, null); + this(Executors.newSingleThreadExecutor(), DEFAULT_RECONNECTION_TIME_MILLIS, uri, null, sslEngineFactory, eventSourceHandler, null); } public EventSource(URI uri, SSLEngineFactory sslEngineFactory, EventSourceHandler eventSourceHandler, Map headers) { - this(Executors.newSingleThreadExecutor(), DEFAULT_RECONNECTION_TIME_MILLIS, uri, sslEngineFactory, eventSourceHandler, headers); + this(Executors.newSingleThreadExecutor(), DEFAULT_RECONNECTION_TIME_MILLIS, uri, null, sslEngineFactory, eventSourceHandler, headers); } - public ChannelFuture connect() { + + private ChannelFuture connect() { readyState = CONNECTING; - - //To avoid perpetual "SocketUnresolvedException" + int port = uri.getPort(); - if (port==-1) - { - port = (uri.getScheme().equals("https"))?443:80; + if (port == -1) { + port = (uri.getScheme().equals("https")) ? 443 : 80; } bootstrap.setOption("remoteAddress", new InetSocketAddress(uri.getHost(), port)); return bootstrap.connect(); @@ -156,17 +174,8 @@ public boolean isConnected() { public EventSource close() { readyState = CLOSED; clientHandler.close(); - return this; - } - - /** - * Wait until the connection is closed - * - * @return self - * @throws InterruptedException if waiting was interrupted - */ - public EventSource join() throws InterruptedException { - clientHandler.join(); + setEventSourceHandler(null); + Log.d(EventSource.class.getName(), "eventSource closed:" + count.getAndIncrement()); return this; } @@ -176,24 +185,51 @@ public void onConnect() throws Exception { readyState = OPEN; // pass event to the proper handler - eventSourceHandler.onConnect(); + if (eventSourceHandler != null) { + eventSourceHandler.onConnect(); + } } @Override public void onMessage(String event, MessageEvent message) throws Exception { // pass event to the proper handler - eventSourceHandler.onMessage(event, message); + if (eventSourceHandler != null) { + eventSourceHandler.onMessage(event, message); + } } @Override public void onError(Throwable t) { // pass event to the proper handler - eventSourceHandler.onError(t); + if (eventSourceHandler != null) { + eventSourceHandler.onError(t); + } } - + @Override public void onClosed(boolean willReconnect) { // pass event to the proper handler - eventSourceHandler.onClosed(willReconnect); + if (eventSourceHandler != null) { + eventSourceHandler.onClosed(willReconnect); + } + } + + public EventSourceHandler getEventSourceHandler() { + return eventSourceHandler; + } + + public void setEventSourceHandler(EventSourceHandler eventSourceHandler) { + this.eventSourceHandler = eventSourceHandler; + } + + @Override + protected void finalize() throws Throwable { + super.finalize(); + + setEventSourceHandler(null); + clientHandler = null; + bootstrap.getFactory().releaseExternalResources(); + bootstrap.releaseExternalResources(); + bootstrap = null; } } diff --git a/eventsource_android/src/main/java/tylerjroach/com/eventsource_android/EventSourceHandler.java b/eventsource_android/src/main/java/tylerjroach/com/eventsource_android/EventSourceHandler.java index 7f95298..3af2105 100644 --- a/eventsource_android/src/main/java/tylerjroach/com/eventsource_android/EventSourceHandler.java +++ b/eventsource_android/src/main/java/tylerjroach/com/eventsource_android/EventSourceHandler.java @@ -2,7 +2,10 @@ public interface EventSourceHandler { void onConnect() throws Exception; + void onMessage(String event, MessageEvent message) throws Exception; + void onError(Throwable t); + void onClosed(boolean willReconnect); } diff --git a/eventsource_android/src/main/java/tylerjroach/com/eventsource_android/MessageEvent.java b/eventsource_android/src/main/java/tylerjroach/com/eventsource_android/MessageEvent.java index 979cf6f..083db4b 100644 --- a/eventsource_android/src/main/java/tylerjroach/com/eventsource_android/MessageEvent.java +++ b/eventsource_android/src/main/java/tylerjroach/com/eventsource_android/MessageEvent.java @@ -23,7 +23,8 @@ public boolean equals(Object o) { MessageEvent that = (MessageEvent) o; if (data != null ? !data.equals(that.data) : that.data != null) return false; - if (lastEventId != null ? !lastEventId.equals(that.lastEventId) : that.lastEventId != null) return false; + if (lastEventId != null ? !lastEventId.equals(that.lastEventId) : that.lastEventId != null) + return false; if (origin != null ? !origin.equals(that.origin) : that.origin != null) return false; return true; diff --git a/eventsource_android/src/main/java/tylerjroach/com/eventsource_android/SSLEngineFactory.java b/eventsource_android/src/main/java/tylerjroach/com/eventsource_android/SSLEngineFactory.java index 0a64333..695a668 100644 --- a/eventsource_android/src/main/java/tylerjroach/com/eventsource_android/SSLEngineFactory.java +++ b/eventsource_android/src/main/java/tylerjroach/com/eventsource_android/SSLEngineFactory.java @@ -7,20 +7,20 @@ import javax.net.ssl.SSLEngine; public class SSLEngineFactory { - SSLEngine GetNewSSLEngine() { - SSLEngine sslEngine = null; - SSLContext sslContext; - try { - sslContext = SSLContext.getInstance("TLS"); - try { - sslContext.init(null, null, null); - sslEngine = sslContext.createSSLEngine(); - } catch (KeyManagementException e) { - return null; - } - } catch (NoSuchAlgorithmException e1) { - return null; - } - return sslEngine; - } + SSLEngine GetNewSSLEngine() { + SSLEngine sslEngine = null; + SSLContext sslContext; + try { + sslContext = SSLContext.getInstance("TLS"); + try { + sslContext.init(null, null, null); + sslEngine = sslContext.createSSLEngine(); + } catch (KeyManagementException e) { + return null; + } + } catch (NoSuchAlgorithmException e1) { + return null; + } + return sslEngine; + } } diff --git a/eventsource_android/src/main/java/tylerjroach/com/eventsource_android/impl/AsyncEventSourceHandler.java b/eventsource_android/src/main/java/tylerjroach/com/eventsource_android/impl/AsyncEventSourceHandler.java index 6b430bd..baef0e1 100644 --- a/eventsource_android/src/main/java/tylerjroach/com/eventsource_android/impl/AsyncEventSourceHandler.java +++ b/eventsource_android/src/main/java/tylerjroach/com/eventsource_android/impl/AsyncEventSourceHandler.java @@ -27,11 +27,10 @@ public void run() { } }); } - + @Override - public void onClosed(final boolean willReconnect) - { - executor.execute(new Runnable() { + public void onClosed(final boolean willReconnect) { + executor.execute(new Runnable() { @Override public void run() { try { diff --git a/eventsource_android/src/main/java/tylerjroach/com/eventsource_android/impl/ConnectionHandler.java b/eventsource_android/src/main/java/tylerjroach/com/eventsource_android/impl/ConnectionHandler.java index e17f4b9..cb27da1 100644 --- a/eventsource_android/src/main/java/tylerjroach/com/eventsource_android/impl/ConnectionHandler.java +++ b/eventsource_android/src/main/java/tylerjroach/com/eventsource_android/impl/ConnectionHandler.java @@ -2,5 +2,6 @@ public interface ConnectionHandler { void setReconnectionTimeMillis(long reconnectionTimeMillis); + void setLastEventId(String lastEventId); } diff --git a/eventsource_android/src/main/java/tylerjroach/com/eventsource_android/impl/netty/EventSourceChannelHandler.java b/eventsource_android/src/main/java/tylerjroach/com/eventsource_android/impl/netty/EventSourceChannelHandler.java index f59d456..0a4d477 100644 --- a/eventsource_android/src/main/java/tylerjroach/com/eventsource_android/impl/netty/EventSourceChannelHandler.java +++ b/eventsource_android/src/main/java/tylerjroach/com/eventsource_android/impl/netty/EventSourceChannelHandler.java @@ -1,5 +1,7 @@ package tylerjroach.com.eventsource_android.impl.netty; +import android.util.Log; + import org.jboss.netty.bootstrap.ClientBootstrap; import org.jboss.netty.channel.Channel; import org.jboss.netty.channel.ChannelEvent; @@ -34,15 +36,14 @@ public class EventSourceChannelHandler extends SimpleChannelUpstreamHandler implements ConnectionHandler { private static final Pattern STATUS_PATTERN = Pattern.compile("HTTP/1.1 (\\d+) (.*)"); - private static final Pattern CONTENT_TYPE_PATTERN = Pattern.compile("Content-Type: text/event-stream"); + private static final Pattern CONTENT_TYPE_PATTERN = Pattern.compile("Content-Type: text/event-stream(.*)"); private final EventSourceHandler eventSourceHandler; private final ClientBootstrap bootstrap; - private final URI uri; private final Map headers; private final EventStreamParser messageDispatcher; - private final Timer timer = new HashedWheelTimer(); + private URI uri, requestUri; private Channel channel; private boolean reconnectOnClose = true; private long reconnectionTimeMillis; @@ -51,6 +52,12 @@ public class EventSourceChannelHandler extends SimpleChannelUpstreamHandler impl private boolean headerDone; private Integer status; private AtomicBoolean reconnecting = new AtomicBoolean(false); + private StringBuffer data = new StringBuffer(); + + public EventSourceChannelHandler(EventSourceHandler eventSourceHandler, long reconnectionTimeMillis, ClientBootstrap bootstrap, URI uri, URI requestUri, Map headers) { + this(eventSourceHandler, reconnectionTimeMillis, bootstrap, uri, headers); + this.requestUri = requestUri; + } public EventSourceChannelHandler(EventSourceHandler eventSourceHandler, long reconnectionTimeMillis, ClientBootstrap bootstrap, URI uri, Map headers) { this.eventSourceHandler = eventSourceHandler; @@ -61,6 +68,16 @@ public EventSourceChannelHandler(EventSourceHandler eventSourceHandler, long rec this.messageDispatcher = new EventStreamParser(uri.toString(), eventSourceHandler, this); } + private static boolean isHexNumber(String cadena) { + try { + Long.parseLong(cadena, 16); + return true; + } catch (NumberFormatException ex) { + // Error handling code... + return false; + } + } + @Override public void handleUpstream(ChannelHandlerContext ctx, ChannelEvent e) throws Exception { super.handleUpstream(ctx, e); @@ -68,7 +85,15 @@ public void handleUpstream(ChannelHandlerContext ctx, ChannelEvent e) throws Exc @Override public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception { - HttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, uri.toString()); + + HttpRequest request; + + if (requestUri != null) { + request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/" + requestUri.toString()); + } else { + request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, uri.toString()); + } + request.addHeader(Names.ACCEPT, "text/event-stream"); if (headers != null) { @@ -76,9 +101,8 @@ public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) thr request.addHeader(entry.getKey(), entry.getValue()); } } - request.addHeader(Names.HOST, uri.getHost()); - request.addHeader(Names.ORIGIN, uri.getScheme()+"://" + uri.getHost()); + request.addHeader(Names.ORIGIN, uri.getScheme() + "://" + uri.getHost()); request.addHeader(Names.CACHE_CONTROL, "no-cache"); if (lastEventId != null) { request.addHeader("Last-Event-ID", lastEventId); @@ -94,7 +118,7 @@ public void channelDisconnected(ChannelHandlerContext ctx, ChannelStateEvent e) @Override public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception { - eventSourceHandler.onClosed(reconnectOnClose); + eventSourceHandler.onClosed(reconnectOnClose); if (reconnectOnClose) { reconnect(); } @@ -102,43 +126,98 @@ public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) throws @Override public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception { - String line = (String) e.getMessage(); - if (status == null) { - Matcher statusMatcher = STATUS_PATTERN.matcher(line); - if (statusMatcher.matches()) { - status = Integer.parseInt(statusMatcher.group(1)); - if (status != 200) { - eventSourceHandler.onError(new EventSourceException("Bad status from " + uri + ": " + status)); - reconnect(); + String messageAsString = (String) e.getMessage(); + + String[] lines = messageAsString.split("\\n",-1); + //int l = 0; + for (String dirtyLine : lines) { + String line = dirtyLine.replace("\r", ""); + //l++; + //Log.d(EventSourceChannelHandler.class.getName(), "line" + l + ": " + line); + if (!headerDone) { + if (status == null) { + // checking Status header + Matcher statusMatcher = STATUS_PATTERN.matcher(line); + if (statusMatcher.matches()) { + status = Integer.parseInt(statusMatcher.group(1)); + if (status != 200) { + eventSourceHandler.onError(new EventSourceException("Bad status from " + uri + ": " + status)); + reconnect(); + break; + } + //Log.d(EventSourceChannelHandler.class.getName(), "--- HTTP CONNECTED"); + } else { + eventSourceHandler.onError(new EventSourceException("Not HTTP? " + uri + ": " + line)); + reconnect(); + break; + } + } + // checking Content-Type header + if (CONTENT_TYPE_PATTERN.matcher(line).matches()) { + eventStreamOk = true; + //Log.d(EventSourceChannelHandler.class.getName(), "--- SSE DETECTED"); + } + // ignoring other headers + if (line.isEmpty()) { + // checking end of header part + headerDone = true; + if (eventStreamOk) { + eventSourceHandler.onConnect(); + } else { + eventSourceHandler.onError(new EventSourceException("Not event stream: " + uri + " (expected Content-Type: text/event-stream")); + reconnect(); + break; + } } - return; } else { - eventSourceHandler.onError(new EventSourceException("Not HTTP? " + uri + ": " + line)); - reconnect(); - } - } - if (!headerDone) { - if (CONTENT_TYPE_PATTERN.matcher(line).matches()) { - eventStreamOk = true; - } - if (line.isEmpty()) { - headerDone = true; - if (eventStreamOk) { - eventSourceHandler.onConnect(); + // data flow: data line or data chunk + if (isChunkStart(line)) { + // ignoring chunk size in case of chunk transfer Encoding + //Log.d(EventSourceChannelHandler.class.getName(), "CHUNK WITH SIZE: " + line); } else { - eventSourceHandler.onError(new EventSourceException("Not event stream: " + uri + " (expected Content-Type: text/event-stream")); - reconnect(); + String[] eventLines = line.split("\\n",-1); + + for (String eventLine : eventLines) { + if (eventLine.startsWith("event:")) { + // dispatching new event + messageDispatcher.line(eventLine); + //Log.d(EventSourceChannelHandler.class.getName(), "SSE EVENT: " + eventLine); + } else if (eventLine.startsWith("id:")) { + // dispatching event id + messageDispatcher.line(eventLine); + //Log.d(EventSourceChannelHandler.class.getName(), "SSE EVENT ID: " + eventLine); + } else if (eventLine.startsWith("data:")) { + // append first line to data : data may be chunked + data.append(eventLine); + } else if (eventLine.isEmpty() && data.length() != 0) { + // end of data : dispatch aggregated data + messageDispatcher.line(data.toString()); + // prepare next event data buffer + //Log.d(EventSourceChannelHandler.class.getName(), "SSE EVENT DATA: " + data.toString()); + // dispatch the end line (empty line) in order to dispatch the event + messageDispatcher.line(eventLine); + data = new StringBuffer(); + } else { + // new data chunk to append + data.append(eventLine); + } + } } } - } else { - messageDispatcher.line(line); } + } + private boolean isChunkStart(String line) { + + return isHexNumber(line); + } + + @Override public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception { Throwable error = e.getCause(); - if(error instanceof ConnectException) { + if (error instanceof ConnectException) { error = new EventSourceException("Failed to connect to " + uri, error); } eventSourceHandler.onError(error); @@ -170,15 +249,20 @@ public EventSourceChannelHandler join() throws InterruptedException { } private void reconnect() { - if(!reconnecting.get()) { + if (!reconnecting.get()) { reconnecting.set(true); + data = new StringBuffer(); + lastEventId = null; + status = null; + eventStreamOk = false; + headerDone = false; timer.newTimeout(new TimerTask() { @Override public void run(Timeout timeout) throws Exception { reconnecting.set(false); int port = uri.getPort(); - if (port==-1) { - port = (uri.getScheme().equals("https"))?443:80; + if (port == -1) { + port = (uri.getScheme().equals("https")) ? 443 : 80; } bootstrap.setOption("remoteAddress", new InetSocketAddress(uri.getHost(), port)); bootstrap.connect().await(); diff --git a/eventsource_android/src/main/java/tylerjroach/com/eventsource_android/stubs/StubHandler.java b/eventsource_android/src/main/java/tylerjroach/com/eventsource_android/stubs/StubHandler.java index a4ed415..41c125c 100644 --- a/eventsource_android/src/main/java/tylerjroach/com/eventsource_android/stubs/StubHandler.java +++ b/eventsource_android/src/main/java/tylerjroach/com/eventsource_android/stubs/StubHandler.java @@ -17,23 +17,13 @@ public class StubHandler implements ConnectionHandler, EventSourceHandler { private Map> messagesByEvent = new HashMap>(); private List errors = new ArrayList(); - @Override - public void setReconnectionTimeMillis(long reconnectionTimeMillis) { - this.reconnectionTimeMillis = reconnectionTimeMillis; - } - - @Override - public void setLastEventId(String lastEventId) { - this.lastEventId = lastEventId; - } - @Override public void onConnect() throws Exception { connected = true; } - + @Override - public void onClosed(boolean willReconnect){ + public void onClosed(boolean willReconnect) { connected = false; } @@ -51,17 +41,27 @@ public Long getReconnectionTimeMillis() { return reconnectionTimeMillis; } + @Override + public void setReconnectionTimeMillis(long reconnectionTimeMillis) { + this.reconnectionTimeMillis = reconnectionTimeMillis; + } + public String getLastEventId() { return lastEventId; } + @Override + public void setLastEventId(String lastEventId) { + this.lastEventId = lastEventId; + } + public boolean isConnected() { return connected; } public List getMessageEvents(String event) { List events = messagesByEvent.get(event); - if(events == null) { + if (events == null) { events = new ArrayList(); messagesByEvent.put(event, events); } diff --git a/eventsource_android/src/main/res/layout/activity_main.xml b/eventsource_android/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..06f6848 --- /dev/null +++ b/eventsource_android/src/main/res/layout/activity_main.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/eventsource_android/src/main/res/menu/menu_main.xml b/eventsource_android/src/main/res/menu/menu_main.xml new file mode 100644 index 0000000..a212461 --- /dev/null +++ b/eventsource_android/src/main/res/menu/menu_main.xml @@ -0,0 +1,6 @@ +

+ + diff --git a/eventsource_android/src/main/res/values-w820dp/dimens.xml b/eventsource_android/src/main/res/values-w820dp/dimens.xml new file mode 100644 index 0000000..63fc816 --- /dev/null +++ b/eventsource_android/src/main/res/values-w820dp/dimens.xml @@ -0,0 +1,6 @@ + + + 64dp + diff --git a/eventsource_android/src/main/res/values/dimens.xml b/eventsource_android/src/main/res/values/dimens.xml new file mode 100644 index 0000000..47c8224 --- /dev/null +++ b/eventsource_android/src/main/res/values/dimens.xml @@ -0,0 +1,5 @@ + + + 16dp + 16dp + diff --git a/eventsource_android/src/main/res/values/strings.xml b/eventsource_android/src/main/res/values/strings.xml new file mode 100644 index 0000000..bddd68f --- /dev/null +++ b/eventsource_android/src/main/res/values/strings.xml @@ -0,0 +1,7 @@ + + MainActivity + + Hello world! + Settings + +