aboutsummaryrefslogtreecommitdiffstats
path: root/AndroidAsync/src/com
diff options
context:
space:
mode:
authorKoushik Dutta <koushd@gmail.com>2012-08-24 09:39:47 -0700
committerKoushik Dutta <koushd@gmail.com>2012-08-24 09:39:47 -0700
commit7b7540379004272830dab781ab442d4870fbb7a2 (patch)
treeb07dd78d2af4276081d734f97348a7e7ab457276 /AndroidAsync/src/com
parent49c71bfea06f9e27d3f1390a14890f871bb44e7f (diff)
downloadAndroidAsync-7b7540379004272830dab781ab442d4870fbb7a2.tar.gz
AndroidAsync-7b7540379004272830dab781ab442d4870fbb7a2.tar.bz2
AndroidAsync-7b7540379004272830dab781ab442d4870fbb7a2.zip
sample
Diffstat (limited to 'AndroidAsync/src/com')
-rw-r--r--AndroidAsync/src/com/koushikdutta/async/AsyncInputStream.java7
-rw-r--r--AndroidAsync/src/com/koushikdutta/async/AsyncServer.java306
-rw-r--r--AndroidAsync/src/com/koushikdutta/async/AsyncSocket.java6
-rw-r--r--AndroidAsync/src/com/koushikdutta/async/AsyncSocketImpl.java210
-rw-r--r--AndroidAsync/src/com/koushikdutta/async/BufferedDataEmitter.java40
-rw-r--r--AndroidAsync/src/com/koushikdutta/async/BufferedDataSink.java56
-rw-r--r--AndroidAsync/src/com/koushikdutta/async/ByteBufferList.java171
-rw-r--r--AndroidAsync/src/com/koushikdutta/async/ChannelWrapper.java43
-rw-r--r--AndroidAsync/src/com/koushikdutta/async/CloseableData.java9
-rw-r--r--AndroidAsync/src/com/koushikdutta/async/DataEmitter.java9
-rw-r--r--AndroidAsync/src/com/koushikdutta/async/DataEmitterStream.java49
-rw-r--r--AndroidAsync/src/com/koushikdutta/async/DataExchange.java5
-rw-r--r--AndroidAsync/src/com/koushikdutta/async/DataSink.java12
-rw-r--r--AndroidAsync/src/com/koushikdutta/async/DataTransformer.java6
-rw-r--r--AndroidAsync/src/com/koushikdutta/async/DataTransformerBase.java37
-rw-r--r--AndroidAsync/src/com/koushikdutta/async/DatagramChannelWrapper.java45
-rw-r--r--AndroidAsync/src/com/koushikdutta/async/ExceptionCallback.java5
-rw-r--r--AndroidAsync/src/com/koushikdutta/async/ExceptionEmitter.java6
-rw-r--r--AndroidAsync/src/com/koushikdutta/async/LineEmitter.java43
-rw-r--r--AndroidAsync/src/com/koushikdutta/async/NullDataCallback.java10
-rw-r--r--AndroidAsync/src/com/koushikdutta/async/PushParser.java217
-rw-r--r--AndroidAsync/src/com/koushikdutta/async/SSLDataExchange.java289
-rw-r--r--AndroidAsync/src/com/koushikdutta/async/ServerSocketChannelWrapper.java52
-rw-r--r--AndroidAsync/src/com/koushikdutta/async/SocketChannelWrapper.java37
-rw-r--r--AndroidAsync/src/com/koushikdutta/async/TapCallback.java21
-rw-r--r--AndroidAsync/src/com/koushikdutta/async/Util.java20
-rw-r--r--AndroidAsync/src/com/koushikdutta/async/callback/ClosedCallback.java5
-rw-r--r--AndroidAsync/src/com/koushikdutta/async/callback/CompletedCallback.java5
-rw-r--r--AndroidAsync/src/com/koushikdutta/async/callback/ConnectCallback.java7
-rw-r--r--AndroidAsync/src/com/koushikdutta/async/callback/DataCallback.java9
-rw-r--r--AndroidAsync/src/com/koushikdutta/async/callback/ListenCallback.java8
-rw-r--r--AndroidAsync/src/com/koushikdutta/async/callback/ResultCallback.java5
-rw-r--r--AndroidAsync/src/com/koushikdutta/async/callback/WritableCallback.java5
-rw-r--r--AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClient.java202
-rw-r--r--AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpGet.java16
-rw-r--r--AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpRequest.java56
-rw-r--r--AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponse.java10
-rw-r--r--AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponseImpl.java205
-rw-r--r--AndroidAsync/src/com/koushikdutta/async/http/callback/HttpConnectCallback.java7
-rw-r--r--AndroidAsync/src/com/koushikdutta/async/http/libcore/HeaderParser.java111
-rw-r--r--AndroidAsync/src/com/koushikdutta/async/http/libcore/HttpDate.java91
-rw-r--r--AndroidAsync/src/com/koushikdutta/async/http/libcore/Memory.java178
-rw-r--r--AndroidAsync/src/com/koushikdutta/async/http/libcore/Objects.java32
-rw-r--r--AndroidAsync/src/com/koushikdutta/async/http/libcore/RawHeaders.java296
-rw-r--r--AndroidAsync/src/com/koushikdutta/async/http/libcore/RequestHeaders.java292
-rw-r--r--AndroidAsync/src/com/koushikdutta/async/http/libcore/ResponseHeaders.java518
-rw-r--r--AndroidAsync/src/com/koushikdutta/async/http/libcore/ResponseSource.java40
-rw-r--r--AndroidAsync/src/com/koushikdutta/async/http/transform/ChunkedTransformer.java118
-rw-r--r--AndroidAsync/src/com/koushikdutta/async/http/transform/GZIPTransformer.java133
-rw-r--r--AndroidAsync/src/com/koushikdutta/async/http/transform/InflaterTransformer.java62
-rw-r--r--AndroidAsync/src/com/koushikdutta/async/stream/OutputStreamDataCallback.java44
-rw-r--r--AndroidAsync/src/com/koushikdutta/test/TestActivity.java315
52 files changed, 4481 insertions, 0 deletions
diff --git a/AndroidAsync/src/com/koushikdutta/async/AsyncInputStream.java b/AndroidAsync/src/com/koushikdutta/async/AsyncInputStream.java
new file mode 100644
index 0000000..20bb5ad
--- /dev/null
+++ b/AndroidAsync/src/com/koushikdutta/async/AsyncInputStream.java
@@ -0,0 +1,7 @@
+package com.koushikdutta.async;
+
+import com.koushikdutta.async.callback.DataCallback;
+
+public interface AsyncInputStream {
+ public void read(int count, DataCallback callback);
+}
diff --git a/AndroidAsync/src/com/koushikdutta/async/AsyncServer.java b/AndroidAsync/src/com/koushikdutta/async/AsyncServer.java
new file mode 100644
index 0000000..18883cd
--- /dev/null
+++ b/AndroidAsync/src/com/koushikdutta/async/AsyncServer.java
@@ -0,0 +1,306 @@
+package com.koushikdutta.async;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.nio.channels.ClosedChannelException;
+import java.nio.channels.DatagramChannel;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.Selector;
+import java.nio.channels.ServerSocketChannel;
+import java.nio.channels.SocketChannel;
+import java.nio.channels.spi.SelectorProvider;
+import java.util.LinkedList;
+import java.util.Set;
+import java.util.concurrent.Semaphore;
+
+import com.koushikdutta.async.callback.ConnectCallback;
+import com.koushikdutta.async.callback.ListenCallback;
+
+import junit.framework.Assert;
+import android.util.Log;
+
+public class AsyncServer {
+ private static final String LOGTAG = "NIO";
+
+ static AsyncServer mInstance;
+ public static AsyncServer getDefault() {
+ if (mInstance == null)
+ mInstance = new AsyncServer();
+
+ if (!mInstance.mRun) {
+ try {
+ mInstance.initialize();
+ }
+ catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ new Thread() {
+ @Override
+ public void run() {
+ mInstance.run();
+ }
+ }.start();
+ }
+
+ return mInstance;
+ }
+
+ Selector mSelector;
+
+ public AsyncServer() {
+ }
+
+ private void handleSocket(final AsyncSocketImpl handler) throws ClosedChannelException {
+ final ChannelWrapper sc = handler.getChannel();
+ SelectionKey ckey = sc.register(mSelector);
+ ckey.attach(handler);
+ handler.mKey = ckey;
+ }
+
+ public void post(Runnable runnable) {
+ synchronized (mQueue) {
+ mQueue.add(runnable);
+ }
+ if (Thread.currentThread() == mAffinity) {
+ runQueue();
+ }
+ else {
+ mSelector.wakeup();
+ }
+ }
+
+ public void run(final Runnable runnable) {
+ final Semaphore semaphore = new Semaphore(0);
+ post(new Runnable() {
+ @Override
+ public void run() {
+ runnable.run();
+ semaphore.release();
+ }
+ });
+ try {
+ semaphore.acquire();
+ }
+ catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void runQueue() {
+ Assert.assertEquals(Thread.currentThread(), mAffinity);
+ synchronized (mQueue) {
+ while (mQueue.size() > 0) {
+ Runnable run = mQueue.remove();
+ run.run();
+ }
+ }
+ }
+
+ boolean mRun = false;
+ boolean mShuttingDown = false;
+ LinkedList<Runnable> mQueue = new LinkedList<Runnable>();
+
+ public void stop() {
+ mRun = false;
+ mShuttingDown = true;
+ mSelector.wakeup();
+ }
+
+ protected void onDataTransmitted(int transmitted) {
+ }
+
+ public void listen(InetAddress host, int port, final ListenCallback handler) throws IOException {
+ ServerSocketChannel server = ServerSocketChannel.open();
+ final ServerSocketChannelWrapper wrapper = new ServerSocketChannelWrapper(server);
+ InetSocketAddress isa = new InetSocketAddress(host, port);
+ server.socket().bind(isa);
+ run(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ SelectionKey key = wrapper.register(mSelector);
+ key.attach(handler);
+ }
+ catch (ClosedChannelException e) {
+ e.printStackTrace();
+ }
+ }
+ });
+ }
+
+ public void connectSocket(final SocketAddress remote, final ConnectCallback handler) {
+ try {
+ final SocketChannel socket = SocketChannel.open();
+ final ChannelWrapper sc = new SocketChannelWrapper(socket);
+ post(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ SelectionKey ckey = sc.register(mSelector);
+ ckey.attach(handler);
+ socket.connect(remote);
+ }
+ catch (Exception e) {
+ handler.onConnectCompleted(e, null);
+ }
+ }
+ });
+ }
+ catch (Exception e) {
+ handler.onConnectCompleted(e, null);
+ }
+ }
+
+ public void connectSocket(final String host, final int port, final ConnectCallback handler) {
+ try {
+ final SocketChannel socket = SocketChannel.open();
+ final ChannelWrapper sc = new SocketChannelWrapper(socket);
+ post(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ SocketAddress remote = new InetSocketAddress(host, port);
+ socket.connect(remote);
+ SelectionKey ckey = sc.register(mSelector);
+ ckey.attach(handler);
+ }
+ catch (Exception e) {
+ handler.onConnectCompleted(e, null);
+ }
+ }
+ });
+ }
+ catch (Exception e) {
+ handler.onConnectCompleted(e, null);
+ }
+ }
+
+ public AsyncSocket connectDatagram(final SocketAddress remote) throws IOException {
+ final DatagramChannel socket = DatagramChannel.open();
+ final AsyncSocketImpl handler = new AsyncSocketImpl();
+ handler.attach(socket);
+ // ugh.. this should really be post to make it nonblocking...
+ // but i want datagrams to be immediately writreable.
+ // they're not really used anyways.
+ run(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ handleSocket(handler);
+ socket.connect(remote);
+ }
+ catch (Exception e) {
+ }
+ }
+ });
+ return handler;
+ }
+
+ public void initialize() throws IOException {
+ synchronized (this) {
+ if (mSelector == null)
+ mSelector = SelectorProvider.provider().openSelector();
+ }
+ }
+
+ Thread mAffinity;
+ public void run() {
+ synchronized (this) {
+ if (mRun) {
+ Log.i(LOGTAG, "Already running.");
+ return;
+ }
+ if (mShuttingDown) {
+ Log.i(LOGTAG, "Shutdown in progress.");
+ return;
+ }
+ mRun = true;
+ mAffinity = Thread.currentThread();
+ }
+
+ while (mRun) {
+ try {
+ runLoop();
+ }
+ catch (Exception e) {
+ Log.i(LOGTAG, "exception?");
+ e.printStackTrace();
+ }
+ }
+
+ // SHUT. DOWN. EVERYTHING.
+ for (SelectionKey key : mSelector.keys()) {
+ try {
+ key.channel().close();
+ }
+ catch (IOException e) {
+ }
+ }
+
+ try {
+ mSelector.close();
+ }
+ catch (IOException e) {
+ }
+ mSelector = null;
+ mShuttingDown = false;
+ mAffinity = null;
+ }
+
+ private void runLoop() throws IOException {
+ mSelector.select();
+ runQueue();
+ Set<SelectionKey> readyKeys = mSelector.selectedKeys();
+ for (SelectionKey key : readyKeys) {
+ if (key.isAcceptable()) {
+ ServerSocketChannel nextReady = (ServerSocketChannel) key.channel();
+ SocketChannel sc = nextReady.accept();
+ if (sc == null)
+ continue;
+ sc.configureBlocking(false);
+ SelectionKey ckey = sc.register(mSelector, SelectionKey.OP_READ);
+ ListenCallback serverHandler = (ListenCallback) key.attachment();
+ AsyncSocketImpl handler = new AsyncSocketImpl();
+ handler.attach(sc);
+ handler.mKey = ckey;
+ ckey.attach(handler);
+ serverHandler.onAccepted(handler);
+ }
+ else if (key.isReadable()) {
+ AsyncSocketImpl handler = (AsyncSocketImpl) key.attachment();
+ int transmitted = handler.onReadable();
+ onDataTransmitted(transmitted);
+ }
+ else if (key.isWritable()) {
+ AsyncSocketImpl handler = (AsyncSocketImpl) key.attachment();
+ handler.onDataWritable();
+ }
+ else if (key.isConnectable()) {
+ ConnectCallback handler = (ConnectCallback) key.attachment();
+ SocketChannel sc = (SocketChannel) key.channel();
+ key.interestOps(SelectionKey.OP_READ);
+ try {
+ sc.finishConnect();
+ AsyncSocketImpl newHandler = new AsyncSocketImpl();
+ newHandler.mKey = key;
+ newHandler.attach(sc);
+ key.attach(newHandler);
+ handler.onConnectCompleted(null, newHandler);
+ }
+ catch (Exception ex) {
+ key.cancel();
+ sc.close();
+ handler.onConnectCompleted(ex, null);
+ }
+ }
+ else {
+ Log.i(LOGTAG, "wtf");
+ Assert.fail();
+ }
+ }
+ readyKeys.clear();
+ }
+}
diff --git a/AndroidAsync/src/com/koushikdutta/async/AsyncSocket.java b/AndroidAsync/src/com/koushikdutta/async/AsyncSocket.java
new file mode 100644
index 0000000..82d4b08
--- /dev/null
+++ b/AndroidAsync/src/com/koushikdutta/async/AsyncSocket.java
@@ -0,0 +1,6 @@
+package com.koushikdutta.async;
+
+
+public interface AsyncSocket extends DataExchange, CloseableData, ExceptionEmitter {
+ public boolean isConnected();
+}
diff --git a/AndroidAsync/src/com/koushikdutta/async/AsyncSocketImpl.java b/AndroidAsync/src/com/koushikdutta/async/AsyncSocketImpl.java
new file mode 100644
index 0000000..83e372c
--- /dev/null
+++ b/AndroidAsync/src/com/koushikdutta/async/AsyncSocketImpl.java
@@ -0,0 +1,210 @@
+package com.koushikdutta.async;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.DatagramChannel;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.SocketChannel;
+
+import junit.framework.Assert;
+
+import com.koushikdutta.async.callback.ClosedCallback;
+import com.koushikdutta.async.callback.DataCallback;
+import com.koushikdutta.async.callback.WritableCallback;
+
+class AsyncSocketImpl implements AsyncSocket {
+ AsyncSocketImpl() {
+ }
+
+ public boolean isChunked() {
+ return mChannel.isChunked();
+ }
+
+ void attach(SocketChannel channel) throws IOException {
+ mChannel = new SocketChannelWrapper(channel);
+ }
+
+ void attach(DatagramChannel channel) throws IOException {
+ mChannel = new DatagramChannelWrapper(channel);
+ }
+
+ ChannelWrapper getChannel() {
+ return mChannel;
+ }
+
+ public void onDataWritable() {
+ Assert.assertNotNull(mWriteableHandler);
+ mWriteableHandler.onWriteable();
+ }
+
+ private ChannelWrapper mChannel;
+ SelectionKey mKey;
+
+ @Override
+ public void write(ByteBufferList list) {
+ if (!mChannel.isConnected()) {
+ Assert.assertFalse(mChannel.isChunked());
+ return;
+ }
+
+ try {
+ mChannel.write(list.toArray());
+ handleRemaining(list.remaining());
+ }
+ catch (IOException e) {
+ }
+ }
+
+ private void handleRemaining(int remaining) {
+ if (remaining > 0) {
+ // chunked channels should not fail
+ Assert.assertFalse(mChannel.isChunked());
+ // register for a write notification if a write fails
+ mKey.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
+ }
+ else {
+ mKey.interestOps(SelectionKey.OP_READ);
+ }
+ }
+
+ @Override
+ public void write(ByteBuffer b) {
+ try {
+ if (!mChannel.isConnected()) {
+ Assert.assertFalse(mChannel.isChunked());
+ return;
+ }
+
+ // keep writing until the the socket can't write any more, or the
+ // data is exhausted.
+ mChannel.write(b);
+ handleRemaining(b.remaining());
+ }
+ catch (IOException ex) {
+ ex.printStackTrace();
+ }
+ }
+
+ int onReadable() {
+ int total = 0;
+ try {
+ boolean closed = false;
+ ByteBufferList list = new ByteBufferList();
+ while (true) {
+ ByteBuffer b = ByteBuffer.allocate(2 << 10);
+ list.add(b);
+ int read = mChannel.read(b);
+ b.limit(b.position());
+ b.position(0);
+ if (read < 0) {
+ close();
+ closed = true;
+ }
+ else {
+ total += read;
+ }
+ if (read <= 0)
+ break;
+ if (mChannel.isChunked()) {
+ Assert.assertNotNull(mDataHandler);
+ mDataHandler.onDataAvailable(this, list);
+ list = new ByteBufferList();
+ }
+ }
+ if (!mChannel.isChunked()) {
+// Util.emitAllData(this, list);
+ int remaining;
+ while (mDataHandler != null && (remaining = list.remaining()) > 0) {
+ DataCallback handler = mDataHandler;
+ mDataHandler.onDataAvailable(this, list);
+ if (remaining == list.remaining() && handler == mDataHandler) {
+ Assert.fail("mDataHandler failed to consume data, yet remains the mDataHandler.");
+ break;
+ }
+ }
+ Assert.assertEquals(list.remaining(), 0);
+ }
+
+ if (closed)
+ reportClose();
+ }
+ catch (Exception e) {
+ close();
+ report(e);
+ reportClose();
+ }
+
+ return total;
+ }
+
+ private void reportClose() {
+ if (mClosedHander != null)
+ mClosedHander.onClosed();
+ }
+
+ @Override
+ public void close() {
+ mKey.cancel();
+ try {
+ mChannel.close();
+ }
+ catch (IOException e) {
+ }
+ }
+
+ WritableCallback mWriteableHandler;
+ @Override
+ public void setWriteableCallback(WritableCallback handler) {
+ mWriteableHandler = handler;
+ }
+
+ DataCallback mDataHandler;
+ @Override
+ public void setDataCallback(DataCallback callback) {
+ mDataHandler = callback;
+ }
+
+ @Override
+ public DataCallback getDataCallback() {
+ return mDataHandler;
+ }
+
+ ClosedCallback mClosedHander;
+ @Override
+ public void setClosedCallback(ClosedCallback handler) {
+ mClosedHander = handler;
+ }
+
+ @Override
+ public ClosedCallback getCloseHandler() {
+ return mClosedHander;
+ }
+
+ @Override
+ public WritableCallback getWriteableCallback() {
+ return mWriteableHandler;
+ }
+
+ void report(Exception e) {
+ if (mExceptionCallback != null)
+ mExceptionCallback.onException(e);
+ else
+ e.printStackTrace();
+ }
+
+ private ExceptionCallback mExceptionCallback;
+ @Override
+ public void setExceptionCallback(ExceptionCallback callback) {
+ mExceptionCallback = callback;
+ }
+
+ @Override
+ public ExceptionCallback getExceptionCallback() {
+ return mExceptionCallback;
+ }
+
+ @Override
+ public boolean isConnected() {
+ return mChannel.isConnected();
+ }
+}
diff --git a/AndroidAsync/src/com/koushikdutta/async/BufferedDataEmitter.java b/AndroidAsync/src/com/koushikdutta/async/BufferedDataEmitter.java
new file mode 100644
index 0000000..50e1289
--- /dev/null
+++ b/AndroidAsync/src/com/koushikdutta/async/BufferedDataEmitter.java
@@ -0,0 +1,40 @@
+package com.koushikdutta.async;
+
+import com.koushikdutta.async.callback.DataCallback;
+
+public class BufferedDataEmitter implements DataEmitter, DataCallback {
+ public BufferedDataEmitter() {
+ }
+
+ public void onDataAvailable() {
+ if (mDataCallback != null)
+ mDataCallback.onDataAvailable(this, mBuffers);
+ }
+
+ ByteBufferList mBuffers = new ByteBufferList();
+
+ DataCallback mDataCallback;
+ @Override
+ public void setDataCallback(DataCallback callback) {
+ mDataCallback = callback;
+ }
+
+ @Override
+ public DataCallback getDataCallback() {
+ return mDataCallback;
+ }
+
+ @Override
+ public boolean isChunked() {
+ return false;
+ }
+
+ @Override
+ public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) {
+ mBuffers.add(bb);
+ bb.clear();
+
+ onDataAvailable();
+ }
+
+}
diff --git a/AndroidAsync/src/com/koushikdutta/async/BufferedDataSink.java b/AndroidAsync/src/com/koushikdutta/async/BufferedDataSink.java
new file mode 100644
index 0000000..4b176b7
--- /dev/null
+++ b/AndroidAsync/src/com/koushikdutta/async/BufferedDataSink.java
@@ -0,0 +1,56 @@
+package com.koushikdutta.async;
+
+import java.nio.ByteBuffer;
+
+import junit.framework.Assert;
+
+import com.koushikdutta.async.callback.WritableCallback;
+
+public class BufferedDataSink implements DataSink {
+ DataSink mDataSink;
+ public BufferedDataSink(DataSink datasink) {
+ mDataSink = datasink;
+ mDataSink.setWriteableCallback(new WritableCallback() {
+ @Override
+ public void onWriteable() {
+ writePending();
+ }
+ });
+ }
+
+ public DataSink getDataSink() {
+ return mDataSink;
+ }
+
+ private void writePending() {
+ mDataSink.write(mPendingWrites);
+ }
+
+ ByteBufferList mPendingWrites = new ByteBufferList();
+
+ @Override
+ public void write(ByteBuffer bb) {
+ mPendingWrites.add(ByteBuffer.wrap(bb.array(), bb.arrayOffset() + bb.position(), bb.remaining()));
+ bb.position(0);
+ bb.limit(0);
+ writePending();
+ }
+
+ @Override
+ public void write(ByteBufferList bb) {
+ mPendingWrites.add(bb);
+ bb.clear();
+ writePending();
+ }
+
+ @Override
+ public void setWriteableCallback(WritableCallback handler) {
+ Assert.fail("BufferingDataSink is always writeable.");
+ }
+
+ @Override
+ public WritableCallback getWriteableCallback() {
+ Assert.fail("BufferingDataSink is always writeable.");
+ return null;
+ }
+}
diff --git a/AndroidAsync/src/com/koushikdutta/async/ByteBufferList.java b/AndroidAsync/src/com/koushikdutta/async/ByteBufferList.java
new file mode 100644
index 0000000..6995b61
--- /dev/null
+++ b/AndroidAsync/src/com/koushikdutta/async/ByteBufferList.java
@@ -0,0 +1,171 @@
+package com.koushikdutta.async;
+
+import java.nio.ByteBuffer;
+import java.util.Iterator;
+import java.util.LinkedList;
+
+import junit.framework.Assert;
+
+public class ByteBufferList implements Iterable<ByteBuffer> {
+ LinkedList<ByteBuffer> mBuffers = new LinkedList<ByteBuffer>();
+
+ public ByteBuffer peek() {
+ return mBuffers.peek();
+ }
+
+ public ByteBufferList() {
+ }
+
+ public ByteBuffer[] toArray() {
+ ByteBuffer[] ret = new ByteBuffer[mBuffers.size()];
+ ret = mBuffers.toArray(ret);
+ return ret;
+ }
+
+ public int remaining() {
+ int ret = 0;
+ for (ByteBuffer bb: mBuffers) {
+ ret += bb.remaining();
+ }
+ return ret;
+ }
+
+ public int getInt() {
+ return read(4).getInt();
+ }
+
+ public char getByteChar() {
+ return (char)read(1).get();
+ }
+
+ public int getShort() {
+ return read(2).getShort();
+ }
+
+ public byte get() {
+ return read(1).get();
+ }
+
+ public long getLong() {
+ return read(8).getLong();
+ }
+
+ public void get(byte[] bytes) {
+ read(bytes.length).get(bytes);
+ }
+
+ public ByteBufferList get(int length) {
+ Assert.assertTrue(remaining() >= length);
+ ByteBufferList ret = new ByteBufferList();
+ int offset = 0;
+ for (ByteBuffer b: mBuffers) {
+ int remaining = b.remaining();
+
+ if (remaining == 0)
+ continue;
+ // done
+ if (offset > length)
+ break;
+
+ if (offset + remaining > length) {
+ int need = length - offset;
+ // this is shared between both
+ ret.add(ByteBuffer.wrap(b.array(), b.arrayOffset() + b.position(), need));
+ b.position(b.position() + need);
+ }
+ else {
+ // this belongs to the new list
+ ret.add(ByteBuffer.wrap(b.array(), b.arrayOffset() + b.position(), remaining));
+ b.position(b.limit());
+ }
+
+ offset += remaining;
+ }
+
+ return ret;
+ }
+
+ public ByteBuffer read(int count) {
+ Assert.assertTrue(count <= remaining());
+
+ ByteBuffer first = mBuffers.peek();
+ while (first.position() == first.limit()) {
+ mBuffers.remove();
+ first = mBuffers.peek();
+ }
+
+ if (first.remaining() >= count) {
+ return first;
+ }
+ else {
+ // reallocate the count into a single buffer, and return it
+ byte[] bytes = new byte[count];
+ int offset = 0;
+ ByteBuffer bb = null;
+ while (offset < count) {
+ bb = mBuffers.remove();
+ int toRead = Math.min(count - offset, bb.remaining());
+ bb.get(bytes, offset, toRead);
+ offset += toRead;
+ }
+ Assert.assertNotNull(bb);
+ // if there was still data left in the last buffer we popped
+ // toss it back into the head
+ if (bb.position() < bb.limit())
+ mBuffers.add(0, bb);
+ ByteBuffer ret = ByteBuffer.wrap(bytes);
+ mBuffers.add(0, ret);
+ return ret;
+ }
+ }
+
+ public void trim() {
+ // this clears out buffers that are empty in the beginning of the list
+ if (size() > 0)
+ read(0);
+ }
+
+ public void add(ByteBuffer b) {
+ if (b.remaining() <= 0)
+ return;
+ mBuffers.add(b);
+ trim();
+ }
+
+ public void add(ByteBufferList b) {
+ if (b.remaining() <= 0)
+ return;
+ mBuffers.addAll(b.mBuffers);
+ trim();
+ }
+
+ public void clear() {
+ mBuffers.clear();
+ }
+
+ public ByteBuffer remove() {
+ return mBuffers.remove();
+ }
+
+ public int size() {
+ return mBuffers.size();
+ }
+
+ @Override
+ public Iterator<ByteBuffer> iterator() {
+ return mBuffers.iterator();
+ }
+
+ public void spewString() {
+ for (ByteBuffer bb: mBuffers) {
+ try {
+ String s = new String(bb.array(), bb.arrayOffset() + bb.position(), bb.limit());
+ System.out.println(s);
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+
+ }
+ }
+ }
+}
diff --git a/AndroidAsync/src/com/koushikdutta/async/ChannelWrapper.java b/AndroidAsync/src/com/koushikdutta/async/ChannelWrapper.java
new file mode 100644
index 0000000..68b9ed2
--- /dev/null
+++ b/AndroidAsync/src/com/koushikdutta/async/ChannelWrapper.java
@@ -0,0 +1,43 @@
+package com.koushikdutta.async;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.ClosedChannelException;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.Selector;
+import java.nio.channels.spi.AbstractSelectableChannel;
+
+abstract class ChannelWrapper implements ReadableByteChannel {
+ private AbstractSelectableChannel mChannel;
+ ChannelWrapper(AbstractSelectableChannel channel) throws IOException {
+ channel.configureBlocking(false);
+ mChannel = channel;
+ }
+
+ public abstract boolean isConnected();
+
+ public abstract int write(ByteBuffer src) throws IOException;
+ public abstract int write(ByteBuffer[] src) throws IOException;
+
+ // register for default events appropriate for this channel
+ public abstract SelectionKey register(Selector sel) throws ClosedChannelException;
+
+ public SelectionKey register(Selector sel, int ops) throws ClosedChannelException {
+ return mChannel.register(sel, ops);
+ }
+
+ public boolean isChunked() {
+ return false;
+ }
+
+ @Override
+ public boolean isOpen() {
+ return mChannel.isOpen();
+ }
+
+ @Override
+ public void close() throws IOException {
+ mChannel.close();
+ }
+}
diff --git a/AndroidAsync/src/com/koushikdutta/async/CloseableData.java b/AndroidAsync/src/com/koushikdutta/async/CloseableData.java
new file mode 100644
index 0000000..56563c5
--- /dev/null
+++ b/AndroidAsync/src/com/koushikdutta/async/CloseableData.java
@@ -0,0 +1,9 @@
+package com.koushikdutta.async;
+
+import com.koushikdutta.async.callback.ClosedCallback;
+
+public interface CloseableData {
+ public void close();
+ public void setClosedCallback(ClosedCallback handler);
+ public ClosedCallback getCloseHandler();
+}
diff --git a/AndroidAsync/src/com/koushikdutta/async/DataEmitter.java b/AndroidAsync/src/com/koushikdutta/async/DataEmitter.java
new file mode 100644
index 0000000..c868be6
--- /dev/null
+++ b/AndroidAsync/src/com/koushikdutta/async/DataEmitter.java
@@ -0,0 +1,9 @@
+package com.koushikdutta.async;
+
+import com.koushikdutta.async.callback.DataCallback;
+
+public interface DataEmitter {
+ public void setDataCallback(DataCallback callback);
+ public DataCallback getDataCallback();
+ public boolean isChunked();
+}
diff --git a/AndroidAsync/src/com/koushikdutta/async/DataEmitterStream.java b/AndroidAsync/src/com/koushikdutta/async/DataEmitterStream.java
new file mode 100644
index 0000000..37bc496
--- /dev/null
+++ b/AndroidAsync/src/com/koushikdutta/async/DataEmitterStream.java
@@ -0,0 +1,49 @@
+package com.koushikdutta.async;
+
+import junit.framework.Assert;
+
+import com.koushikdutta.async.callback.DataCallback;
+
+public class DataEmitterStream implements AsyncInputStream {
+ DataCallback mPendingRead;
+ int mPendingReadLength;
+ ByteBufferList mPendingData = new ByteBufferList();
+ @Override
+ public void read(int count, DataCallback callback) {
+ Assert.assertNull(mPendingRead);
+ mPendingReadLength = count;
+ mPendingRead = callback;
+ mPendingData = new ByteBufferList();
+ mEmitter.setDataCallback(mMyHandler);
+ }
+
+ DataCallback mMyHandler = new DataCallback() {
+ @Override
+ public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) {
+ // if we're registered for data, we must be waiting for a read
+ Assert.assertNotNull(mPendingRead);
+ do {
+ int need = Math.min(bb.remaining(), mPendingReadLength - mPendingData.remaining());
+ mPendingData.add(bb.get(need));
+ }
+ while (handlePendingData() && mPendingRead != null);
+ }
+ };
+
+ private boolean handlePendingData() {
+ if (mPendingReadLength > mPendingData.remaining())
+ return false;
+
+ DataCallback pendingRead = mPendingRead;
+ mPendingRead = null;
+ pendingRead.onDataAvailable(mEmitter, mPendingData);
+
+ return true;
+ }
+
+ DataEmitter mEmitter;
+ public DataEmitterStream(DataEmitter emitter) {
+ Assert.assertFalse(emitter.isChunked());
+ mEmitter = emitter;
+ }
+}
diff --git a/AndroidAsync/src/com/koushikdutta/async/DataExchange.java b/AndroidAsync/src/com/koushikdutta/async/DataExchange.java
new file mode 100644
index 0000000..3de062d
--- /dev/null
+++ b/AndroidAsync/src/com/koushikdutta/async/DataExchange.java
@@ -0,0 +1,5 @@
+package com.koushikdutta.async;
+
+public interface DataExchange extends DataEmitter, DataSink {
+
+}
diff --git a/AndroidAsync/src/com/koushikdutta/async/DataSink.java b/AndroidAsync/src/com/koushikdutta/async/DataSink.java
new file mode 100644
index 0000000..fbd2ca6
--- /dev/null
+++ b/AndroidAsync/src/com/koushikdutta/async/DataSink.java
@@ -0,0 +1,12 @@
+package com.koushikdutta.async;
+
+import java.nio.ByteBuffer;
+
+import com.koushikdutta.async.callback.WritableCallback;
+
+public interface DataSink {
+ public void write(ByteBuffer bb);
+ public void write(ByteBufferList bb);
+ public void setWriteableCallback(WritableCallback handler);
+ public WritableCallback getWriteableCallback();
+}
diff --git a/AndroidAsync/src/com/koushikdutta/async/DataTransformer.java b/AndroidAsync/src/com/koushikdutta/async/DataTransformer.java
new file mode 100644
index 0000000..d33e822
--- /dev/null
+++ b/AndroidAsync/src/com/koushikdutta/async/DataTransformer.java
@@ -0,0 +1,6 @@
+package com.koushikdutta.async;
+
+import com.koushikdutta.async.callback.DataCallback;
+
+public interface DataTransformer extends DataEmitter, DataCallback, ExceptionCallback {
+}
diff --git a/AndroidAsync/src/com/koushikdutta/async/DataTransformerBase.java b/AndroidAsync/src/com/koushikdutta/async/DataTransformerBase.java
new file mode 100644
index 0000000..0469ba4
--- /dev/null
+++ b/AndroidAsync/src/com/koushikdutta/async/DataTransformerBase.java
@@ -0,0 +1,37 @@
+package com.koushikdutta.async;
+
+import junit.framework.Assert;
+
+import com.koushikdutta.async.callback.DataCallback;
+
+public class DataTransformerBase implements DataTransformer {
+ public DataTransformerBase() {
+ }
+
+ private DataCallback mDataCallback;
+ @Override
+ public void setDataCallback(DataCallback callback) {
+ mDataCallback = callback;
+ }
+
+ @Override
+ public DataCallback getDataCallback() {
+ return mDataCallback;
+ }
+
+ @Override
+ public boolean isChunked() {
+ return false;
+ }
+
+ @Override
+ public void onException(Exception error) {
+ error.printStackTrace();
+ }
+
+ @Override
+ public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) {
+ Assert.assertNotNull(mDataCallback);
+ Util.emitAllData(this, bb);
+ }
+}
diff --git a/AndroidAsync/src/com/koushikdutta/async/DatagramChannelWrapper.java b/AndroidAsync/src/com/koushikdutta/async/DatagramChannelWrapper.java
new file mode 100644
index 0000000..4bc5f6b
--- /dev/null
+++ b/AndroidAsync/src/com/koushikdutta/async/DatagramChannelWrapper.java
@@ -0,0 +1,45 @@
+package com.koushikdutta.async;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.ClosedChannelException;
+import java.nio.channels.DatagramChannel;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.Selector;
+
+class DatagramChannelWrapper extends ChannelWrapper {
+ DatagramChannel mChannel;
+
+ DatagramChannelWrapper(DatagramChannel channel) throws IOException {
+ super(channel);
+ mChannel = channel;
+ }
+ @Override
+ public int read(ByteBuffer buffer) throws IOException {
+ return mChannel.read(buffer);
+ }
+ @Override
+ public boolean isConnected() {
+ return mChannel.isConnected();
+ }
+ @Override
+ public int write(ByteBuffer src) throws IOException {
+ return mChannel.write(src);
+ }
+ @Override
+ public int write(ByteBuffer[] src) throws IOException {
+ return (int)mChannel.write(src);
+ }
+ @Override
+ public SelectionKey register(Selector sel, int ops) throws ClosedChannelException {
+ return mChannel.register(sel, ops);
+ }
+ @Override
+ public boolean isChunked() {
+ return true;
+ }
+ @Override
+ public SelectionKey register(Selector sel) throws ClosedChannelException {
+ return register(sel, SelectionKey.OP_READ);
+ }
+}
diff --git a/AndroidAsync/src/com/koushikdutta/async/ExceptionCallback.java b/AndroidAsync/src/com/koushikdutta/async/ExceptionCallback.java
new file mode 100644
index 0000000..f33dcac
--- /dev/null
+++ b/AndroidAsync/src/com/koushikdutta/async/ExceptionCallback.java
@@ -0,0 +1,5 @@
+package com.koushikdutta.async;
+
+public interface ExceptionCallback {
+ public void onException(Exception error);
+}
diff --git a/AndroidAsync/src/com/koushikdutta/async/ExceptionEmitter.java b/AndroidAsync/src/com/koushikdutta/async/ExceptionEmitter.java
new file mode 100644
index 0000000..21d5e2f
--- /dev/null
+++ b/AndroidAsync/src/com/koushikdutta/async/ExceptionEmitter.java
@@ -0,0 +1,6 @@
+package com.koushikdutta.async;
+
+public interface ExceptionEmitter {
+ public void setExceptionCallback(ExceptionCallback callback);
+ public ExceptionCallback getExceptionCallback();
+}
diff --git a/AndroidAsync/src/com/koushikdutta/async/LineEmitter.java b/AndroidAsync/src/com/koushikdutta/async/LineEmitter.java
new file mode 100644
index 0000000..9694d5b
--- /dev/null
+++ b/AndroidAsync/src/com/koushikdutta/async/LineEmitter.java
@@ -0,0 +1,43 @@
+package com.koushikdutta.async;
+
+import junit.framework.Assert;
+
+import com.koushikdutta.async.callback.DataCallback;
+
+public class LineEmitter {
+ static public interface StringCallback {
+ public void onStringAvailable(String s);
+ }
+
+ StringBuilder data = new StringBuilder();
+
+ public LineEmitter(final DataEmitter emitter) {
+ emitter.setDataCallback(new DataCallback() {
+ @Override
+ public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) {
+ while (bb.remaining() > 0) {
+ byte b = bb.get();
+ if (b == '\n') {
+ Assert.assertNotNull(mLineCallback);
+ mLineCallback.onStringAvailable(data.toString());
+ if (emitter.getDataCallback() != this)
+ return;
+ data = new StringBuilder();
+ }
+ else {
+ data.append((char)b);
+ }
+ }
+ }
+ });
+ }
+
+ StringCallback mLineCallback;
+ public void setLineCallback(StringCallback callback) {
+ mLineCallback = callback;
+ }
+
+ public StringCallback getLineCallback() {
+ return mLineCallback;
+ }
+}
diff --git a/AndroidAsync/src/com/koushikdutta/async/NullDataCallback.java b/AndroidAsync/src/com/koushikdutta/async/NullDataCallback.java
new file mode 100644
index 0000000..3911286
--- /dev/null
+++ b/AndroidAsync/src/com/koushikdutta/async/NullDataCallback.java
@@ -0,0 +1,10 @@
+package com.koushikdutta.async;
+
+import com.koushikdutta.async.callback.DataCallback;
+
+public class NullDataCallback implements DataCallback {
+ @Override
+ public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) {
+ bb.clear();
+ }
+}
diff --git a/AndroidAsync/src/com/koushikdutta/async/PushParser.java b/AndroidAsync/src/com/koushikdutta/async/PushParser.java
new file mode 100644
index 0000000..60e9639
--- /dev/null
+++ b/AndroidAsync/src/com/koushikdutta/async/PushParser.java
@@ -0,0 +1,217 @@
+package com.koushikdutta.async;
+
+import java.lang.reflect.Method;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.LinkedList;
+
+import junit.framework.Assert;
+
+import com.koushikdutta.async.callback.DataCallback;
+
+public class PushParser {
+ private LinkedList<Object> mWaiting = new LinkedList<Object>();
+
+ static class BufferWaiter {
+ int length;
+ }
+
+ static class UntilWaiter {
+ byte value;
+ DataCallback callback;
+ }
+
+ int mNeeded = 0;
+ public PushParser readInt() {
+ mNeeded += 4;
+ mWaiting.add(int.class);
+ return this;
+ }
+
+ public PushParser readByte() {
+ mNeeded += 1;
+ mWaiting.add(byte.class);
+ return this;
+ }
+
+ public PushParser readShort() {
+ mNeeded += 2;
+ mWaiting.add(short.class);
+ return this;
+ }
+
+ public PushParser readLong() {
+ mNeeded += 8;
+ mWaiting.add(long.class);
+ return this;
+ }
+
+ public PushParser readBuffer(int length) {
+ if (length != -1)
+ mNeeded += length;
+ BufferWaiter bw = new BufferWaiter();
+ bw.length = length;
+ mWaiting.add(bw);
+ return this;
+ }
+
+ public PushParser readLenBuffer() {
+ readInt();
+ BufferWaiter bw = new BufferWaiter();
+ bw.length = -1;
+ mWaiting.add(bw);
+ return this;
+ }
+
+ public PushParser until(byte b, DataCallback callback) {
+ UntilWaiter waiter = new UntilWaiter();
+ waiter.value = b;
+ waiter.callback = callback;
+ mWaiting.add(b);
+ return this;
+ }
+
+ public PushParser noop() {
+ mWaiting.add(Object.class);
+ return this;
+ }
+
+ AsyncInputStream mReader;
+ DataEmitter mEmitter;
+ public PushParser(DataEmitter s) {
+ mEmitter = s;
+ mReader = new DataEmitterStream(s);
+ }
+
+ private ArrayList<Object> mArgs = new ArrayList<Object>();
+ private TapCallback mCallback;
+
+ Exception stack() {
+ try {
+ throw new Exception();
+ }
+ catch (Exception e) {
+ return e;
+ }
+ }
+
+ public void tap(TapCallback callback) {
+ Assert.assertNull(mCallback);
+ Assert.assertTrue(mWaiting.size() > 0);
+
+ mCallback = callback;
+
+ new DataCallback() {
+ {
+ onDataAvailable(mEmitter, null);
+ }
+
+ @Override
+ public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) {
+ try {
+ while (mWaiting.size() > 0) {
+ Object waiting = mWaiting.peek();
+ if (waiting == null)
+ break;
+// System.out.println("Remaining: " + bb.remaining());
+ if (waiting == int.class) {
+ mArgs.add(bb.getInt());
+ mNeeded -= 4;
+ }
+ else if (waiting == short.class) {
+ mArgs.add(bb.getShort());
+ mNeeded -= 2;
+ }
+ else if (waiting == byte.class) {
+ mArgs.add(bb.get());
+ mNeeded -= 1;
+ }
+ else if (waiting == long.class) {
+ mArgs.add(bb.getLong());
+ mNeeded -= 8;
+ }
+ else if (waiting == Object.class) {
+ mArgs.add(null);
+ }
+ else if (waiting instanceof UntilWaiter) {
+ UntilWaiter uw = (UntilWaiter)waiting;
+ boolean found = false;
+ ByteBufferList cb = new ByteBufferList();
+ ByteBuffer lastBuffer = null;
+ do {
+ if (lastBuffer != bb.peek()) {
+ lastBuffer.mark();
+ if (lastBuffer != null) {
+ lastBuffer.reset();
+ cb.add(lastBuffer);
+ }
+ lastBuffer = bb.peek();
+ }
+ }
+ while (bb.remaining() > 0 && (found = (bb.get() != uw.value)));
+
+ int mark = lastBuffer.position();
+ lastBuffer.reset();
+ ByteBuffer add = ByteBuffer.wrap(lastBuffer.array(), lastBuffer.arrayOffset() + lastBuffer.position(), mark - lastBuffer.position());
+ cb.add(add);
+ lastBuffer.position(mark);
+
+ if (!found) {
+ if (uw.callback != null)
+ uw.callback.onDataAvailable(emitter, cb);
+ throw new Exception();
+ }
+ }
+ else if (waiting instanceof BufferWaiter) {
+ BufferWaiter bw = (BufferWaiter)waiting;
+ int length = bw.length;
+ if (length == -1) {
+ length = (Integer)mArgs.get(mArgs.size() - 1);
+ mArgs.remove(mArgs.size() - 1);
+ bw.length = length;
+ mNeeded += length;
+ }
+ if (bb.remaining() < length) {
+// System.out.print("imminient feilure detected");
+ throw new Exception();
+ }
+
+// e.printStackTrace();
+// System.out.println("Buffer length: " + length);
+ byte[] bytes = null;
+ if (length > 0) {
+ bytes = new byte[length];
+ bb.get(bytes);
+ }
+ mNeeded -= length;
+ mArgs.add(bytes);
+ }
+ else {
+ Assert.fail();
+ }
+// System.out.println("Parsed: " + mArgs.get(0));
+ mWaiting.remove();
+ }
+ }
+ catch (Exception ex) {
+ Assert.assertTrue(mNeeded != 0);
+// ex.printStackTrace();
+ mReader.read(mNeeded, this);
+ return;
+ }
+
+ try {
+ Object[] args = mArgs.toArray();
+ mArgs.clear();
+ TapCallback callback = mCallback;
+ mCallback = null;
+ Method method = callback.getTap();
+ method.invoke(callback, args);
+ }
+ catch (Exception ex) {
+ ex.printStackTrace();
+ }
+ }
+ };
+ }
+}
diff --git a/AndroidAsync/src/com/koushikdutta/async/SSLDataExchange.java b/AndroidAsync/src/com/koushikdutta/async/SSLDataExchange.java
new file mode 100644
index 0000000..93f48d6
--- /dev/null
+++ b/AndroidAsync/src/com/koushikdutta/async/SSLDataExchange.java
@@ -0,0 +1,289 @@
+package com.koushikdutta.async;
+
+import java.nio.ByteBuffer;
+import java.security.KeyStore;
+import java.security.cert.X509Certificate;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLEngineResult;
+import javax.net.ssl.SSLEngineResult.HandshakeStatus;
+import javax.net.ssl.SSLEngineResult.Status;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509TrustManager;
+
+import junit.framework.Assert;
+
+import org.apache.http.conn.ssl.StrictHostnameVerifier;
+
+import com.koushikdutta.async.callback.DataCallback;
+import com.koushikdutta.async.callback.WritableCallback;
+
+public class SSLDataExchange implements DataTransformer, DataExchange, ExceptionEmitter {
+ DataExchange mExchange;
+ BufferedDataEmitter mEmitter = new BufferedDataEmitter();
+ BufferedDataSink mSink;
+ ByteBuffer mReadTmp = ByteBuffer.allocate(8192);
+ boolean mUnwrapping = false;
+ public SSLDataExchange(DataExchange exchange) {
+ mExchange = exchange;
+
+ engine.setUseClientMode(true);
+ mSink = new BufferedDataSink(exchange);
+
+ // SSL needs buffering of data written during handshake.
+ // aka exhcange.setDatacallback
+// mEmitter.buffer(exchange);
+
+ mEmitter.setDataCallback(new DataCallback() {
+ @Override
+ public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) {
+ try {
+ if (mUnwrapping)
+ return;
+ mUnwrapping = true;
+
+ ByteBufferList out = new ByteBufferList();
+
+ mReadTmp.position(0);
+ mReadTmp.limit(mReadTmp.capacity());
+ ByteBuffer b;
+ if (bb.size() > 1)
+ b = bb.read(bb.remaining());
+ else if (bb.size() == 1)
+ b = bb.peek();
+ else {
+ b = ByteBuffer.allocate(0);
+ }
+
+ while (true) {
+ int remaining = b.remaining();
+
+ SSLEngineResult res = engine.unwrap(b, mReadTmp);
+ if (res.getStatus() == Status.BUFFER_OVERFLOW) {
+ addToPending(out);
+ mReadTmp = ByteBuffer.allocate(mReadTmp.remaining() * 2);
+ remaining = -1;
+ }
+ handleResult(res);
+ if (b.remaining() == remaining)
+ break;
+ }
+
+ addToPending(out);
+ Util.emitAllData(SSLDataExchange.this, out);
+ }
+ catch (Exception ex) {
+ report(ex);
+ }
+ finally {
+ mUnwrapping = false;
+ }
+ }
+ });
+ }
+
+ void addToPending(ByteBufferList out) {
+ if (mReadTmp.position() > 0) {
+ mReadTmp.limit(mReadTmp.position());
+ mReadTmp.position(0);
+ out.add(mReadTmp);
+ mReadTmp = ByteBuffer.allocate(mReadTmp.capacity());
+ }
+ }
+
+ static {
+ try {
+ ctx = SSLContext.getInstance("Default");
+ }
+ catch (Exception ex) {
+ }
+ }
+ static SSLContext ctx;
+
+ SSLEngine engine = ctx.createSSLEngine();
+ boolean finishedHandshake = false;
+
+ DataCallback mDataCallback;
+ @Override
+ public void setDataCallback(DataCallback callback) {
+ mDataCallback = callback;
+ }
+ @Override
+ public DataCallback getDataCallback() {
+ return mDataCallback;
+ }
+ @Override
+ public boolean isChunked() {
+ return false;
+ }
+
+ private String mHost;
+ public String getHost() {
+ return mHost;
+ }
+ public void setHost(String host) {
+ mHost = host;
+ }
+
+ private int mPort;
+ public int getPort() {
+ return mPort;
+ }
+ public void setPort(int port) {
+ mPort = port;
+ }
+
+ private void handleResult(SSLEngineResult res) {
+ if (res.getHandshakeStatus() == HandshakeStatus.NEED_TASK) {
+ final Runnable task = engine.getDelegatedTask();
+ task.run();
+ }
+
+ if (res.getHandshakeStatus() == HandshakeStatus.NEED_WRAP) {
+ write(ByteBuffer.allocate(0));
+ }
+
+ if (res.getHandshakeStatus() == HandshakeStatus.NEED_UNWRAP) {
+ mEmitter.onDataAvailable();
+ }
+
+ try {
+ if (!finishedHandshake && (engine.getHandshakeStatus() == HandshakeStatus.NOT_HANDSHAKING || engine.getHandshakeStatus() == HandshakeStatus.FINISHED)) {
+ TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ tmf.init((KeyStore) null);
+ boolean trusted = false;
+ for (TrustManager tm : tmf.getTrustManagers()) {
+ try {
+ X509TrustManager xtm = (X509TrustManager) tm;
+ X509Certificate[] certs = (X509Certificate[]) engine.getSession().getPeerCertificates();
+ xtm.checkServerTrusted(certs, "SSL");
+ if (mHost != null) {
+ StrictHostnameVerifier verifier = new StrictHostnameVerifier();
+ verifier.verify(mHost, StrictHostnameVerifier.getCNs(certs[0]), StrictHostnameVerifier.getDNSSubjectAlts(certs[0]));
+ }
+ trusted = true;
+ break;
+ }
+ catch (Exception ex) {
+ ex.printStackTrace();
+ }
+ }
+ finishedHandshake = true;
+ if (!trusted)
+ throw new SSLPeerUnverifiedException("Not trusted by any of the system trust managers.");
+ Assert.assertNotNull(mWriteableCallback);
+ mWriteableCallback.onWriteable();
+ mEmitter.onDataAvailable();
+ }
+ }
+ catch (Exception ex) {
+ report(ex);
+ }
+ }
+
+ private void report(Exception e) {
+ if (mErrorCallback != null)
+ mErrorCallback.onException(e);
+ }
+
+ private void writeTmp() {
+ mWriteTmp.limit(mWriteTmp.position());
+ mWriteTmp.position(0);
+ if (mWriteTmp.remaining() > 0)
+ mSink.write(mWriteTmp);
+ }
+
+ boolean checkWrapResult(SSLEngineResult res) {
+ if (res.getStatus() == Status.BUFFER_OVERFLOW) {
+ mWriteTmp = ByteBuffer.allocate(mWriteTmp.remaining() * 2);
+ return false;
+ }
+ return true;
+ }
+
+ private boolean mWrapping = false;
+ ByteBuffer mWriteTmp = ByteBuffer.allocate(8192);
+ @Override
+ public void write(ByteBuffer bb) {
+ mWrapping = true;
+ int remaining;
+ SSLEngineResult res = null;
+ do {
+ remaining = bb.remaining();
+ mWriteTmp.position(0);
+ mWriteTmp.limit(mWriteTmp.capacity());
+ try {
+ res = engine.wrap(bb, mWriteTmp);
+ if (!checkWrapResult(res))
+ remaining = -1;
+ writeTmp();
+ handleResult(res);
+ }
+ catch (SSLException e) {
+ report(e);
+ }
+ }
+ while (remaining != bb.remaining() || (res != null && res.getHandshakeStatus() == HandshakeStatus.NEED_WRAP));
+ mWrapping = false;
+ }
+
+ @Override
+ public void write(ByteBufferList bb) {
+ if (mWrapping)
+ return;
+ mWrapping = true;
+ int remaining;
+ SSLEngineResult res = null;
+ do {
+ remaining = bb.remaining();
+ mWriteTmp.position(0);
+ mWriteTmp.limit(mWriteTmp.capacity());
+ try {
+ res = engine.wrap(bb.toArray(), mWriteTmp);
+ if (!checkWrapResult(res))
+ remaining = -1;
+ writeTmp();
+ handleResult(res);
+ }
+ catch (SSLException e) {
+ report(e);
+ }
+ }
+ while (remaining != bb.remaining() || (res != null && res.getHandshakeStatus() == HandshakeStatus.NEED_WRAP));
+ mWrapping = false;
+ }
+
+ WritableCallback mWriteableCallback;
+ @Override
+ public void setWriteableCallback(WritableCallback handler) {
+ mWriteableCallback = handler;
+ }
+
+ private ExceptionCallback mErrorCallback;
+ @Override
+ public void setExceptionCallback(ExceptionCallback callback) {
+ mErrorCallback = callback;
+ }
+ @Override
+ public ExceptionCallback getExceptionCallback() {
+ return mErrorCallback;
+ }
+ @Override
+ public WritableCallback getWriteableCallback() {
+ return mWriteableCallback;
+ }
+
+ @Override
+ public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) {
+ mEmitter.onDataAvailable(emitter, bb);
+ }
+
+ @Override
+ public void onException(Exception error) {
+ report(error);
+ }
+}
diff --git a/AndroidAsync/src/com/koushikdutta/async/ServerSocketChannelWrapper.java b/AndroidAsync/src/com/koushikdutta/async/ServerSocketChannelWrapper.java
new file mode 100644
index 0000000..ee31298
--- /dev/null
+++ b/AndroidAsync/src/com/koushikdutta/async/ServerSocketChannelWrapper.java
@@ -0,0 +1,52 @@
+package com.koushikdutta.async;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.ClosedChannelException;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.Selector;
+import java.nio.channels.ServerSocketChannel;
+
+import junit.framework.Assert;
+
+class ServerSocketChannelWrapper extends ChannelWrapper {
+ ServerSocketChannel mChannel;
+
+ ServerSocketChannelWrapper(ServerSocketChannel channel) throws IOException {
+ super(channel);
+ mChannel = channel;
+ }
+
+ @Override
+ public int read(ByteBuffer buffer) throws IOException {
+ final String msg = "Can't read ServerSocketChannel";
+ Assert.fail(msg);
+ throw new IOException(msg);
+ }
+
+ @Override
+ public boolean isConnected() {
+ Assert.fail("ServerSocketChannel is never connected");
+ return false;
+ }
+
+ @Override
+ public int write(ByteBuffer src) throws IOException {
+ final String msg = "Can't write ServerSocketChannel";
+ Assert.fail(msg);
+ throw new IOException(msg);
+
+ }
+
+ @Override
+ public SelectionKey register(Selector sel) throws ClosedChannelException {
+ return mChannel.register(sel, SelectionKey.OP_ACCEPT);
+ }
+
+ @Override
+ public int write(ByteBuffer[] src) throws IOException {
+ final String msg = "Can't write ServerSocketChannel";
+ Assert.fail(msg);
+ throw new IOException(msg);
+ }
+}
diff --git a/AndroidAsync/src/com/koushikdutta/async/SocketChannelWrapper.java b/AndroidAsync/src/com/koushikdutta/async/SocketChannelWrapper.java
new file mode 100644
index 0000000..47feef6
--- /dev/null
+++ b/AndroidAsync/src/com/koushikdutta/async/SocketChannelWrapper.java
@@ -0,0 +1,37 @@
+package com.koushikdutta.async;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.ClosedChannelException;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.Selector;
+import java.nio.channels.SocketChannel;
+
+class SocketChannelWrapper extends ChannelWrapper {
+ SocketChannel mChannel;
+
+ SocketChannelWrapper(SocketChannel channel) throws IOException {
+ super(channel);
+ mChannel = channel;
+ }
+ @Override
+ public int read(ByteBuffer buffer) throws IOException {
+ return mChannel.read(buffer);
+ }
+ @Override
+ public boolean isConnected() {
+ return mChannel.isConnected();
+ }
+ @Override
+ public int write(ByteBuffer src) throws IOException {
+ return mChannel.write(src);
+ }
+ @Override
+ public int write(ByteBuffer[] src) throws IOException {
+ return (int)mChannel.write(src);
+ }
+ @Override
+ public SelectionKey register(Selector sel) throws ClosedChannelException {
+ return register(sel, SelectionKey.OP_CONNECT);
+ }
+}
diff --git a/AndroidAsync/src/com/koushikdutta/async/TapCallback.java b/AndroidAsync/src/com/koushikdutta/async/TapCallback.java
new file mode 100644
index 0000000..a70b689
--- /dev/null
+++ b/AndroidAsync/src/com/koushikdutta/async/TapCallback.java
@@ -0,0 +1,21 @@
+package com.koushikdutta.async;
+
+import java.lang.reflect.Method;
+import java.util.Hashtable;
+
+public class TapCallback {
+ static Hashtable<Class, Method> mTable = new Hashtable<Class, Method>();
+
+ Method getTap() {
+ Method found = mTable.get(getClass());
+ if (found != null)
+ return found;
+ for (Method method : getClass().getMethods()) {
+ if ("tap".equals(method.getName())) {
+ mTable.put(getClass(), method);
+ return method;
+ }
+ }
+ return null;
+ }
+}
diff --git a/AndroidAsync/src/com/koushikdutta/async/Util.java b/AndroidAsync/src/com/koushikdutta/async/Util.java
new file mode 100644
index 0000000..5a7c09d
--- /dev/null
+++ b/AndroidAsync/src/com/koushikdutta/async/Util.java
@@ -0,0 +1,20 @@
+package com.koushikdutta.async;
+
+import junit.framework.Assert;
+
+import com.koushikdutta.async.callback.DataCallback;
+
+public class Util {
+ public static void emitAllData(DataEmitter emitter, ByteBufferList list) {
+ int remaining;
+ while (emitter.getDataCallback() != null && (remaining = list.remaining()) > 0) {
+ DataCallback handler = emitter.getDataCallback();
+ handler.onDataAvailable(emitter, list);
+ if (remaining == list.remaining() && handler == emitter.getDataCallback()) {
+ Assert.fail("mDataHandler failed to consume data, yet remains the mDataHandler.");
+ break;
+ }
+ }
+ Assert.assertEquals(list.remaining(), 0);
+ }
+}
diff --git a/AndroidAsync/src/com/koushikdutta/async/callback/ClosedCallback.java b/AndroidAsync/src/com/koushikdutta/async/callback/ClosedCallback.java
new file mode 100644
index 0000000..9ae034e
--- /dev/null
+++ b/AndroidAsync/src/com/koushikdutta/async/callback/ClosedCallback.java
@@ -0,0 +1,5 @@
+package com.koushikdutta.async.callback;
+
+public interface ClosedCallback {
+ public void onClosed();
+}
diff --git a/AndroidAsync/src/com/koushikdutta/async/callback/CompletedCallback.java b/AndroidAsync/src/com/koushikdutta/async/callback/CompletedCallback.java
new file mode 100644
index 0000000..d6c0342
--- /dev/null
+++ b/AndroidAsync/src/com/koushikdutta/async/callback/CompletedCallback.java
@@ -0,0 +1,5 @@
+package com.koushikdutta.async.callback;
+
+public interface CompletedCallback {
+ public void onCompleted(Exception ex);
+}
diff --git a/AndroidAsync/src/com/koushikdutta/async/callback/ConnectCallback.java b/AndroidAsync/src/com/koushikdutta/async/callback/ConnectCallback.java
new file mode 100644
index 0000000..fdaa838
--- /dev/null
+++ b/AndroidAsync/src/com/koushikdutta/async/callback/ConnectCallback.java
@@ -0,0 +1,7 @@
+package com.koushikdutta.async.callback;
+
+import com.koushikdutta.async.AsyncSocket;
+
+public interface ConnectCallback {
+ public void onConnectCompleted(Exception ex, AsyncSocket socket);
+}
diff --git a/AndroidAsync/src/com/koushikdutta/async/callback/DataCallback.java b/AndroidAsync/src/com/koushikdutta/async/callback/DataCallback.java
new file mode 100644
index 0000000..564e48b
--- /dev/null
+++ b/AndroidAsync/src/com/koushikdutta/async/callback/DataCallback.java
@@ -0,0 +1,9 @@
+package com.koushikdutta.async.callback;
+
+import com.koushikdutta.async.ByteBufferList;
+import com.koushikdutta.async.DataEmitter;
+
+
+public interface DataCallback {
+ public void onDataAvailable(DataEmitter emitter, ByteBufferList bb);
+}
diff --git a/AndroidAsync/src/com/koushikdutta/async/callback/ListenCallback.java b/AndroidAsync/src/com/koushikdutta/async/callback/ListenCallback.java
new file mode 100644
index 0000000..f3b4967
--- /dev/null
+++ b/AndroidAsync/src/com/koushikdutta/async/callback/ListenCallback.java
@@ -0,0 +1,8 @@
+package com.koushikdutta.async.callback;
+
+import com.koushikdutta.async.AsyncSocket;
+
+
+public interface ListenCallback {
+ public void onAccepted(AsyncSocket handler);
+}
diff --git a/AndroidAsync/src/com/koushikdutta/async/callback/ResultCallback.java b/AndroidAsync/src/com/koushikdutta/async/callback/ResultCallback.java
new file mode 100644
index 0000000..6d4ce7b
--- /dev/null
+++ b/AndroidAsync/src/com/koushikdutta/async/callback/ResultCallback.java
@@ -0,0 +1,5 @@
+package com.koushikdutta.async.callback;
+
+public interface ResultCallback<T> {
+ public void onCompleted(Exception e, T result);
+}
diff --git a/AndroidAsync/src/com/koushikdutta/async/callback/WritableCallback.java b/AndroidAsync/src/com/koushikdutta/async/callback/WritableCallback.java
new file mode 100644
index 0000000..edc4baf
--- /dev/null
+++ b/AndroidAsync/src/com/koushikdutta/async/callback/WritableCallback.java
@@ -0,0 +1,5 @@
+package com.koushikdutta.async.callback;
+
+public interface WritableCallback {
+ public void onWriteable();
+}
diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClient.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClient.java
new file mode 100644
index 0000000..e9b5e80
--- /dev/null
+++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClient.java
@@ -0,0 +1,202 @@
+package com.koushikdutta.async.http;
+
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.List;
+
+import com.koushikdutta.async.AsyncServer;
+import com.koushikdutta.async.AsyncSocket;
+import com.koushikdutta.async.ByteBufferList;
+import com.koushikdutta.async.DataEmitter;
+import com.koushikdutta.async.NullDataCallback;
+import com.koushikdutta.async.callback.ClosedCallback;
+import com.koushikdutta.async.callback.CompletedCallback;
+import com.koushikdutta.async.callback.ConnectCallback;
+import com.koushikdutta.async.callback.DataCallback;
+import com.koushikdutta.async.callback.ResultCallback;
+import com.koushikdutta.async.http.callback.HttpConnectCallback;
+import com.koushikdutta.async.http.libcore.RawHeaders;
+
+public class AsyncHttpClient {
+ private static Hashtable<String, HashSet<AsyncSocket>> mSockets = new Hashtable<String, HashSet<AsyncSocket>>();
+
+ public static void connect(final AsyncHttpRequest request, final HttpConnectCallback callback) {
+ connect(AsyncServer.getDefault(), request, callback);
+ }
+
+ public static void connect(final AsyncServer server, final AsyncHttpRequest request, final HttpConnectCallback callback) {
+ connect(server, request, callback, 0);
+ }
+
+ public static void connect(final AsyncServer server, final AsyncHttpRequest request, final HttpConnectCallback callback, int redirectCount) {
+ if (redirectCount > 5) {
+ callback.onConnectCompleted(new Exception("too many redirects"), null);
+ return;
+ }
+ final URI uri = request.getUri();
+ int port = uri.getPort();
+ if (port == -1) {
+ if (uri.getScheme().equals("http"))
+ port = 80;
+ else if (uri.getScheme().equals("https"))
+ port = 443;
+ else {
+ callback.onConnectCompleted(new Exception("invalid uri scheme"), null);
+ return;
+ }
+ }
+
+ ConnectCallback socketConnected = new ConnectCallback() {
+ @Override
+ public void onConnectCompleted(Exception ex, final AsyncSocket socket) {
+ if (ex != null) {
+ callback.onConnectCompleted(ex, null);
+ return;
+ }
+ final AsyncHttpResponseImpl ret = new AsyncHttpResponseImpl(request) {
+ boolean keepalive = false;
+ protected void onHeadersReceived() {
+ super.onHeadersReceived();
+
+ try {
+ RawHeaders headers = getRawHeaders();
+ if ((headers.getResponseCode() == HttpURLConnection.HTTP_MOVED_PERM || headers.getResponseCode() == HttpURLConnection.HTTP_MOVED_TEMP) && request.getFollowRedirect()) {
+ AsyncHttpRequest newReq = new AsyncHttpRequest(new URI(headers.get("Location")), request.getMethod());
+ connect(server, newReq, callback);
+
+ setDataCallback(new NullDataCallback());
+ }
+
+ String kas = headers.get("Connection");
+ if (kas != null && "keep-alive".toLowerCase().equals(kas.toLowerCase()))
+ keepalive = true;
+ }
+ catch (Exception ex) {
+ callback.onConnectCompleted(ex, null);
+ }
+ };
+
+ protected void onCompleted(Exception ex) {
+ super.onCompleted(ex);
+ if (!keepalive) {
+ socket.close();
+ }
+ else {
+ HashSet<AsyncSocket> sockets = mSockets.get(uri.getHost());
+ if (sockets == null) {
+ sockets = new HashSet<AsyncSocket>();
+ mSockets.put(uri.getHost(), sockets);
+ }
+ final HashSet<AsyncSocket> ss = sockets;
+ synchronized (sockets) {
+ sockets.add(socket);
+ socket.setClosedCallback(new ClosedCallback() {
+ @Override
+ public void onClosed() {
+ synchronized (ss) {
+ ss.remove(socket);
+ }
+ socket.setClosedCallback(null);
+ }
+ });
+ }
+ }
+ };
+ };
+ ret.setSocket(socket);
+ callback.onConnectCompleted(null, ret);
+ }
+ };
+
+ HashSet<AsyncSocket> sockets = mSockets.get(uri.getHost());
+ if (sockets != null) {
+ synchronized (sockets) {
+ for (AsyncSocket socket: sockets) {
+ if (socket.isConnected()) {
+ socket.setClosedCallback(null);
+ socketConnected.onConnectCompleted(null, socket);
+ return;
+ }
+ }
+ }
+ }
+ server.connectSocket(uri.getHost(), port, socketConnected);
+ }
+
+ public static void connect(URI uri, final HttpConnectCallback callback) {
+ connect(AsyncServer.getDefault(), new AsyncHttpGet(uri), callback);
+ }
+
+ public static void connect(String uri, final HttpConnectCallback callback) {
+ try {
+ connect(AsyncServer.getDefault(), new AsyncHttpGet(new URI(uri)), callback);
+ }
+ catch (URISyntaxException e) {
+ callback.onConnectCompleted(e, null);
+ }
+ }
+
+ public static interface DownloadCallback extends ResultCallback<ByteBufferList> {
+ }
+
+ public static interface StringCallback extends ResultCallback<String> {
+ }
+
+ private interface ResultConvert {
+ public Object convert(ByteBufferList bb);
+ }
+
+ public static void download(String uri, final DownloadCallback callback) {
+ download(uri, callback, new ResultConvert() {
+ @Override
+ public Object convert(ByteBufferList b) {
+ return b;
+ }
+ });
+ }
+
+ public static void download(String uri, final StringCallback callback) {
+ download(uri, callback, new ResultConvert() {
+ @Override
+ public Object convert(ByteBufferList bb) {
+ StringBuilder builder = new StringBuilder();
+ for (ByteBuffer b: bb) {
+ builder.append(new String(b.array(), b.arrayOffset() + b.position(), b.remaining()));
+ }
+ return builder.toString();
+ }
+ });
+ }
+
+ private static void download(String uri, final ResultCallback callback, final ResultConvert convert) {
+ connect(uri, new HttpConnectCallback() {
+ ByteBufferList buffer = new ByteBufferList();
+ @Override
+ public void onConnectCompleted(Exception ex, AsyncHttpResponse response) {
+ if (ex != null) {
+ callback.onCompleted(ex, null);
+ return;
+ }
+
+ response.setDataCallback(new DataCallback() {
+ @Override
+ public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) {
+ buffer.add(bb);
+ bb.clear();
+ }
+ });
+ response.setCompletedCallback(new CompletedCallback() {
+ @Override
+ public void onCompleted(Exception ex) {
+ callback.onCompleted(ex, buffer != null ? convert.convert(buffer) : null);
+ }
+ });
+ }
+ });
+ }
+}
diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpGet.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpGet.java
new file mode 100644
index 0000000..06f679d
--- /dev/null
+++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpGet.java
@@ -0,0 +1,16 @@
+package com.koushikdutta.async.http;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+public class AsyncHttpGet extends AsyncHttpRequest {
+ public static final String METHOD = "GET";
+
+ public AsyncHttpGet(String uri) throws URISyntaxException {
+ super(new URI(uri), METHOD);
+ }
+
+ public AsyncHttpGet(URI uri) {
+ super(uri, METHOD);
+ }
+}
diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpRequest.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpRequest.java
new file mode 100644
index 0000000..b0fb5cc
--- /dev/null
+++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpRequest.java
@@ -0,0 +1,56 @@
+package com.koushikdutta.async.http;
+
+import java.net.URI;
+
+import junit.framework.Assert;
+
+import com.koushikdutta.async.http.libcore.RawHeaders;
+import com.koushikdutta.async.http.libcore.RequestHeaders;
+
+public class AsyncHttpRequest {
+ public String getRequestLine() {
+ String path = getUri().getPath();
+ if (path.length() == 0)
+ path = "/";
+ return String.format("%s %s HTTP/1.1", mMethod, path);
+ }
+
+ protected final String getDefaultUserAgent() {
+ String agent = System.getProperty("http.agent");
+ return agent != null ? agent : ("Java" + System.getProperty("java.version"));
+ }
+
+ private String mMethod;
+ public String getMethod() {
+ return mMethod;
+ }
+
+ public AsyncHttpRequest(URI uri, String method) {
+ Assert.assertNotNull(uri);
+ mMethod = method;
+ mHeaders = new RequestHeaders(uri, mRawHeaders);
+ mRawHeaders.setStatusLine(getRequestLine());
+ mHeaders.setHost(uri.getHost());
+ mHeaders.setUserAgent(getDefaultUserAgent());
+ mHeaders.setAcceptEncoding("gzip");
+ }
+
+ public URI getUri() {
+ return mHeaders.getUri();
+ }
+
+ private RawHeaders mRawHeaders = new RawHeaders();
+ private RequestHeaders mHeaders;
+
+ public String getRequestString() {
+ return mRawHeaders.toHeaderString();
+ }
+
+ private boolean mFollowRedirect = true;
+ public boolean getFollowRedirect() {
+ return mFollowRedirect;
+ }
+ public void setFollowRedirect(boolean follow) {
+ mFollowRedirect = follow;
+ }
+}
diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponse.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponse.java
new file mode 100644
index 0000000..0aa61ea
--- /dev/null
+++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponse.java
@@ -0,0 +1,10 @@
+package com.koushikdutta.async.http;
+
+import com.koushikdutta.async.ExceptionEmitter;
+import com.koushikdutta.async.DataEmitter;
+import com.koushikdutta.async.callback.CompletedCallback;
+
+public interface AsyncHttpResponse extends DataEmitter, ExceptionEmitter {
+ public void setCompletedCallback(CompletedCallback handler);
+ public CompletedCallback getCloseHandler();
+}
diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponseImpl.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponseImpl.java
new file mode 100644
index 0000000..c6c7f74
--- /dev/null
+++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponseImpl.java
@@ -0,0 +1,205 @@
+package com.koushikdutta.async.http;
+
+import java.nio.ByteBuffer;
+
+import junit.framework.Assert;
+
+import com.koushikdutta.async.AsyncSocket;
+import com.koushikdutta.async.BufferedDataSink;
+import com.koushikdutta.async.ByteBufferList;
+import com.koushikdutta.async.DataEmitter;
+import com.koushikdutta.async.DataExchange;
+import com.koushikdutta.async.DataTransformerBase;
+import com.koushikdutta.async.ExceptionCallback;
+import com.koushikdutta.async.LineEmitter;
+import com.koushikdutta.async.LineEmitter.StringCallback;
+import com.koushikdutta.async.SSLDataExchange;
+import com.koushikdutta.async.callback.ClosedCallback;
+import com.koushikdutta.async.callback.CompletedCallback;
+import com.koushikdutta.async.callback.DataCallback;
+import com.koushikdutta.async.http.libcore.RawHeaders;
+import com.koushikdutta.async.http.libcore.ResponseHeaders;
+import com.koushikdutta.async.http.transform.ChunkedTransformer;
+import com.koushikdutta.async.http.transform.GZIPTransformer;
+import com.koushikdutta.async.http.transform.InflaterTransformer;
+
+public class AsyncHttpResponseImpl extends DataTransformerBase implements AsyncHttpResponse {
+ private RawHeaders mRawHeaders = new RawHeaders();
+ RawHeaders getRawHeaders() {
+ return mRawHeaders;
+ }
+
+ void setSocket(AsyncSocket socket) {
+ mSocket = socket;
+ // socket and exchange are the same for regular http
+ // but different for https (ssl)
+ // the exchange will be a wrapper around socket that does
+ // ssl translation.
+ DataExchange exchange = socket;
+ if (mRequest.getUri().getScheme().equals("https")) {
+ SSLDataExchange ssl = new SSLDataExchange(socket);
+ exchange = ssl;
+ socket.setDataCallback(ssl);
+ }
+ mExchange = exchange;
+
+ mWriter = new BufferedDataSink(exchange);
+ String rs = mRequest.getRequestString();
+ mWriter.write(ByteBuffer.wrap(rs.getBytes()));
+
+ LineEmitter liner = new LineEmitter(exchange);
+ liner.setLineCallback(mHeaderCallback);
+
+ mSocket.setExceptionCallback(new ExceptionCallback() {
+ @Override
+ public void onException(Exception error) {
+ report(error);
+ }
+ });
+ mSocket.setClosedCallback(new ClosedCallback() {
+ @Override
+ public void onClosed() {
+ if (!mCompleted) {
+ report(new Exception("connection closed before response completed."));
+ }
+ }
+ });
+ }
+
+ protected void onHeadersReceived() {
+ mHeaders = new ResponseHeaders(mRequest.getUri(), mRawHeaders);
+
+ DataCallback callback = this;
+
+ if ("gzip".equals(mHeaders.getContentEncoding())) {
+ GZIPTransformer gunzipper = new GZIPTransformer() {
+ @Override
+ public void onException(Exception error) {
+ report(error);
+ }
+ };
+ gunzipper.setDataCallback(callback);
+ callback = gunzipper;
+ }
+ else if ("deflate".equals(mHeaders.getContentEncoding())) {
+ InflaterTransformer inflater = new InflaterTransformer() {
+ @Override
+ public void onException(Exception error) {
+ report(error);
+ }
+ };
+ inflater.setDataCallback(callback);
+ callback = inflater;
+ }
+
+ if (!mHeaders.isChunked()) {
+ if (mHeaders.getContentLength() < 0) {
+ report(new Exception("not using chunked encoding, and no content-length found."));
+ return;
+ }
+ DataTransformerBase contentLengthWatcher = new DataTransformerBase() {
+ int totalRead = 0;
+ @Override
+ public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) {
+ totalRead += bb.remaining();
+ Assert.assertTrue(totalRead <= mHeaders.getContentLength());
+ super.onDataAvailable(emitter, bb);
+ if (totalRead == mHeaders.getContentLength())
+ report(null);
+ }
+ };
+ contentLengthWatcher.setDataCallback(callback);
+ callback = contentLengthWatcher;
+ }
+ else {
+ ChunkedTransformer chunker = new ChunkedTransformer() {
+ @Override
+ public void onCompleted(Exception ex) {
+ AsyncHttpResponseImpl.this.report(ex);
+ }
+
+ @Override
+ public void onException(Exception error) {
+ report(error);
+ }
+ };
+ chunker.setDataCallback(callback);
+ callback = chunker;
+ }
+ mExchange.setDataCallback(callback);
+ }
+
+ StringCallback mHeaderCallback = new StringCallback() {
+ @Override
+ public void onStringAvailable(String s) {
+ try {
+ if (mRawHeaders.getStatusLine() == null) {
+ mRawHeaders.setStatusLine(s);
+ }
+ else if (!"\r".equals(s)){
+ mRawHeaders.addLine(s);
+ }
+ else {
+ onHeadersReceived();
+// System.out.println(mRawHeaders.toHeaderString());
+ }
+ }
+ catch (Exception ex) {
+ report(ex);
+ }
+ }
+ };
+
+ void report(Exception ex) {
+ if (ex != null) {
+ if (mErrorCallback != null)
+ mErrorCallback.onException(ex);
+ }
+ onCompleted(ex);
+ }
+
+ private boolean hasParsedStatusLine = false;
+ private BufferedDataSink mWriter;
+ private AsyncSocket mSocket;
+ private AsyncHttpRequest mRequest;
+ private DataExchange mExchange;
+ private ResponseHeaders mHeaders;
+ public AsyncHttpResponseImpl(AsyncHttpRequest request) {
+ mRequest = request;
+ }
+
+ boolean mCompleted = false;
+ protected void onCompleted(Exception ex) {
+ // DISCONNECT. EVERYTHING.
+ mSocket.setClosedCallback(null);
+ mSocket.setExceptionCallback(null);
+ mSocket.setDataCallback(null);
+ mSocket.setWriteableCallback(null);
+ mCompleted = true;
+// System.out.println("closing up shop");
+ if (mCompletedCallback != null)
+ mCompletedCallback.onCompleted(ex);
+ }
+
+ CompletedCallback mCompletedCallback;
+ @Override
+ public void setCompletedCallback(CompletedCallback handler) {
+ mCompletedCallback = handler;
+ }
+
+ @Override
+ public CompletedCallback getCloseHandler() {
+ return mCompletedCallback;
+ }
+
+ ExceptionCallback mErrorCallback;
+ @Override
+ public void setExceptionCallback(ExceptionCallback callback) {
+ mErrorCallback = callback;
+ }
+
+ @Override
+ public ExceptionCallback getExceptionCallback() {
+ return mErrorCallback;
+ }
+}
diff --git a/AndroidAsync/src/com/koushikdutta/async/http/callback/HttpConnectCallback.java b/AndroidAsync/src/com/koushikdutta/async/http/callback/HttpConnectCallback.java
new file mode 100644
index 0000000..3f85e18
--- /dev/null
+++ b/AndroidAsync/src/com/koushikdutta/async/http/callback/HttpConnectCallback.java
@@ -0,0 +1,7 @@
+package com.koushikdutta.async.http.callback;
+
+import com.koushikdutta.async.http.AsyncHttpResponse;
+
+public interface HttpConnectCallback {
+ public void onConnectCompleted(Exception ex, AsyncHttpResponse response);
+}
diff --git a/AndroidAsync/src/com/koushikdutta/async/http/libcore/HeaderParser.java b/AndroidAsync/src/com/koushikdutta/async/http/libcore/HeaderParser.java
new file mode 100644
index 0000000..a91be67
--- /dev/null
+++ b/AndroidAsync/src/com/koushikdutta/async/http/libcore/HeaderParser.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * 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.koushikdutta.async.http.libcore;
+
+final class HeaderParser {
+
+ public interface CacheControlHandler {
+ void handle(String directive, String parameter);
+ }
+
+ /**
+ * Parse a comma-separated list of cache control header values.
+ */
+ public static void parseCacheControl(String value, CacheControlHandler handler) {
+ int pos = 0;
+ while (pos < value.length()) {
+ int tokenStart = pos;
+ pos = skipUntil(value, pos, "=,");
+ String directive = value.substring(tokenStart, pos).trim();
+
+ if (pos == value.length() || value.charAt(pos) == ',') {
+ pos++; // consume ',' (if necessary)
+ handler.handle(directive, null);
+ continue;
+ }
+
+ pos++; // consume '='
+ pos = skipWhitespace(value, pos);
+
+ String parameter;
+
+ // quoted string
+ if (pos < value.length() && value.charAt(pos) == '\"') {
+ pos++; // consume '"' open quote
+ int parameterStart = pos;
+ pos = skipUntil(value, pos, "\"");
+ parameter = value.substring(parameterStart, pos);
+ pos++; // consume '"' close quote (if necessary)
+
+ // unquoted string
+ } else {
+ int parameterStart = pos;
+ pos = skipUntil(value, pos, ",");
+ parameter = value.substring(parameterStart, pos).trim();
+ }
+
+ handler.handle(directive, parameter);
+ }
+ }
+
+ /**
+ * Returns the next index in {@code input} at or after {@code pos} that
+ * contains a character from {@code characters}. Returns the input length if
+ * none of the requested characters can be found.
+ */
+ private static int skipUntil(String input, int pos, String characters) {
+ for (; pos < input.length(); pos++) {
+ if (characters.indexOf(input.charAt(pos)) != -1) {
+ break;
+ }
+ }
+ return pos;
+ }
+
+ /**
+ * Returns the next non-whitespace character in {@code input} that is white
+ * space. Result is undefined if input contains newline characters.
+ */
+ private static int skipWhitespace(String input, int pos) {
+ for (; pos < input.length(); pos++) {
+ char c = input.charAt(pos);
+ if (c != ' ' && c != '\t') {
+ break;
+ }
+ }
+ return pos;
+ }
+
+ /**
+ * Returns {@code value} as a positive integer, or 0 if it is negative, or
+ * -1 if it cannot be parsed.
+ */
+ public static int parseSeconds(String value) {
+ try {
+ long seconds = Long.parseLong(value);
+ if (seconds > Integer.MAX_VALUE) {
+ return Integer.MAX_VALUE;
+ } else if (seconds < 0) {
+ return 0;
+ } else {
+ return (int) seconds;
+ }
+ } catch (NumberFormatException e) {
+ return -1;
+ }
+ }
+}
diff --git a/AndroidAsync/src/com/koushikdutta/async/http/libcore/HttpDate.java b/AndroidAsync/src/com/koushikdutta/async/http/libcore/HttpDate.java
new file mode 100644
index 0000000..59e4929
--- /dev/null
+++ b/AndroidAsync/src/com/koushikdutta/async/http/libcore/HttpDate.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * 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.koushikdutta.async.http.libcore;
+
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+
+/**
+ * Best-effort parser for HTTP dates.
+ */
+public final class HttpDate {
+
+ /**
+ * Most websites serve cookies in the blessed format. Eagerly create the parser to ensure such
+ * cookies are on the fast path.
+ */
+ private static final ThreadLocal<DateFormat> STANDARD_DATE_FORMAT
+ = new ThreadLocal<DateFormat>() {
+ @Override protected DateFormat initialValue() {
+ DateFormat rfc1123 = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US);
+ rfc1123.setTimeZone(TimeZone.getTimeZone("UTC"));
+ return rfc1123;
+ }
+ };
+
+ /**
+ * If we fail to parse a date in a non-standard format, try each of these formats in sequence.
+ */
+ private static final String[] BROWSER_COMPATIBLE_DATE_FORMATS = new String[] {
+ /* This list comes from {@code org.apache.http.impl.cookie.BrowserCompatSpec}. */
+ "EEEE, dd-MMM-yy HH:mm:ss zzz", // RFC 1036
+ "EEE MMM d HH:mm:ss yyyy", // ANSI C asctime()
+ "EEE, dd-MMM-yyyy HH:mm:ss z",
+ "EEE, dd-MMM-yyyy HH-mm-ss z",
+ "EEE, dd MMM yy HH:mm:ss z",
+ "EEE dd-MMM-yyyy HH:mm:ss z",
+ "EEE dd MMM yyyy HH:mm:ss z",
+ "EEE dd-MMM-yyyy HH-mm-ss z",
+ "EEE dd-MMM-yy HH:mm:ss z",
+ "EEE dd MMM yy HH:mm:ss z",
+ "EEE,dd-MMM-yy HH:mm:ss z",
+ "EEE,dd-MMM-yyyy HH:mm:ss z",
+ "EEE, dd-MM-yyyy HH:mm:ss z",
+
+ /* RI bug 6641315 claims a cookie of this format was once served by www.yahoo.com */
+ "EEE MMM d yyyy HH:mm:ss z",
+ };
+
+ /**
+ * Returns the date for {@code value}. Returns null if the value couldn't be
+ * parsed.
+ */
+ public static Date parse(String value) {
+ try {
+ return STANDARD_DATE_FORMAT.get().parse(value);
+ } catch (ParseException ignore) {
+ }
+ for (String formatString : BROWSER_COMPATIBLE_DATE_FORMATS) {
+ try {
+ return new SimpleDateFormat(formatString, Locale.US).parse(value);
+ } catch (ParseException ignore) {
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the string for {@code value}.
+ */
+ public static String format(Date value) {
+ return STANDARD_DATE_FORMAT.get().format(value);
+ }
+}
diff --git a/AndroidAsync/src/com/koushikdutta/async/http/libcore/Memory.java b/AndroidAsync/src/com/koushikdutta/async/http/libcore/Memory.java
new file mode 100644
index 0000000..60a56c4
--- /dev/null
+++ b/AndroidAsync/src/com/koushikdutta/async/http/libcore/Memory.java
@@ -0,0 +1,178 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.koushikdutta.async.http.libcore;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * Unsafe access to memory.
+ */
+public final class Memory {
+ private Memory() { }
+
+ /**
+ * Used to optimize nio heap buffer bulk get operations. 'dst' must be a primitive array.
+ * 'dstOffset' is measured in units of 'sizeofElements' bytes.
+ */
+ public static native void unsafeBulkGet(Object dst, int dstOffset, int byteCount,
+ byte[] src, int srcOffset, int sizeofElements, boolean swap);
+
+ /**
+ * Used to optimize nio heap buffer bulk put operations. 'src' must be a primitive array.
+ * 'srcOffset' is measured in units of 'sizeofElements' bytes.
+ */
+ public static native void unsafeBulkPut(byte[] dst, int dstOffset, int byteCount,
+ Object src, int srcOffset, int sizeofElements, boolean swap);
+
+ public static int peekInt(byte[] src, int offset, ByteOrder order) {
+ if (order == ByteOrder.BIG_ENDIAN) {
+ return (((src[offset++] & 0xff) << 24) |
+ ((src[offset++] & 0xff) << 16) |
+ ((src[offset++] & 0xff) << 8) |
+ ((src[offset ] & 0xff) << 0));
+ } else {
+ return (((src[offset++] & 0xff) << 0) |
+ ((src[offset++] & 0xff) << 8) |
+ ((src[offset++] & 0xff) << 16) |
+ ((src[offset ] & 0xff) << 24));
+ }
+ }
+
+ public static long peekLong(byte[] src, int offset, ByteOrder order) {
+ if (order == ByteOrder.BIG_ENDIAN) {
+ int h = ((src[offset++] & 0xff) << 24) |
+ ((src[offset++] & 0xff) << 16) |
+ ((src[offset++] & 0xff) << 8) |
+ ((src[offset++] & 0xff) << 0);
+ int l = ((src[offset++] & 0xff) << 24) |
+ ((src[offset++] & 0xff) << 16) |
+ ((src[offset++] & 0xff) << 8) |
+ ((src[offset ] & 0xff) << 0);
+ return (((long) h) << 32L) | ((long) l) & 0xffffffffL;
+ } else {
+ int l = ((src[offset++] & 0xff) << 0) |
+ ((src[offset++] & 0xff) << 8) |
+ ((src[offset++] & 0xff) << 16) |
+ ((src[offset++] & 0xff) << 24);
+ int h = ((src[offset++] & 0xff) << 0) |
+ ((src[offset++] & 0xff) << 8) |
+ ((src[offset++] & 0xff) << 16) |
+ ((src[offset ] & 0xff) << 24);
+ return (((long) h) << 32L) | ((long) l) & 0xffffffffL;
+ }
+ }
+
+ public static short peekShort(byte[] src, int offset, ByteOrder order) {
+ if (order == ByteOrder.BIG_ENDIAN) {
+ return (short) ((src[offset] << 8) | (src[offset + 1] & 0xff));
+ } else {
+ return (short) ((src[offset + 1] << 8) | (src[offset] & 0xff));
+ }
+ }
+
+ public static void pokeInt(byte[] dst, int offset, int value, ByteOrder order) {
+ if (order == ByteOrder.BIG_ENDIAN) {
+ dst[offset++] = (byte) ((value >> 24) & 0xff);
+ dst[offset++] = (byte) ((value >> 16) & 0xff);
+ dst[offset++] = (byte) ((value >> 8) & 0xff);
+ dst[offset ] = (byte) ((value >> 0) & 0xff);
+ } else {
+ dst[offset++] = (byte) ((value >> 0) & 0xff);
+ dst[offset++] = (byte) ((value >> 8) & 0xff);
+ dst[offset++] = (byte) ((value >> 16) & 0xff);
+ dst[offset ] = (byte) ((value >> 24) & 0xff);
+ }
+ }
+
+ public static void pokeLong(byte[] dst, int offset, long value, ByteOrder order) {
+ if (order == ByteOrder.BIG_ENDIAN) {
+ int i = (int) (value >> 32);
+ dst[offset++] = (byte) ((i >> 24) & 0xff);
+ dst[offset++] = (byte) ((i >> 16) & 0xff);
+ dst[offset++] = (byte) ((i >> 8) & 0xff);
+ dst[offset++] = (byte) ((i >> 0) & 0xff);
+ i = (int) value;
+ dst[offset++] = (byte) ((i >> 24) & 0xff);
+ dst[offset++] = (byte) ((i >> 16) & 0xff);
+ dst[offset++] = (byte) ((i >> 8) & 0xff);
+ dst[offset ] = (byte) ((i >> 0) & 0xff);
+ } else {
+ int i = (int) value;
+ dst[offset++] = (byte) ((i >> 0) & 0xff);
+ dst[offset++] = (byte) ((i >> 8) & 0xff);
+ dst[offset++] = (byte) ((i >> 16) & 0xff);
+ dst[offset++] = (byte) ((i >> 24) & 0xff);
+ i = (int) (value >> 32);
+ dst[offset++] = (byte) ((i >> 0) & 0xff);
+ dst[offset++] = (byte) ((i >> 8) & 0xff);
+ dst[offset++] = (byte) ((i >> 16) & 0xff);
+ dst[offset ] = (byte) ((i >> 24) & 0xff);
+ }
+ }
+
+ public static void pokeShort(byte[] dst, int offset, short value, ByteOrder order) {
+ if (order == ByteOrder.BIG_ENDIAN) {
+ dst[offset++] = (byte) ((value >> 8) & 0xff);
+ dst[offset ] = (byte) ((value >> 0) & 0xff);
+ } else {
+ dst[offset++] = (byte) ((value >> 0) & 0xff);
+ dst[offset ] = (byte) ((value >> 8) & 0xff);
+ }
+ }
+
+ /**
+ * Copies 'byteCount' bytes from the source to the destination. The objects are either
+ * instances of DirectByteBuffer or byte[]. The offsets in the byte[] case must include
+ * the Buffer.arrayOffset if the array came from a Buffer.array call. We could make this
+ * private and provide the four type-safe variants, but then ByteBuffer.put(ByteBuffer)
+ * would need to work out which to call based on whether the source and destination buffers
+ * are direct or not.
+ *
+ * @hide make type-safe before making public?
+ */
+ public static native void memmove(Object dstObject, int dstOffset, Object srcObject, int srcOffset, long byteCount);
+
+ public static native byte peekByte(int address);
+ public static native int peekInt(int address, boolean swap);
+ public static native long peekLong(int address, boolean swap);
+ public static native short peekShort(int address, boolean swap);
+
+ public static native void peekByteArray(int address, byte[] dst, int dstOffset, int byteCount);
+ public static native void peekCharArray(int address, char[] dst, int dstOffset, int charCount, boolean swap);
+ public static native void peekDoubleArray(int address, double[] dst, int dstOffset, int doubleCount, boolean swap);
+ public static native void peekFloatArray(int address, float[] dst, int dstOffset, int floatCount, boolean swap);
+ public static native void peekIntArray(int address, int[] dst, int dstOffset, int intCount, boolean swap);
+ public static native void peekLongArray(int address, long[] dst, int dstOffset, int longCount, boolean swap);
+ public static native void peekShortArray(int address, short[] dst, int dstOffset, int shortCount, boolean swap);
+
+ public static native void pokeByte(int address, byte value);
+ public static native void pokeInt(int address, int value, boolean swap);
+ public static native void pokeLong(int address, long value, boolean swap);
+ public static native void pokeShort(int address, short value, boolean swap);
+
+ public static native void pokeByteArray(int address, byte[] src, int offset, int count);
+ public static native void pokeCharArray(int address, char[] src, int offset, int count, boolean swap);
+ public static native void pokeDoubleArray(int address, double[] src, int offset, int count, boolean swap);
+ public static native void pokeFloatArray(int address, float[] src, int offset, int count, boolean swap);
+ public static native void pokeIntArray(int address, int[] src, int offset, int count, boolean swap);
+ public static native void pokeLongArray(int address, long[] src, int offset, int count, boolean swap);
+ public static native void pokeShortArray(int address, short[] src, int offset, int count, boolean swap);
+}
diff --git a/AndroidAsync/src/com/koushikdutta/async/http/libcore/Objects.java b/AndroidAsync/src/com/koushikdutta/async/http/libcore/Objects.java
new file mode 100644
index 0000000..a0bfbad
--- /dev/null
+++ b/AndroidAsync/src/com/koushikdutta/async/http/libcore/Objects.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * 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.koushikdutta.async.http.libcore;
+
+public final class Objects {
+ private Objects() {}
+
+ /**
+ * Returns true if two possibly-null objects are equal.
+ */
+ public static boolean equal(Object a, Object b) {
+ return a == b || (a != null && a.equals(b));
+ }
+
+ public static int hashCode(Object o) {
+ return (o == null) ? 0 : o.hashCode();
+ }
+}
diff --git a/AndroidAsync/src/com/koushikdutta/async/http/libcore/RawHeaders.java b/AndroidAsync/src/com/koushikdutta/async/http/libcore/RawHeaders.java
new file mode 100644
index 0000000..79be2e1
--- /dev/null
+++ b/AndroidAsync/src/com/koushikdutta/async/http/libcore/RawHeaders.java
@@ -0,0 +1,296 @@
+package com.koushikdutta.async.http.libcore;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.TreeMap;
+
+/**
+ * The HTTP status and unparsed header fields of a single HTTP message. Values
+ * are represented as uninterpreted strings; use {@link RequestHeaders} and
+ * {@link ResponseHeaders} for interpreted headers. This class maintains the
+ * order of the header fields within the HTTP message.
+ *
+ * <p>This class tracks fields line-by-line. A field with multiple comma-
+ * separated values on the same line will be treated as a field with a single
+ * value by this class. It is the caller's responsibility to detect and split
+ * on commas if their field permits multiple values. This simplifies use of
+ * single-valued fields whose values routinely contain commas, such as cookies
+ * or dates.
+ *
+ * <p>This class trims whitespace from values. It never returns values with
+ * leading or trailing whitespace.
+ */
+public final class RawHeaders {
+ private static final Comparator<String> FIELD_NAME_COMPARATOR = new Comparator<String>() {
+ @Override public int compare(String a, String b) {
+ if (a == b) {
+ return 0;
+ } else if (a == null) {
+ return -1;
+ } else if (b == null) {
+ return 1;
+ } else {
+ return String.CASE_INSENSITIVE_ORDER.compare(a, b);
+ }
+ }
+ };
+
+ private final List<String> namesAndValues = new ArrayList<String>(20);
+ private String statusLine;
+ private int httpMinorVersion = 1;
+ private int responseCode = -1;
+ private String responseMessage;
+
+ public RawHeaders() {}
+
+ public RawHeaders(RawHeaders copyFrom) {
+ namesAndValues.addAll(copyFrom.namesAndValues);
+ statusLine = copyFrom.statusLine;
+ httpMinorVersion = copyFrom.httpMinorVersion;
+ responseCode = copyFrom.responseCode;
+ responseMessage = copyFrom.responseMessage;
+ }
+
+ /**
+ * Sets the response status line (like "HTTP/1.0 200 OK") or request line
+ * (like "GET / HTTP/1.1").
+ */
+ public void setStatusLine(String statusLine) {
+ statusLine = statusLine.trim();
+ this.statusLine = statusLine;
+
+ if (statusLine == null || !statusLine.startsWith("HTTP/")) {
+ return;
+ }
+ statusLine = statusLine.trim();
+ int mark = statusLine.indexOf(" ") + 1;
+ if (mark == 0) {
+ return;
+ }
+ if (statusLine.charAt(mark - 2) != '1') {
+ this.httpMinorVersion = 0;
+ }
+ int last = mark + 3;
+ if (last > statusLine.length()) {
+ last = statusLine.length();
+ }
+ this.responseCode = Integer.parseInt(statusLine.substring(mark, last));
+ if (last + 1 <= statusLine.length()) {
+ this.responseMessage = statusLine.substring(last + 1);
+ }
+ }
+
+ public String getStatusLine() {
+ return statusLine;
+ }
+
+ /**
+ * Returns the status line's HTTP minor version. This returns 0 for HTTP/1.0
+ * and 1 for HTTP/1.1. This returns 1 if the HTTP version is unknown.
+ */
+ public int getHttpMinorVersion() {
+ return httpMinorVersion != -1 ? httpMinorVersion : 1;
+ }
+
+ /**
+ * Returns the HTTP status code or -1 if it is unknown.
+ */
+ public int getResponseCode() {
+ return responseCode;
+ }
+
+ /**
+ * Returns the HTTP status message or null if it is unknown.
+ */
+ public String getResponseMessage() {
+ return responseMessage;
+ }
+
+ /**
+ * Add an HTTP header line containing a field name, a literal colon, and a
+ * value.
+ */
+ public void addLine(String line) {
+ int index = line.indexOf(":");
+ if (index == -1) {
+ add("", line);
+ } else {
+ add(line.substring(0, index), line.substring(index + 1));
+ }
+ }
+
+ /**
+ * Add a field with the specified value.
+ */
+ public void add(String fieldName, String value) {
+ if (fieldName == null) {
+ throw new IllegalArgumentException("fieldName == null");
+ }
+ if (value == null) {
+ /*
+ * Given null values, the RI sends a malformed field line like
+ * "Accept\r\n". For platform compatibility and HTTP compliance, we
+ * print a warning and ignore null values.
+ */
+ System.err.println("Ignoring HTTP header field '" + fieldName + "' because its value is null");
+ return;
+ }
+ namesAndValues.add(fieldName);
+ namesAndValues.add(value.trim());
+ }
+
+ public void removeAll(String fieldName) {
+ for (int i = 0; i < namesAndValues.size(); i += 2) {
+ if (fieldName.equalsIgnoreCase(namesAndValues.get(i))) {
+ namesAndValues.remove(i); // field name
+ namesAndValues.remove(i); // value
+ }
+ }
+ }
+
+ public void addAll(String fieldName, List<String> headerFields) {
+ for (String value : headerFields) {
+ add(fieldName, value);
+ }
+ }
+
+ /**
+ * Set a field with the specified value. If the field is not found, it is
+ * added. If the field is found, the existing values are replaced.
+ */
+ public void set(String fieldName, String value) {
+ removeAll(fieldName);
+ add(fieldName, value);
+ }
+
+ /**
+ * Returns the number of field values.
+ */
+ public int length() {
+ return namesAndValues.size() / 2;
+ }
+
+ /**
+ * Returns the field at {@code position} or null if that is out of range.
+ */
+ public String getFieldName(int index) {
+ int fieldNameIndex = index * 2;
+ if (fieldNameIndex < 0 || fieldNameIndex >= namesAndValues.size()) {
+ return null;
+ }
+ return namesAndValues.get(fieldNameIndex);
+ }
+
+ /**
+ * Returns the value at {@code index} or null if that is out of range.
+ */
+ public String getValue(int index) {
+ int valueIndex = index * 2 + 1;
+ if (valueIndex < 0 || valueIndex >= namesAndValues.size()) {
+ return null;
+ }
+ return namesAndValues.get(valueIndex);
+ }
+
+ /**
+ * Returns the last value corresponding to the specified field, or null.
+ */
+ public String get(String fieldName) {
+ for (int i = namesAndValues.size() - 2; i >= 0; i -= 2) {
+ if (fieldName.equalsIgnoreCase(namesAndValues.get(i))) {
+ return namesAndValues.get(i + 1);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @param fieldNames a case-insensitive set of HTTP header field names.
+ */
+ public RawHeaders getAll(Set<String> fieldNames) {
+ RawHeaders result = new RawHeaders();
+ for (int i = 0; i < namesAndValues.size(); i += 2) {
+ String fieldName = namesAndValues.get(i);
+ if (fieldNames.contains(fieldName)) {
+ result.add(fieldName, namesAndValues.get(i + 1));
+ }
+ }
+ return result;
+ }
+
+ public String toHeaderString() {
+ StringBuilder result = new StringBuilder(256);
+ result.append(statusLine).append("\r\n");
+ for (int i = 0; i < namesAndValues.size(); i += 2) {
+ result.append(namesAndValues.get(i)).append(": ")
+ .append(namesAndValues.get(i + 1)).append("\r\n");
+ }
+ result.append("\r\n");
+ return result.toString();
+ }
+
+ /**
+ * Returns an immutable map containing each field to its list of values. The
+ * status line is mapped to null.
+ */
+ public Map<String, List<String>> toMultimap() {
+ Map<String, List<String>> result = new TreeMap<String, List<String>>(FIELD_NAME_COMPARATOR);
+ for (int i = 0; i < namesAndValues.size(); i += 2) {
+ String fieldName = namesAndValues.get(i);
+ String value = namesAndValues.get(i + 1);
+
+ List<String> allValues = new ArrayList<String>();
+ List<String> otherValues = result.get(fieldName);
+ if (otherValues != null) {
+ allValues.addAll(otherValues);
+ }
+ allValues.add(value);
+ result.put(fieldName, Collections.unmodifiableList(allValues));
+ }
+ if (statusLine != null) {
+ result.put(null, Collections.unmodifiableList(Collections.singletonList(statusLine)));
+ }
+ return Collections.unmodifiableMap(result);
+ }
+
+ /**
+ * Creates a new instance from the given map of fields to values. If
+ * present, the null field's last element will be used to set the status
+ * line.
+ */
+ public static RawHeaders fromMultimap(Map<String, List<String>> map) {
+ RawHeaders result = new RawHeaders();
+ for (Entry<String, List<String>> entry : map.entrySet()) {
+ String fieldName = entry.getKey();
+ List<String> values = entry.getValue();
+ if (fieldName != null) {
+ result.addAll(fieldName, values);
+ } else if (!values.isEmpty()) {
+ result.setStatusLine(values.get(values.size() - 1));
+ }
+ }
+ return result;
+ }
+}
diff --git a/AndroidAsync/src/com/koushikdutta/async/http/libcore/RequestHeaders.java b/AndroidAsync/src/com/koushikdutta/async/http/libcore/RequestHeaders.java
new file mode 100644
index 0000000..4e29e71
--- /dev/null
+++ b/AndroidAsync/src/com/koushikdutta/async/http/libcore/RequestHeaders.java
@@ -0,0 +1,292 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * 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.koushikdutta.async.http.libcore;
+
+import java.net.URI;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Parsed HTTP request headers.
+ */
+public final class RequestHeaders {
+ private final URI uri;
+ private final RawHeaders headers;
+
+ /** Don't use a cache to satisfy this request. */
+ private boolean noCache;
+ private int maxAgeSeconds = -1;
+ private int maxStaleSeconds = -1;
+ private int minFreshSeconds = -1;
+
+ /**
+ * This field's name "only-if-cached" is misleading. It actually means "do
+ * not use the network". It is set by a client who only wants to make a
+ * request if it can be fully satisfied by the cache. Cached responses that
+ * would require validation (ie. conditional gets) are not permitted if this
+ * header is set.
+ */
+ private boolean onlyIfCached;
+
+ /**
+ * True if the request contains an authorization field. Although this isn't
+ * necessarily a shared cache, it follows the spec's strict requirements for
+ * shared caches.
+ */
+ private boolean hasAuthorization;
+
+ private int contentLength = -1;
+ private String transferEncoding;
+ private String userAgent;
+ private String host;
+ private String connection;
+ private String acceptEncoding;
+ private String contentType;
+ private String ifModifiedSince;
+ private String ifNoneMatch;
+ private String proxyAuthorization;
+
+ public RequestHeaders(URI uri, RawHeaders headers) {
+ this.uri = uri;
+ this.headers = headers;
+
+ HeaderParser.CacheControlHandler handler = new HeaderParser.CacheControlHandler() {
+ @Override public void handle(String directive, String parameter) {
+ if (directive.equalsIgnoreCase("no-cache")) {
+ noCache = true;
+ } else if (directive.equalsIgnoreCase("max-age")) {
+ maxAgeSeconds = HeaderParser.parseSeconds(parameter);
+ } else if (directive.equalsIgnoreCase("max-stale")) {
+ maxStaleSeconds = HeaderParser.parseSeconds(parameter);
+ } else if (directive.equalsIgnoreCase("min-fresh")) {
+ minFreshSeconds = HeaderParser.parseSeconds(parameter);
+ } else if (directive.equalsIgnoreCase("only-if-cached")) {
+ onlyIfCached = true;
+ }
+ }
+ };
+
+ for (int i = 0; i < headers.length(); i++) {
+ String fieldName = headers.getFieldName(i);
+ String value = headers.getValue(i);
+ if ("Cache-Control".equalsIgnoreCase(fieldName)) {
+ HeaderParser.parseCacheControl(value, handler);
+ } else if ("Pragma".equalsIgnoreCase(fieldName)) {
+ if (value.equalsIgnoreCase("no-cache")) {
+ noCache = true;
+ }
+ } else if ("If-None-Match".equalsIgnoreCase(fieldName)) {
+ ifNoneMatch = value;
+ } else if ("If-Modified-Since".equalsIgnoreCase(fieldName)) {
+ ifModifiedSince = value;
+ } else if ("Authorization".equalsIgnoreCase(fieldName)) {
+ hasAuthorization = true;
+ } else if ("Content-Length".equalsIgnoreCase(fieldName)) {
+ try {
+ contentLength = Integer.parseInt(value);
+ } catch (NumberFormatException ignored) {
+ }
+ } else if ("Transfer-Encoding".equalsIgnoreCase(fieldName)) {
+ transferEncoding = value;
+ } else if ("User-Agent".equalsIgnoreCase(fieldName)) {
+ userAgent = value;
+ } else if ("Host".equalsIgnoreCase(fieldName)) {
+ host = value;
+ } else if ("Connection".equalsIgnoreCase(fieldName)) {
+ connection = value;
+ } else if ("Accept-Encoding".equalsIgnoreCase(fieldName)) {
+ acceptEncoding = value;
+ } else if ("Content-Type".equalsIgnoreCase(fieldName)) {
+ contentType = value;
+ } else if ("Proxy-Authorization".equalsIgnoreCase(fieldName)) {
+ proxyAuthorization = value;
+ }
+ }
+ }
+
+ public boolean isChunked() {
+ return "chunked".equalsIgnoreCase(transferEncoding);
+ }
+
+ public boolean hasConnectionClose() {
+ return "close".equalsIgnoreCase(connection);
+ }
+
+ public URI getUri() {
+ return uri;
+ }
+
+ public RawHeaders getHeaders() {
+ return headers;
+ }
+
+ public boolean isNoCache() {
+ return noCache;
+ }
+
+ public int getMaxAgeSeconds() {
+ return maxAgeSeconds;
+ }
+
+ public int getMaxStaleSeconds() {
+ return maxStaleSeconds;
+ }
+
+ public int getMinFreshSeconds() {
+ return minFreshSeconds;
+ }
+
+ public boolean isOnlyIfCached() {
+ return onlyIfCached;
+ }
+
+ public boolean hasAuthorization() {
+ return hasAuthorization;
+ }
+
+ public int getContentLength() {
+ return contentLength;
+ }
+
+ public String getTransferEncoding() {
+ return transferEncoding;
+ }
+
+ public String getUserAgent() {
+ return userAgent;
+ }
+
+ public String getHost() {
+ return host;
+ }
+
+ public String getConnection() {
+ return connection;
+ }
+
+ public String getAcceptEncoding() {
+ return acceptEncoding;
+ }
+
+ public String getContentType() {
+ return contentType;
+ }
+
+ public String getIfModifiedSince() {
+ return ifModifiedSince;
+ }
+
+ public String getIfNoneMatch() {
+ return ifNoneMatch;
+ }
+
+ public String getProxyAuthorization() {
+ return proxyAuthorization;
+ }
+
+ public void setChunked() {
+ if (this.transferEncoding != null) {
+ headers.removeAll("Transfer-Encoding");
+ }
+ headers.add("Transfer-Encoding", "chunked");
+ this.transferEncoding = "chunked";
+ }
+
+ public void setContentLength(int contentLength) {
+ if (this.contentLength != -1) {
+ headers.removeAll("Content-Length");
+ }
+ headers.add("Content-Length", Integer.toString(contentLength));
+ this.contentLength = contentLength;
+ }
+
+ public void setUserAgent(String userAgent) {
+ if (this.userAgent != null) {
+ headers.removeAll("User-Agent");
+ }
+ headers.add("User-Agent", userAgent);
+ this.userAgent = userAgent;
+ }
+
+ public void setHost(String host) {
+ if (this.host != null) {
+ headers.removeAll("Host");
+ }
+ headers.add("Host", host);
+ this.host = host;
+ }
+
+ public void setConnection(String connection) {
+ if (this.connection != null) {
+ headers.removeAll("Connection");
+ }
+ headers.add("Connection", connection);
+ this.connection = connection;
+ }
+
+ public void setAcceptEncoding(String acceptEncoding) {
+ if (this.acceptEncoding != null) {
+ headers.removeAll("Accept-Encoding");
+ }
+ headers.add("Accept-Encoding", acceptEncoding);
+ this.acceptEncoding = acceptEncoding;
+ }
+
+ public void setContentType(String contentType) {
+ if (this.contentType != null) {
+ headers.removeAll("Content-Type");
+ }
+ headers.add("Content-Type", contentType);
+ this.contentType = contentType;
+ }
+
+ public void setIfModifiedSince(Date date) {
+ if (ifModifiedSince != null) {
+ headers.removeAll("If-Modified-Since");
+ }
+ String formattedDate = HttpDate.format(date);
+ headers.add("If-Modified-Since", formattedDate);
+ ifModifiedSince = formattedDate;
+ }
+
+ public void setIfNoneMatch(String ifNoneMatch) {
+ if (this.ifNoneMatch != null) {
+ headers.removeAll("If-None-Match");
+ }
+ headers.add("If-None-Match", ifNoneMatch);
+ this.ifNoneMatch = ifNoneMatch;
+ }
+
+ /**
+ * Returns true if the request contains conditions that save the server from
+ * sending a response that the client has locally. When the caller adds
+ * conditions, this cache won't participate in the request.
+ */
+ public boolean hasConditions() {
+ return ifModifiedSince != null || ifNoneMatch != null;
+ }
+
+ public void addCookies(Map<String, List<String>> allCookieHeaders) {
+ for (Map.Entry<String, List<String>> entry : allCookieHeaders.entrySet()) {
+ String key = entry.getKey();
+ if ("Cookie".equalsIgnoreCase(key) || "Cookie2".equalsIgnoreCase(key)) {
+ headers.addAll(key, entry.getValue());
+ }
+ }
+ }
+}
diff --git a/AndroidAsync/src/com/koushikdutta/async/http/libcore/ResponseHeaders.java b/AndroidAsync/src/com/koushikdutta/async/http/libcore/ResponseHeaders.java
new file mode 100644
index 0000000..8f7bb65
--- /dev/null
+++ b/AndroidAsync/src/com/koushikdutta/async/http/libcore/ResponseHeaders.java
@@ -0,0 +1,518 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * 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.koushikdutta.async.http.libcore;
+
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Parsed HTTP response headers.
+ */
+public final class ResponseHeaders {
+
+ /** HTTP header name for the local time when the request was sent. */
+ private static final String SENT_MILLIS = "X-Android-Sent-Millis";
+
+ /** HTTP header name for the local time when the response was received. */
+ private static final String RECEIVED_MILLIS = "X-Android-Received-Millis";
+
+ private final URI uri;
+ private final RawHeaders headers;
+
+ /** The server's time when this response was served, if known. */
+ private Date servedDate;
+
+ /** The last modified date of the response, if known. */
+ private Date lastModified;
+
+ /**
+ * The expiration date of the response, if known. If both this field and the
+ * max age are set, the max age is preferred.
+ */
+ private Date expires;
+
+ /**
+ * Extension header set by HttpURLConnectionImpl specifying the timestamp
+ * when the HTTP request was first initiated.
+ */
+ private long sentRequestMillis;
+
+ /**
+ * Extension header set by HttpURLConnectionImpl specifying the timestamp
+ * when the HTTP response was first received.
+ */
+ private long receivedResponseMillis;
+
+ /**
+ * In the response, this field's name "no-cache" is misleading. It doesn't
+ * prevent us from caching the response; it only means we have to validate
+ * the response with the origin server before returning it. We can do this
+ * with a conditional get.
+ */
+ private boolean noCache;
+
+ /** If true, this response should not be cached. */
+ private boolean noStore;
+
+ /**
+ * The duration past the response's served date that it can be served
+ * without validation.
+ */
+ private int maxAgeSeconds = -1;
+
+ /**
+ * The "s-maxage" directive is the max age for shared caches. Not to be
+ * confused with "max-age" for non-shared caches, As in Firefox and Chrome,
+ * this directive is not honored by this cache.
+ */
+ private int sMaxAgeSeconds = -1;
+
+ /**
+ * This request header field's name "only-if-cached" is misleading. It
+ * actually means "do not use the network". It is set by a client who only
+ * wants to make a request if it can be fully satisfied by the cache.
+ * Cached responses that would require validation (ie. conditional gets) are
+ * not permitted if this header is set.
+ */
+ private boolean isPublic;
+ private boolean mustRevalidate;
+ private String etag;
+ private int ageSeconds = -1;
+
+ /** Case-insensitive set of field names. */
+ private Set<String> varyFields = Collections.emptySet();
+
+ private String contentEncoding;
+ private String transferEncoding;
+ private int contentLength = -1;
+ private String connection;
+ private String proxyAuthenticate;
+ private String wwwAuthenticate;
+
+ public ResponseHeaders(URI uri, RawHeaders headers) {
+ this.uri = uri;
+ this.headers = headers;
+
+ HeaderParser.CacheControlHandler handler = new HeaderParser.CacheControlHandler() {
+ @Override public void handle(String directive, String parameter) {
+ if (directive.equalsIgnoreCase("no-cache")) {
+ noCache = true;
+ } else if (directive.equalsIgnoreCase("no-store")) {
+ noStore = true;
+ } else if (directive.equalsIgnoreCase("max-age")) {
+ maxAgeSeconds = HeaderParser.parseSeconds(parameter);
+ } else if (directive.equalsIgnoreCase("s-maxage")) {
+ sMaxAgeSeconds = HeaderParser.parseSeconds(parameter);
+ } else if (directive.equalsIgnoreCase("public")) {
+ isPublic = true;
+ } else if (directive.equalsIgnoreCase("must-revalidate")) {
+ mustRevalidate = true;
+ }
+ }
+ };
+
+ for (int i = 0; i < headers.length(); i++) {
+ String fieldName = headers.getFieldName(i);
+ String value = headers.getValue(i);
+ if ("Cache-Control".equalsIgnoreCase(fieldName)) {
+ HeaderParser.parseCacheControl(value, handler);
+ } else if ("Date".equalsIgnoreCase(fieldName)) {
+ servedDate = HttpDate.parse(value);
+ } else if ("Expires".equalsIgnoreCase(fieldName)) {
+ expires = HttpDate.parse(value);
+ } else if ("Last-Modified".equalsIgnoreCase(fieldName)) {
+ lastModified = HttpDate.parse(value);
+ } else if ("ETag".equalsIgnoreCase(fieldName)) {
+ etag = value;
+ } else if ("Pragma".equalsIgnoreCase(fieldName)) {
+ if (value.equalsIgnoreCase("no-cache")) {
+ noCache = true;
+ }
+ } else if ("Age".equalsIgnoreCase(fieldName)) {
+ ageSeconds = HeaderParser.parseSeconds(value);
+ } else if ("Vary".equalsIgnoreCase(fieldName)) {
+ // Replace the immutable empty set with something we can mutate.
+ if (varyFields.isEmpty()) {
+ varyFields = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
+ }
+ for (String varyField : value.split(",")) {
+ varyFields.add(varyField.trim());
+ }
+ } else if ("Content-Encoding".equalsIgnoreCase(fieldName)) {
+ contentEncoding = value;
+ } else if ("Transfer-Encoding".equalsIgnoreCase(fieldName)) {
+ transferEncoding = value;
+ } else if ("Content-Length".equalsIgnoreCase(fieldName)) {
+ try {
+ contentLength = Integer.parseInt(value);
+ } catch (NumberFormatException ignored) {
+ }
+ } else if ("Connection".equalsIgnoreCase(fieldName)) {
+ connection = value;
+ } else if ("Proxy-Authenticate".equalsIgnoreCase(fieldName)) {
+ proxyAuthenticate = value;
+ } else if ("WWW-Authenticate".equalsIgnoreCase(fieldName)) {
+ wwwAuthenticate = value;
+ } else if (SENT_MILLIS.equalsIgnoreCase(fieldName)) {
+ sentRequestMillis = Long.parseLong(value);
+ } else if (RECEIVED_MILLIS.equalsIgnoreCase(fieldName)) {
+ receivedResponseMillis = Long.parseLong(value);
+ }
+ }
+ }
+
+ public boolean isContentEncodingGzip() {
+ return "gzip".equalsIgnoreCase(contentEncoding);
+ }
+
+ public void stripContentEncoding() {
+ contentEncoding = null;
+ headers.removeAll("Content-Encoding");
+ }
+
+ public boolean isChunked() {
+ return "chunked".equalsIgnoreCase(transferEncoding);
+ }
+
+ public boolean hasConnectionClose() {
+ return "close".equalsIgnoreCase(connection);
+ }
+
+ public URI getUri() {
+ return uri;
+ }
+
+ public RawHeaders getHeaders() {
+ return headers;
+ }
+
+ public Date getServedDate() {
+ return servedDate;
+ }
+
+ public Date getLastModified() {
+ return lastModified;
+ }
+
+ public Date getExpires() {
+ return expires;
+ }
+
+ public boolean isNoCache() {
+ return noCache;
+ }
+
+ public boolean isNoStore() {
+ return noStore;
+ }
+
+ public int getMaxAgeSeconds() {
+ return maxAgeSeconds;
+ }
+
+ public int getSMaxAgeSeconds() {
+ return sMaxAgeSeconds;
+ }
+
+ public boolean isPublic() {
+ return isPublic;
+ }
+
+ public boolean isMustRevalidate() {
+ return mustRevalidate;
+ }
+
+ public String getEtag() {
+ return etag;
+ }
+
+ public Set<String> getVaryFields() {
+ return varyFields;
+ }
+
+ public String getContentEncoding() {
+ return contentEncoding;
+ }
+
+ public int getContentLength() {
+ return contentLength;
+ }
+
+ public String getConnection() {
+ return connection;
+ }
+
+ public String getProxyAuthenticate() {
+ return proxyAuthenticate;
+ }
+
+ public String getWwwAuthenticate() {
+ return wwwAuthenticate;
+ }
+
+ public void setLocalTimestamps(long sentRequestMillis, long receivedResponseMillis) {
+ this.sentRequestMillis = sentRequestMillis;
+ headers.add(SENT_MILLIS, Long.toString(sentRequestMillis));
+ this.receivedResponseMillis = receivedResponseMillis;
+ headers.add(RECEIVED_MILLIS, Long.toString(receivedResponseMillis));
+ }
+
+ /**
+ * Returns the current age of the response, in milliseconds. The calculation
+ * is specified by RFC 2616, 13.2.3 Age Calculations.
+ */
+ private long computeAge(long nowMillis) {
+ long apparentReceivedAge = servedDate != null
+ ? Math.max(0, receivedResponseMillis - servedDate.getTime())
+ : 0;
+ long receivedAge = ageSeconds != -1
+ ? Math.max(apparentReceivedAge, TimeUnit.SECONDS.toMillis(ageSeconds))
+ : apparentReceivedAge;
+ long responseDuration = receivedResponseMillis - sentRequestMillis;
+ long residentDuration = nowMillis - receivedResponseMillis;
+ return receivedAge + responseDuration + residentDuration;
+ }
+
+ /**
+ * Returns the number of milliseconds that the response was fresh for,
+ * starting from the served date.
+ */
+ private long computeFreshnessLifetime() {
+ if (maxAgeSeconds != -1) {
+ return TimeUnit.SECONDS.toMillis(maxAgeSeconds);
+ } else if (expires != null) {
+ long servedMillis = servedDate != null ? servedDate.getTime() : receivedResponseMillis;
+ long delta = expires.getTime() - servedMillis;
+ return delta > 0 ? delta : 0;
+ } else if (lastModified != null && uri.getRawQuery() == null) {
+ /*
+ * As recommended by the HTTP RFC and implemented in Firefox, the
+ * max age of a document should be defaulted to 10% of the
+ * document's age at the time it was served. Default expiration
+ * dates aren't used for URIs containing a query.
+ */
+ long servedMillis = servedDate != null ? servedDate.getTime() : sentRequestMillis;
+ long delta = servedMillis - lastModified.getTime();
+ return delta > 0 ? (delta / 10) : 0;
+ }
+ return 0;
+ }
+
+ /**
+ * Returns true if computeFreshnessLifetime used a heuristic. If we used a
+ * heuristic to serve a cached response older than 24 hours, we are required
+ * to attach a warning.
+ */
+ private boolean isFreshnessLifetimeHeuristic() {
+ return maxAgeSeconds == -1 && expires == null;
+ }
+
+ /**
+ * Returns true if this response can be stored to later serve another
+ * request.
+ */
+ public boolean isCacheable(RequestHeaders request) {
+ /*
+ * Always go to network for uncacheable response codes (RFC 2616, 13.4),
+ * This implementation doesn't support caching partial content.
+ */
+ int responseCode = headers.getResponseCode();
+ if (responseCode != HttpURLConnection.HTTP_OK
+ && responseCode != HttpURLConnection.HTTP_NOT_AUTHORITATIVE
+ && responseCode != HttpURLConnection.HTTP_MULT_CHOICE
+ && responseCode != HttpURLConnection.HTTP_MOVED_PERM
+ && responseCode != HttpURLConnection.HTTP_GONE) {
+ return false;
+ }
+
+ /*
+ * Responses to authorized requests aren't cacheable unless they include
+ * a 'public', 'must-revalidate' or 's-maxage' directive.
+ */
+ if (request.hasAuthorization()
+ && !isPublic
+ && !mustRevalidate
+ && sMaxAgeSeconds == -1) {
+ return false;
+ }
+
+ if (noStore) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns true if a Vary header contains an asterisk. Such responses cannot
+ * be cached.
+ */
+ public boolean hasVaryAll() {
+ return varyFields.contains("*");
+ }
+
+ /**
+ * Returns true if none of the Vary headers on this response have changed
+ * between {@code cachedRequest} and {@code newRequest}.
+ */
+ public boolean varyMatches(Map<String, List<String>> cachedRequest,
+ Map<String, List<String>> newRequest) {
+ for (String field : varyFields) {
+ if (!Objects.equal(cachedRequest.get(field), newRequest.get(field))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Returns the source to satisfy {@code request} given this cached response.
+ */
+ public ResponseSource chooseResponseSource(long nowMillis, RequestHeaders request) {
+ /*
+ * If this response shouldn't have been stored, it should never be used
+ * as a response source. This check should be redundant as long as the
+ * persistence store is well-behaved and the rules are constant.
+ */
+ if (!isCacheable(request)) {
+ return ResponseSource.NETWORK;
+ }
+
+ if (request.isNoCache() || request.hasConditions()) {
+ return ResponseSource.NETWORK;
+ }
+
+ long ageMillis = computeAge(nowMillis);
+ long freshMillis = computeFreshnessLifetime();
+
+ if (request.getMaxAgeSeconds() != -1) {
+ freshMillis = Math.min(freshMillis,
+ TimeUnit.SECONDS.toMillis(request.getMaxAgeSeconds()));
+ }
+
+ long minFreshMillis = 0;
+ if (request.getMinFreshSeconds() != -1) {
+ minFreshMillis = TimeUnit.SECONDS.toMillis(request.getMinFreshSeconds());
+ }
+
+ long maxStaleMillis = 0;
+ if (!mustRevalidate && request.getMaxStaleSeconds() != -1) {
+ maxStaleMillis = TimeUnit.SECONDS.toMillis(request.getMaxStaleSeconds());
+ }
+
+ if (!noCache && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
+ if (ageMillis + minFreshMillis >= freshMillis) {
+ headers.add("Warning", "110 HttpURLConnection \"Response is stale\"");
+ }
+ /*
+ * not available in API 8
+ if (ageMillis > TimeUnit.HOURS.toMillis(24) && isFreshnessLifetimeHeuristic()) {
+ */
+ if (ageMillis > 24L * 60L * 60L * 1000L && isFreshnessLifetimeHeuristic()) {
+ headers.add("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
+ }
+ return ResponseSource.CACHE;
+ }
+
+ if (lastModified != null) {
+ request.setIfModifiedSince(lastModified);
+ } else if (servedDate != null) {
+ request.setIfModifiedSince(servedDate);
+ }
+
+ if (etag != null) {
+ request.setIfNoneMatch(etag);
+ }
+
+ return request.hasConditions()
+ ? ResponseSource.CONDITIONAL_CACHE
+ : ResponseSource.NETWORK;
+ }
+
+ /**
+ * Returns true if this cached response should be used; false if the
+ * network response should be used.
+ */
+ public boolean validate(ResponseHeaders networkResponse) {
+ if (networkResponse.headers.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) {
+ return true;
+ }
+
+ /*
+ * The HTTP spec says that if the network's response is older than our
+ * cached response, we may return the cache's response. Like Chrome (but
+ * unlike Firefox), this client prefers to return the newer response.
+ */
+ if (lastModified != null
+ && networkResponse.lastModified != null
+ && networkResponse.lastModified.getTime() < lastModified.getTime()) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Combines this cached header with a network header as defined by RFC 2616,
+ * 13.5.3.
+ */
+ public ResponseHeaders combine(ResponseHeaders network) {
+ RawHeaders result = new RawHeaders();
+
+ for (int i = 0; i < headers.length(); i++) {
+ String fieldName = headers.getFieldName(i);
+ String value = headers.getValue(i);
+ if (fieldName.equals("Warning") && value.startsWith("1")) {
+ continue; // drop 100-level freshness warnings
+ }
+ if (!isEndToEnd(fieldName) || network.headers.get(fieldName) == null) {
+ result.add(fieldName, value);
+ }
+ }
+
+ for (int i = 0; i < network.headers.length(); i++) {
+ String fieldName = network.headers.getFieldName(i);
+ if (isEndToEnd(fieldName)) {
+ result.add(fieldName, network.headers.getValue(i));
+ }
+ }
+
+ return new ResponseHeaders(uri, result);
+ }
+
+ /**
+ * Returns true if {@code fieldName} is an end-to-end HTTP header, as
+ * defined by RFC 2616, 13.5.1.
+ */
+ private static boolean isEndToEnd(String fieldName) {
+ return !fieldName.equalsIgnoreCase("Connection")
+ && !fieldName.equalsIgnoreCase("Keep-Alive")
+ && !fieldName.equalsIgnoreCase("Proxy-Authenticate")
+ && !fieldName.equalsIgnoreCase("Proxy-Authorization")
+ && !fieldName.equalsIgnoreCase("TE")
+ && !fieldName.equalsIgnoreCase("Trailers")
+ && !fieldName.equalsIgnoreCase("Transfer-Encoding")
+ && !fieldName.equalsIgnoreCase("Upgrade");
+ }
+}
diff --git a/AndroidAsync/src/com/koushikdutta/async/http/libcore/ResponseSource.java b/AndroidAsync/src/com/koushikdutta/async/http/libcore/ResponseSource.java
new file mode 100644
index 0000000..d1eaed4
--- /dev/null
+++ b/AndroidAsync/src/com/koushikdutta/async/http/libcore/ResponseSource.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * 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.koushikdutta.async.http.libcore;
+
+enum ResponseSource {
+
+ /**
+ * Return the response from the cache immediately.
+ */
+ CACHE,
+
+ /**
+ * Make a conditional request to the host, returning the cache response if
+ * the cache is valid and the network response otherwise.
+ */
+ CONDITIONAL_CACHE,
+
+ /**
+ * Return the response from the network.
+ */
+ NETWORK;
+
+ public boolean requiresConnection() {
+ return this == CONDITIONAL_CACHE || this == NETWORK;
+ }
+}
diff --git a/AndroidAsync/src/com/koushikdutta/async/http/transform/ChunkedTransformer.java b/AndroidAsync/src/com/koushikdutta/async/http/transform/ChunkedTransformer.java
new file mode 100644
index 0000000..2997af2
--- /dev/null
+++ b/AndroidAsync/src/com/koushikdutta/async/http/transform/ChunkedTransformer.java
@@ -0,0 +1,118 @@
+package com.koushikdutta.async.http.transform;
+
+import junit.framework.Assert;
+
+import com.koushikdutta.async.ByteBufferList;
+import com.koushikdutta.async.DataEmitter;
+import com.koushikdutta.async.DataTransformerBase;
+import com.koushikdutta.async.Util;
+import com.koushikdutta.async.callback.CompletedCallback;
+
+public abstract class ChunkedTransformer extends DataTransformerBase implements CompletedCallback {
+ private int mChunkLength = 0;
+ private int mChunkLengthRemaining = 0;
+ private State mState = State.CHUNK_LEN;
+
+ private static enum State {
+ CHUNK_LEN,
+ CHUNK_LEN_CR,
+ CHUNK_LEN_CRLF,
+ CHUNK,
+ CHUNK_CR,
+ CHUNK_CRLF,
+ COMPLETE
+ }
+
+ private boolean checkByte(char b, char value) {
+ if (b != value) {
+ onException(new Exception(value + " was expeceted, got " + (char)b));
+ return false;
+ }
+ return true;
+ }
+
+ private boolean checkLF(char b) {
+ return checkByte(b, '\n');
+ }
+
+ private boolean checkCR(char b) {
+ return checkByte(b, '\r');
+ }
+
+ @Override
+ public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) {
+ try {
+ while (bb.remaining() > 0) {
+ switch (mState) {
+ case CHUNK_LEN:
+ char c = bb.getByteChar();
+ if (c == '\r') {
+ mState = State.CHUNK_LEN_CR;
+ }
+ else {
+ mChunkLength *= 16;
+ if (c >= 'a' && c <= 'f')
+ mChunkLength += (c - 'a' + 10);
+ else if (c >= '0' && c <= '9')
+ mChunkLength += c - '0';
+ else if (c >= 'A' && c <= 'F')
+ mChunkLength += (c - 'A' + 10);
+ else {
+ onException(new Exception("invalid chunk length: " + c));
+ return;
+ }
+ }
+ mChunkLengthRemaining = mChunkLength;
+ break;
+ case CHUNK_LEN_CR:
+ if (!checkLF(bb.getByteChar()))
+ return;
+ mState = State.CHUNK;
+ break;
+ case CHUNK:
+ int remaining = bb.remaining();
+ int reading = Math.min(mChunkLengthRemaining, remaining);
+ mChunkLengthRemaining -= reading;
+ if (mChunkLengthRemaining == 0) {
+ mState = State.CHUNK_CR;
+ }
+ if (reading == 0)
+ break;
+ ByteBufferList chunk = bb.get(reading);
+ int newRemaining = bb.remaining();
+ Assert.assertEquals(remaining, chunk.remaining() + bb.remaining());
+ Assert.assertEquals(reading, chunk.remaining());
+ Util.emitAllData(this, chunk);
+ Assert.assertEquals(newRemaining, bb.remaining());
+ break;
+ case CHUNK_CR:
+ if (!checkCR(bb.getByteChar()))
+ return;
+ mState = State.CHUNK_CRLF;
+ break;
+ case CHUNK_CRLF:
+ if (!checkLF(bb.getByteChar()))
+ return;
+ if (mChunkLength > 0) {
+ mState = State.CHUNK_LEN;
+
+ }
+ else {
+ mState = State.COMPLETE;
+ onCompleted(null);
+ }
+ mChunkLength = 0;
+ break;
+ case COMPLETE:
+ Exception fail = new Exception("Continued receiving data after chunk complete");
+ onException(fail);
+ onCompleted(fail);
+ return;
+ }
+ }
+ }
+ catch (Exception ex) {
+ onException(ex);
+ }
+ }
+}
diff --git a/AndroidAsync/src/com/koushikdutta/async/http/transform/GZIPTransformer.java b/AndroidAsync/src/com/koushikdutta/async/http/transform/GZIPTransformer.java
new file mode 100644
index 0000000..d07c808
--- /dev/null
+++ b/AndroidAsync/src/com/koushikdutta/async/http/transform/GZIPTransformer.java
@@ -0,0 +1,133 @@
+package com.koushikdutta.async.http.transform;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.zip.CRC32;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.Inflater;
+
+import com.koushikdutta.async.ByteBufferList;
+import com.koushikdutta.async.DataEmitter;
+import com.koushikdutta.async.DataEmitterStream;
+import com.koushikdutta.async.PushParser;
+import com.koushikdutta.async.TapCallback;
+import com.koushikdutta.async.callback.DataCallback;
+import com.koushikdutta.async.http.libcore.Memory;
+
+public abstract class GZIPTransformer extends InflaterTransformer {
+ private static final int FCOMMENT = 16;
+
+ private static final int FEXTRA = 4;
+
+ private static final int FHCRC = 2;
+
+ private static final int FNAME = 8;
+
+
+
+ public GZIPTransformer() {
+ super(new Inflater(true));
+ }
+
+ boolean mNeedsHeader = true;
+ protected CRC32 crc = new CRC32();
+
+ public static int unsignedToBytes(byte b) {
+ return b & 0xFF;
+ }
+
+ DataEmitterStream mHeaderParser;
+ @Override
+ public void onDataAvailable(final DataEmitter emitter, ByteBufferList bb) {
+ if (mNeedsHeader) {
+ final PushParser parser = new PushParser(emitter);
+ parser
+ .readBuffer(10)
+ .tap(new TapCallback() {
+ int flags;
+ boolean hcrc;
+ public void tap(byte[] header) {
+ short magic = Memory.peekShort(header, 0, ByteOrder.LITTLE_ENDIAN);
+ if (magic != (short) GZIPInputStream.GZIP_MAGIC) {
+ onException(new IOException(String.format("unknown format (magic number %x)", magic)));
+ return;
+ }
+ flags = header[3];
+ hcrc = (flags & FHCRC) != 0;
+ if (hcrc) {
+ crc.update(header, 0, header.length);
+ }
+ if ((flags & FEXTRA) != 0) {
+ parser
+ .readBuffer(2)
+ .tap(new TapCallback() {
+ public void tap(byte[] header) {
+ if (hcrc) {
+ crc.update(header, 0, 2);
+ }
+ int length = Memory.peekShort(header, 0, ByteOrder.LITTLE_ENDIAN) & 0xffff;
+ parser
+ .readBuffer(length)
+ .tap(new TapCallback() {
+ public void tap(byte[] buf) {
+ if (hcrc) {
+ crc.update(buf, 0, buf.length);
+ }
+ next();
+ }
+ });
+ }
+ });
+ }
+
+ next();
+ }
+ public void next() {
+ PushParser parser = new PushParser(emitter);
+ DataCallback summer = new DataCallback() {
+ @Override
+ public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) {
+ if (hcrc) {
+ while (bb.size() > 0) {
+ ByteBuffer b = bb.remove();
+ crc.update(b.array(), b.arrayOffset() + b.position(), b.remaining());
+ }
+ }
+ }
+ };
+ if ((flags & FNAME) != 0) {
+ parser.until((byte)0, summer);
+ }
+ if ((flags & FCOMMENT) != 0) {
+ parser.until((byte)0, summer);
+ }
+ if (hcrc) {
+ parser.readBuffer(2);
+ }
+ else {
+ parser.noop();
+ }
+ parser.tap(new TapCallback() {
+ public void tap(byte[] header) {
+ if (header != null) {
+ short crc16 = Memory.peekShort(header, 0, ByteOrder.LITTLE_ENDIAN);
+ if ((short) crc.getValue() != crc16) {
+ onException(new IOException("CRC mismatch"));
+ return;
+ }
+ crc.reset();
+ }
+ mNeedsHeader = false;
+ emitter.setDataCallback(GZIPTransformer.this);
+ }
+ });
+ }
+ });
+ }
+ else {
+ super.onDataAvailable(emitter, bb);
+ }
+ }
+}
diff --git a/AndroidAsync/src/com/koushikdutta/async/http/transform/InflaterTransformer.java b/AndroidAsync/src/com/koushikdutta/async/http/transform/InflaterTransformer.java
new file mode 100644
index 0000000..610bc16
--- /dev/null
+++ b/AndroidAsync/src/com/koushikdutta/async/http/transform/InflaterTransformer.java
@@ -0,0 +1,62 @@
+package com.koushikdutta.async.http.transform;
+
+import java.nio.ByteBuffer;
+import java.util.zip.Inflater;
+
+import junit.framework.Assert;
+
+import com.koushikdutta.async.ByteBufferList;
+import com.koushikdutta.async.DataEmitter;
+import com.koushikdutta.async.DataTransformerBase;
+import com.koushikdutta.async.Util;
+
+public abstract class InflaterTransformer extends DataTransformerBase {
+ private Inflater mInflater;
+
+ @Override
+ public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) {
+ try {
+ ByteBufferList transformed = new ByteBufferList();
+ ByteBuffer output = ByteBuffer.allocate(bb.remaining() * 2);
+ int totalInflated = 0;
+ int totalRead = 0;
+ while (bb.size() > 0) {
+ ByteBuffer b = bb.remove();
+ if (b.hasRemaining()) {
+ totalRead =+ b.remaining();
+ mInflater.setInput(b.array(), b.arrayOffset() + b.position(), b.remaining());
+ do {
+ int inflated = mInflater.inflate(output.array(), output.arrayOffset() + output.position(), output.remaining());
+ totalInflated += inflated;
+ output.position(output.position() + inflated);
+ if (!output.hasRemaining()) {
+ output.limit(output.position());
+ output.position(0);
+ transformed.add(output);
+ Assert.assertNotSame(totalRead, 0);
+ int newSize = output.capacity() * 2;
+ output = ByteBuffer.allocate(newSize);
+ }
+ }
+ while (!mInflater.needsInput() && !mInflater.finished());
+ }
+ }
+ output.limit(output.position());
+ output.position(0);
+ transformed.add(output);
+
+ Util.emitAllData(this, transformed);
+ }
+ catch (Exception ex) {
+ onException(ex);
+ }
+ }
+
+ public InflaterTransformer() {
+ this(new Inflater());
+ }
+
+ public InflaterTransformer(Inflater inflater) {
+ mInflater = inflater;
+ }
+}
diff --git a/AndroidAsync/src/com/koushikdutta/async/stream/OutputStreamDataCallback.java b/AndroidAsync/src/com/koushikdutta/async/stream/OutputStreamDataCallback.java
new file mode 100644
index 0000000..d6f5747
--- /dev/null
+++ b/AndroidAsync/src/com/koushikdutta/async/stream/OutputStreamDataCallback.java
@@ -0,0 +1,44 @@
+package com.koushikdutta.async.stream;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+
+import com.koushikdutta.async.ByteBufferList;
+import com.koushikdutta.async.DataEmitter;
+import com.koushikdutta.async.ExceptionCallback;
+import com.koushikdutta.async.callback.DataCallback;
+
+public class OutputStreamDataCallback implements DataCallback, ExceptionCallback {
+ private OutputStream mOutput;
+ public OutputStreamDataCallback(OutputStream os) {
+ mOutput = os;
+ }
+
+ @Override
+ public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) {
+ try {
+ for (ByteBuffer b: bb) {
+ mOutput.write(b.array(), b.arrayOffset() + b.position(), b.remaining());
+ }
+ }
+ catch (Exception ex) {
+ onException(ex);
+ }
+ bb.clear();
+ }
+
+ public void close() {
+ try {
+ mOutput.close();
+ }
+ catch (IOException e) {
+ onException(e);
+ }
+ }
+
+ @Override
+ public void onException(Exception error) {
+ error.printStackTrace();
+ }
+}
diff --git a/AndroidAsync/src/com/koushikdutta/test/TestActivity.java b/AndroidAsync/src/com/koushikdutta/test/TestActivity.java
new file mode 100644
index 0000000..a5b20a4
--- /dev/null
+++ b/AndroidAsync/src/com/koushikdutta/test/TestActivity.java
@@ -0,0 +1,315 @@
+package com.koushikdutta.test;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.nio.ByteBuffer;
+import java.util.Enumeration;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.os.Bundle;
+import android.os.StrictMode;
+import android.os.StrictMode.ThreadPolicy;
+import android.os.StrictMode.ThreadPolicy.Builder;
+import android.util.Log;
+
+import com.koushikdutta.async.AsyncServer;
+import com.koushikdutta.async.AsyncSocket;
+import com.koushikdutta.async.BufferedDataSink;
+import com.koushikdutta.async.ByteBufferList;
+import com.koushikdutta.async.DataEmitter;
+import com.koushikdutta.async.PushParser;
+import com.koushikdutta.async.TapCallback;
+import com.koushikdutta.async.callback.ConnectCallback;
+import com.koushikdutta.async.callback.ListenCallback;
+import com.koushikdutta.async.callback.CompletedCallback;
+import com.koushikdutta.async.callback.DataCallback;
+import com.koushikdutta.async.http.AsyncHttpClient;
+import com.koushikdutta.async.http.AsyncHttpClient.StringCallback;
+import com.koushikdutta.async.http.AsyncHttpGet;
+import com.koushikdutta.async.http.AsyncHttpResponse;
+import com.koushikdutta.async.http.callback.HttpConnectCallback;
+
+@SuppressLint("NewApi")
+public class TestActivity extends Activity {
+ ByteBuffer addBytes(ByteBuffer o, byte[] bytes) {
+ if (o.remaining() < bytes.length) {
+ ByteBuffer n = ByteBuffer.allocate(o.capacity() * 2 + bytes.length);
+ n.mark();
+ o.limit(o.position());
+ o.reset();
+ n.put(o);
+ o = n;
+ }
+ o.put(bytes);
+ return o;
+ }
+
+ static final String TAG = "TEST";
+ public String getLocalIpAddress() {
+ try {
+ for (Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements();) {
+ NetworkInterface intf = en.nextElement();
+ if (intf.getDisplayName().contains("p2p"))
+ continue;
+ for (Enumeration<InetAddress> enumIpAddr = intf.getInetAddresses(); enumIpAddr.hasMoreElements();) {
+ InetAddress inetAddress = enumIpAddr.nextElement();
+ if (!inetAddress.isLoopbackAddress() && inetAddress instanceof Inet4Address) {
+ return inetAddress.getHostAddress();
+ }
+ }
+ }
+ } catch (SocketException ex) {
+ Log.e(TAG, ex.toString());
+ }
+ return null;
+ }
+
+ /** Called when the activity is first created. */
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+// try {
+// ByteArrayOutputStream bout = new ByteArrayOutputStream();
+// DataOutputStream os = new DataOutputStream(new GZIPOutputStream(bout));
+// os.write("hello asidj aiosjd oiasjd oiasj doas doasj doijq35r90u83qasoidj oaisjdoi asdiohas ihds ".getBytes());
+// os.flush();
+// os.close();
+// byte[] bytes = bout.toByteArray();
+//
+// GZIPTransformer t = new GZIPTransformer() {
+// @Override
+// public void onException(Exception error) {
+// error.printStackTrace();
+// }
+// };
+//
+// t.setDataCallback(new DataCallback() {
+// @Override
+// public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) {
+// bb.spewString();
+// bb.clear();
+// }
+// });
+//
+// ByteBuffer b = ByteBuffer.wrap(bytes);
+// ByteBufferList bb = new ByteBufferList();
+// bb.add(b);
+// DataEmitter dummy = new DataEmitter() {
+// DataCallback cb;
+// @Override
+// public void setDataCallback(DataCallback callback) {
+// cb = callback;
+// }
+//
+// @Override
+// public boolean isChunked() {
+// return false;
+// }
+//
+// @Override
+// public DataCallback getDataCallback() {
+// return cb;
+// }
+// };
+// dummy.setDataCallback(t);
+// Util.emitAllData(dummy, bb);
+// System.out.println("done");
+// }
+// catch (Exception ex) {
+// ex.printStackTrace();
+// }
+//
+// if (true)
+// return;
+
+ ThreadPolicy.Builder b = new Builder();
+ StrictMode.setThreadPolicy(b.permitAll().build());
+
+
+ final String host = "builder.clockworkmod.com";
+ final int port = 443;
+
+ final String shit = "GET / HTTP/1.1\n"
+ + "User-Agent: curl/7.25.0 (x86_64-apple-darwin11.3.0) libcurl/7.25.0 OpenSSL/1.0.1c zlib/1.2.7 libidn/1.22\n"
+ + String.format("Host: %s\n", host)
+ + "Accept: */*\n" + "\n\n";
+
+// final AsyncServer server = new AsyncServer();
+ try {
+// server.initialize();
+//
+// new Thread() {
+// public void run() {
+// server.run();
+// };
+// }.start();
+
+
+ /*
+ // sending datagram to localhost that is unreceived causes the nio selector to spin...
+ // I assume because the kernel does some hackery with piping localhost traffic.
+ // use an explicit ip address of a network interface.
+ final AsyncSocket dg = server.connectDatagram(new InetSocketAddress(getLocalIpAddress(), 50000));
+ dg.setDataCallback(new DataCallback() {
+ @Override
+ public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) {
+ dg.write(bb);
+ System.out.println("pong");
+ }
+ });
+
+ new Thread() {
+ public void run() {
+ try {
+ final DatagramSocket s = new DatagramSocket(50000);
+// BufferedDataSink sink = new BufferedDataSink(dg);
+ dg.write(ByteBuffer.wrap(new byte[1000]));
+
+ DatagramPacket p = new DatagramPacket(new byte[2000], 2000);
+ while (true) {
+ s.receive(p);
+ System.out.println("ping");
+ Thread.sleep(5000);
+ s.send(p);
+ // s.send(p);
+ }
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }.start();
+
+ if (true)
+ return;
+ */
+
+ for (int i = 0; i < 5; i++) {
+ AsyncHttpClient.download("http://builder.clockworkmod.com", new StringCallback() {
+ @Override
+ public void onCompleted(Exception e, String result) {
+ if (e != null) {
+ e.printStackTrace();
+ }
+ System.out.println(result);
+ }
+ });
+ Thread.sleep(5000);
+ }
+
+ if (true)
+ return;
+
+ for (int i = 0; i < 4; i++) {
+// AsyncHttpGet get = new AsyncHttpGet("https://soighoisdfoihsiohsdf.com");
+// AsyncHttpGet get = new AsyncHttpGet("http://www.cnn.com");
+ AsyncHttpGet get = new AsyncHttpGet("https://builder.clockworkmod.com");
+ AsyncHttpClient.connect(get, new HttpConnectCallback() {
+ @Override
+ public void onConnectCompleted(Exception ex, AsyncHttpResponse response) {
+ if (ex != null) {
+ ex.printStackTrace();
+ return;
+ }
+ response.setDataCallback(new DataCallback() {
+ @Override
+ public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) {
+ System.out.println(bb.remaining());
+ bb.clear();
+ }
+ });
+ response.setCompletedCallback(new CompletedCallback() {
+ @Override
+ public void onCompleted(Exception ex) {
+ System.out.println("done!");
+ if (ex != null) {
+ System.out.println("Errors:");
+ ex.printStackTrace();
+ }
+ }
+ });
+ }
+ });
+ }
+
+ if (true)
+ return;
+
+ AsyncServer.getDefault().listen(InetAddress.getLocalHost(), 50001, new ListenCallback() {
+ @Override
+ public void onAccepted(final AsyncSocket handler) {
+ final DataCallback dh = new DataCallback() {
+ @Override
+ public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) {
+ bb.clear();
+ System.out.println(bb.remaining());
+ }
+ };
+
+ final PushParser pp = new PushParser(handler);
+ pp
+ .readBuffer(100000)
+ .tap(new TapCallback() {
+ public void tap(byte[] buffer) {
+ System.out.println(buffer.length);
+
+ pp
+ .readBuffer(15000)
+ .tap(new TapCallback() {
+ public void tap(byte[] buffer) {
+ System.out.println(buffer.length);
+
+ handler.setDataCallback(dh);
+ }
+ });
+
+ }
+ });
+ }
+ });
+
+ AsyncServer.getDefault().connectSocket(new InetSocketAddress(InetAddress.getLocalHost(), 50001), new ConnectCallback() {
+ @Override
+ public void onConnectCompleted(Exception ex, final AsyncSocket socket) {
+ if (ex != null) {
+ System.out.println("connect fail");
+ return;
+ }
+
+ new Thread() {
+ @Override
+ public void run() {
+ try {
+ BufferedDataSink ds = new BufferedDataSink(socket);
+ ByteBuffer b = ByteBuffer.allocate(30000);
+ ds.write(b);
+ Thread.sleep(1000);
+ b = ByteBuffer.allocate(30000);
+ ds.write(b);
+ Thread.sleep(1000);
+ b = ByteBuffer.allocate(30000);
+ ds.write(b);
+ Thread.sleep(1000);
+ b = ByteBuffer.allocate(30000);
+ ds.write(b);
+ }
+ catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ }.start();
+ }
+ });
+
+
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+} \ No newline at end of file