Repository: koush/AndroidAsync Branch: master Commit: 12f9e20443db Files: 244 Total size: 769.9 KB Directory structure: gitextract_o_k792rl/ ├── .github/ │ └── FUNDING.yml ├── .gitignore ├── AndroidAsync/ │ ├── .classpath │ ├── Android.mk │ ├── AndroidManifest.xml │ ├── build.gradle │ ├── lint.xml │ ├── maven.gradle │ ├── proguard-project.txt │ ├── project.properties │ ├── res/ │ │ └── .gitignore │ ├── src/ │ │ └── com/ │ │ └── koushikdutta/ │ │ └── async/ │ │ ├── AsyncDatagramSocket.java │ │ ├── AsyncNetworkSocket.java │ │ ├── AsyncSSLException.java │ │ ├── AsyncSSLServerSocket.java │ │ ├── AsyncSSLSocket.java │ │ ├── AsyncSSLSocketWrapper.java │ │ ├── AsyncSemaphore.java │ │ ├── AsyncServer.java │ │ ├── AsyncServerSocket.java │ │ ├── AsyncSocket.java │ │ ├── BufferedDataSink.java │ │ ├── ByteBufferList.java │ │ ├── ChannelWrapper.java │ │ ├── DataEmitter.java │ │ ├── DataEmitterBase.java │ │ ├── DataEmitterReader.java │ │ ├── DataSink.java │ │ ├── DataTrackingEmitter.java │ │ ├── DatagramChannelWrapper.java │ │ ├── FileDataEmitter.java │ │ ├── FilteredDataEmitter.java │ │ ├── FilteredDataSink.java │ │ ├── HostnameResolutionException.java │ │ ├── LineEmitter.java │ │ ├── PushParser.java │ │ ├── SelectorWrapper.java │ │ ├── ServerSocketChannelWrapper.java │ │ ├── SocketChannelWrapper.java │ │ ├── TapCallback.java │ │ ├── ThreadQueue.java │ │ ├── Util.java │ │ ├── ZipDataSink.java │ │ ├── callback/ │ │ │ ├── CompletedCallback.java │ │ │ ├── ConnectCallback.java │ │ │ ├── ContinuationCallback.java │ │ │ ├── DataCallback.java │ │ │ ├── ListenCallback.java │ │ │ ├── ResultCallback.java │ │ │ ├── SocketCreateCallback.java │ │ │ ├── ValueCallback.java │ │ │ ├── ValueFunction.java │ │ │ └── WritableCallback.java │ │ ├── dns/ │ │ │ ├── Dns.java │ │ │ └── DnsResponse.java │ │ ├── future/ │ │ │ ├── Cancellable.java │ │ │ ├── Continuation.java │ │ │ ├── Converter.java │ │ │ ├── DependentCancellable.java │ │ │ ├── DependentFuture.java │ │ │ ├── DoneCallback.java │ │ │ ├── FailCallback.java │ │ │ ├── FailConvertCallback.java │ │ │ ├── FailRecoverCallback.java │ │ │ ├── Future.java │ │ │ ├── FutureCallback.java │ │ │ ├── FutureRunnable.java │ │ │ ├── FutureThread.java │ │ │ ├── Futures.java │ │ │ ├── HandlerFuture.java │ │ │ ├── MultiFuture.java │ │ │ ├── MultiTransformFuture.java │ │ │ ├── SimpleCancellable.java │ │ │ ├── SimpleFuture.java │ │ │ ├── SuccessCallback.java │ │ │ ├── ThenCallback.java │ │ │ ├── ThenFutureCallback.java │ │ │ ├── TransformFuture.java │ │ │ └── TypeConverter.java │ │ ├── http/ │ │ │ ├── AsyncHttpClient.java │ │ │ ├── AsyncHttpClientMiddleware.java │ │ │ ├── AsyncHttpDelete.java │ │ │ ├── AsyncHttpGet.java │ │ │ ├── AsyncHttpHead.java │ │ │ ├── AsyncHttpPost.java │ │ │ ├── AsyncHttpPut.java │ │ │ ├── AsyncHttpRequest.java │ │ │ ├── AsyncHttpResponse.java │ │ │ ├── AsyncHttpResponseImpl.java │ │ │ ├── AsyncSSLEngineConfigurator.java │ │ │ ├── AsyncSSLSocketMiddleware.java │ │ │ ├── AsyncSocketMiddleware.java │ │ │ ├── BasicNameValuePair.java │ │ │ ├── BodyDecoderException.java │ │ │ ├── ConnectionClosedException.java │ │ │ ├── ConnectionFailedException.java │ │ │ ├── Headers.java │ │ │ ├── HttpDate.java │ │ │ ├── HttpTransportMiddleware.java │ │ │ ├── HttpUtil.java │ │ │ ├── HybiParser.java │ │ │ ├── Multimap.java │ │ │ ├── NameValuePair.java │ │ │ ├── Protocol.java │ │ │ ├── ProtocolVersion.java │ │ │ ├── RedirectLimitExceededException.java │ │ │ ├── RequestLine.java │ │ │ ├── SSLEngineSNIConfigurator.java │ │ │ ├── SimpleMiddleware.java │ │ │ ├── WebSocket.java │ │ │ ├── WebSocketHandshakeException.java │ │ │ ├── WebSocketImpl.java │ │ │ ├── body/ │ │ │ │ ├── AsyncHttpRequestBody.java │ │ │ │ ├── ByteBufferListRequestBody.java │ │ │ │ ├── DocumentBody.java │ │ │ │ ├── FileBody.java │ │ │ │ ├── FilePart.java │ │ │ │ ├── JSONArrayBody.java │ │ │ │ ├── JSONObjectBody.java │ │ │ │ ├── MultipartFormDataBody.java │ │ │ │ ├── Part.java │ │ │ │ ├── StreamBody.java │ │ │ │ ├── StreamPart.java │ │ │ │ ├── StringBody.java │ │ │ │ ├── StringPart.java │ │ │ │ └── UrlEncodedFormBody.java │ │ │ ├── cache/ │ │ │ │ ├── HeaderParser.java │ │ │ │ ├── Objects.java │ │ │ │ ├── RawHeaders.java │ │ │ │ ├── RequestHeaders.java │ │ │ │ ├── ResponseCacheMiddleware.java │ │ │ │ ├── ResponseHeaders.java │ │ │ │ ├── ResponseSource.java │ │ │ │ └── StrictLineReader.java │ │ │ ├── callback/ │ │ │ │ ├── HttpConnectCallback.java │ │ │ │ └── RequestCallback.java │ │ │ ├── filter/ │ │ │ │ ├── ChunkedDataException.java │ │ │ │ ├── ChunkedInputFilter.java │ │ │ │ ├── ChunkedOutputFilter.java │ │ │ │ ├── ContentLengthFilter.java │ │ │ │ ├── DataRemainingException.java │ │ │ │ ├── GZIPInputFilter.java │ │ │ │ ├── InflaterInputFilter.java │ │ │ │ └── PrematureDataEndException.java │ │ │ └── server/ │ │ │ ├── AsyncHttpRequestBodyProvider.java │ │ │ ├── AsyncHttpServer.java │ │ │ ├── AsyncHttpServerRequest.java │ │ │ ├── AsyncHttpServerRequestImpl.java │ │ │ ├── AsyncHttpServerResponse.java │ │ │ ├── AsyncHttpServerResponseImpl.java │ │ │ ├── AsyncHttpServerRouter.java │ │ │ ├── AsyncProxyServer.java │ │ │ ├── BoundaryEmitter.java │ │ │ ├── HttpServerRequestCallback.java │ │ │ ├── MalformedRangeException.java │ │ │ ├── MimeEncodingException.java │ │ │ ├── RouteMatcher.java │ │ │ ├── StreamSkipException.java │ │ │ └── UnknownRequestBody.java │ │ ├── parser/ │ │ │ ├── AsyncParser.java │ │ │ ├── ByteBufferListParser.java │ │ │ ├── DocumentParser.java │ │ │ ├── JSONArrayParser.java │ │ │ ├── JSONObjectParser.java │ │ │ └── StringParser.java │ │ ├── stream/ │ │ │ ├── ByteBufferListInputStream.java │ │ │ ├── FileDataSink.java │ │ │ ├── InputStreamDataEmitter.java │ │ │ ├── OutputStreamDataCallback.java │ │ │ └── OutputStreamDataSink.java │ │ ├── util/ │ │ │ ├── Allocator.java │ │ │ ├── ArrayDeque.java │ │ │ ├── Charsets.java │ │ │ ├── Deque.java │ │ │ ├── FileCache.java │ │ │ ├── FileUtility.java │ │ │ ├── HashList.java │ │ │ ├── IdleTimeout.java │ │ │ ├── LruCache.java │ │ │ ├── StreamUtility.java │ │ │ ├── TaggedList.java │ │ │ ├── ThrottleTimeout.java │ │ │ ├── TimeoutBase.java │ │ │ └── UntypedHashtable.java │ │ └── wrapper/ │ │ ├── AsyncSocketWrapper.java │ │ └── DataEmitterWrapper.java │ └── test/ │ ├── assets/ │ │ ├── 6691924d7d24237d3b3679310157d640 │ │ ├── hello.txt │ │ └── test.json │ ├── res/ │ │ ├── raw/ │ │ │ └── keystore.bks │ │ └── values/ │ │ └── strings.xml │ └── src/ │ └── com/ │ └── koushikdutta/ │ └── async/ │ └── test/ │ ├── BodyTests.java │ ├── ByteUtilTests.java │ ├── CacheTests.java │ ├── CallbackTests.java │ ├── ConscryptTests.java │ ├── ConvertTests.java │ ├── DnsTests.java │ ├── FileCacheTests.java │ ├── FileTests.java │ ├── FutureTests.java │ ├── HttpClientTests.java │ ├── HttpServerTests.java │ ├── Issue59.java │ ├── IssueWithWebSocketFuturesTests.java │ ├── LineEmitterTests.java │ ├── Md5.java │ ├── MultipartTests.java │ ├── ParserTests.java │ ├── ProxyTests.java │ ├── RedirectTests.java │ ├── SSLTests.java │ ├── SanityChecks.java │ ├── TimeoutTests.java │ ├── TriggerFuture.java │ └── WebSocketTests.java ├── AndroidAsync-Kotlin/ │ ├── .gitignore │ ├── README.md │ ├── build.gradle │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src/ │ ├── androidTest/ │ │ └── java/ │ │ └── com/ │ │ └── koushikdutta/ │ │ └── async/ │ │ └── kotlin/ │ │ └── ExampleInstrumentedTest.kt │ ├── main/ │ │ ├── AndroidManifest.xml │ │ └── java/ │ │ └── com/ │ │ └── koushikdutta/ │ │ └── async/ │ │ └── kotlin/ │ │ └── FutureExtensions.kt │ └── test/ │ └── java/ │ └── com/ │ └── koushikdutta/ │ └── async/ │ └── kotlin/ │ └── ExampleUnitTest.kt ├── AndroidAsyncSample/ │ ├── AndroidManifest.xml │ ├── build.gradle │ ├── proguard-project.txt │ ├── project.properties │ ├── res/ │ │ ├── layout/ │ │ │ └── activity_main.xml │ │ ├── menu/ │ │ │ └── activity_main.xml │ │ ├── values/ │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ ├── values-v11/ │ │ │ └── styles.xml │ │ └── values-v14/ │ │ └── styles.xml │ └── src/ │ └── com/ │ └── koushikdutta/ │ └── async/ │ └── sample/ │ ├── MainActivity.java │ └── middleware/ │ ├── BasicAuthMiddleware.java │ └── CacheOverrideMiddleware.java ├── LICENSE ├── README.md ├── build.gradle ├── gradlew ├── gradlew.bat └── settings.gradle ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: koush patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] ================================================ FILE: .gitignore ================================================ bin .settings local.properties gen .gradle build .idea/ .DS_Store okhttp/ okio/ libs *.iml ================================================ FILE: AndroidAsync/.classpath ================================================ ================================================ FILE: AndroidAsync/Android.mk ================================================ # # 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. # LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := AndroidAsync LOCAL_SDK_VERSION := 8 LOCAL_SRC_FILES := $(call all-java-files-under, src) include $(BUILD_STATIC_JAVA_LIBRARY) ================================================ FILE: AndroidAsync/AndroidManifest.xml ================================================ ================================================ FILE: AndroidAsync/build.gradle ================================================ apply plugin: 'com.android.library' android { sourceSets { main { manifest.srcFile 'AndroidManifest.xml' jniLibs.srcDirs = ['libs/'] java.srcDirs=['src/' // , '../conscrypt/' // , '../compat/' ] } androidTest.java.srcDirs=['test/src/'] androidTest.res.srcDirs=['test/res/'] androidTest.assets.srcDirs=['test/assets/'] } lintOptions { abortOnError false } defaultConfig { targetSdkVersion 30 minSdkVersion 21 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } compileOptions { sourceCompatibility 1.8 targetCompatibility 1.8 } compileSdkVersion 30 buildToolsVersion '30.0.2' dependencies { // this is only necessary to get compilation working for self signed certificates. dependency isn't added. compileOnly group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.60' compileOnly group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.60' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' // for when i wanna test this against gms conscrypt androidTestImplementation 'com.google.android.gms:play-services-base:17.0.0' } } apply from: 'maven.gradle' ================================================ FILE: AndroidAsync/lint.xml ================================================ ================================================ FILE: AndroidAsync/maven.gradle ================================================ // Setup // 0) Setup your sonatype credentials by editing/creating ~/.gradle/gradle.properties and enter: // signing.keyId= // signing.password= // signing.secretKeyRingFile= // sonatypeUsername= // sonatypePassword= // 1) Setup your build.gradle for your android project and add this one line of code which imports this gist: // apply from: 'https://raw.github.com/koush/mvn-repo/master/maven.gradle' // 2) gradle clean && gradle build && gradle uploadArchives // 3) That's it! apply plugin: 'maven' apply plugin: 'signing' if (hasProperty('sonatypeUsername') && hasProperty('sonatypePassword') && hasProperty('githubToken')) { afterEvaluate { project -> String user = null String repo = null 'git remote -v'.execute(null, project.projectDir).getText().find('.*?git@github.com/(.*?)/(.*?) .*?') { match -> user = match[1] repo = match[2] } String githubUrl = 'https://api.github.com/repos/' + user + '/' + repo if (System.getenv().GITHUB_TOKEN) githubUrl += '?access_token=' + System.getenv().GITHUB_TOKEN def repoInfo = new groovy.json.JsonSlurper().parseText(new URL(githubUrl).getText()) def android_manifest try { android_manifest = new XmlParser(false, false).parseText(new File(project.projectDir, 'AndroidManifest.xml').getText()) } catch (e) { android_manifest = new XmlParser(false, false).parseText(new File(project.projectDir, 'src/main/AndroidManifest.xml').getText()) } def versionName = android_manifest.'@android:versionName' def package_name = android_manifest.'@package' def artifact_id = project.projectDir.getName().toLowerCase() project.version = versionName project.group = package_name uploadArchives { repositories { mavenDeployer { beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } pom.groupId = package_name pom.artifactId = artifact_id pom.version = versionName repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") { authentication(userName: sonatypeUsername, password: sonatypePassword) } pom.project { name repo packaging 'jar' description repoInfo.description url repoInfo.html_url scm { url repoInfo.git_url connection repoInfo.git_url developerConnection repoInfo.ssh_url } licenses { license { name 'The Apache Software License, Version 2.0' url 'http://www.apache.org/licenses/LICENSE-2.0.txt' distribution 'repo' } } developers { developer { id user name user } } } } } } signing { sign configurations.archives } task androidJavadocs(type: Javadoc) { source = android.sourceSets.main.java.srcDirs } task androidJavadocsJar(type: Jar) { classifier = 'javadoc' baseName = artifact_id from androidJavadocs.destinationDir } task androidSourcesJar(type: Jar) { classifier = 'sources' baseName = artifact_id from android.sourceSets.main.java.srcDirs } artifacts { // archives packageReleaseJar archives androidSourcesJar archives androidJavadocsJar } } } ================================================ FILE: AndroidAsync/proguard-project.txt ================================================ # To enable ProGuard in your project, edit project.properties # to define the proguard.config property as described in that file. # # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in ${sdk.dir}/tools/proguard/proguard-android.txt # You can edit the include path and order by changing the ProGuard # include property in project.properties. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} -keep class * extends com.koushikdutta.async.TapCallback { public protected private *; } ================================================ FILE: AndroidAsync/project.properties ================================================ # This file is automatically generated by Android Tools. # Do not modify this file -- YOUR CHANGES WILL BE ERASED! # # This file must be checked in Version Control Systems. # # To customize properties used by the Ant build system edit # "ant.properties", and override values to adapt the script to your # project structure. # # To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt # Project target. target=android-19 android.library=true ================================================ FILE: AndroidAsync/res/.gitignore ================================================ ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/AsyncDatagramSocket.java ================================================ package com.koushikdutta.async; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; public class AsyncDatagramSocket extends AsyncNetworkSocket { public void disconnect() throws IOException { socketAddress = null; ((DatagramChannelWrapper)getChannel()).disconnect(); } @Override public InetSocketAddress getRemoteAddress() { if (isOpen()) return super.getRemoteAddress(); return ((DatagramChannelWrapper)getChannel()).getRemoteAddress(); } public void connect(InetSocketAddress address) throws IOException { socketAddress = address; ((DatagramChannelWrapper)getChannel()).mChannel.connect(address); } public void send(final String host, final int port, final ByteBuffer buffer) { if (getServer().getAffinity() != Thread.currentThread()) { getServer().run(new Runnable() { @Override public void run() { send(host, port, buffer); } }); return; } try { ((DatagramChannelWrapper)getChannel()).mChannel.send(buffer, new InetSocketAddress(host, port)); } catch (IOException e) { // close(); // reportEndPending(e); // reportClose(e); } } public void send(final InetSocketAddress address, final ByteBuffer buffer) { if (getServer().getAffinity() != Thread.currentThread()) { getServer().run(new Runnable() { @Override public void run() { send(address, buffer); } }); return; } try { int sent = ((DatagramChannelWrapper)getChannel()).mChannel.send(buffer, new InetSocketAddress(address.getHostName(), address.getPort())); } catch (IOException e) { // Log.e("SEND", "send error", e); // close(); // reportEndPending(e); // reportClose(e); } } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/AsyncNetworkSocket.java ================================================ package com.koushikdutta.async; import android.util.Log; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.DataCallback; import com.koushikdutta.async.callback.WritableCallback; import com.koushikdutta.async.util.Allocator; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; import java.nio.ByteBuffer; import java.nio.channels.CancelledKeyException; import java.nio.channels.DatagramChannel; import java.nio.channels.SelectionKey; import java.nio.channels.SocketChannel; public class AsyncNetworkSocket implements AsyncSocket { AsyncNetworkSocket() { } @Override public void end() { mChannel.shutdownOutput(); } public boolean isChunked() { return mChannel.isChunked(); } InetSocketAddress socketAddress; void attach(SocketChannel channel, InetSocketAddress socketAddress) throws IOException { this.socketAddress = socketAddress; allocator = new Allocator(); mChannel = new SocketChannelWrapper(channel); } void attach(DatagramChannel channel) throws IOException { mChannel = new DatagramChannelWrapper(channel); // keep udp at roughly the mtu, which is 1540 or something // letting it grow freaks out nio apparently. allocator = new Allocator(8192); } ChannelWrapper getChannel() { return mChannel; } public void onDataWritable() { if (!mChannel.isChunked()) { // turn write off mKey.interestOps(~SelectionKey.OP_WRITE & mKey.interestOps()); } if (mWriteableHandler != null) mWriteableHandler.onWriteable(); } private ChannelWrapper mChannel; private SelectionKey mKey; private AsyncServer mServer; void setup(AsyncServer server, SelectionKey key) { mServer = server; mKey = key; } @Override public void write(final ByteBufferList list) { if (mServer.getAffinity() != Thread.currentThread()) { mServer.run(new Runnable() { @Override public void run() { write(list); } }); return; } if (!mChannel.isConnected()) { return; } try { int before = list.remaining(); ByteBuffer[] arr = list.getAllArray(); mChannel.write(arr); list.addAll(arr); handleRemaining(list.remaining()); mServer.onDataSent(before - list.remaining()); } catch (IOException e) { closeInternal(); reportEndPending(e); reportClose(e); } } private void handleRemaining(int remaining) throws IOException { if (!mKey.isValid()) throw new IOException(new CancelledKeyException()); if (remaining > 0) { // chunked channels should not fail // register for a write notification if a write fails // turn write on mKey.interestOps(SelectionKey.OP_WRITE | mKey.interestOps()); } else { // turn write off mKey.interestOps(~SelectionKey.OP_WRITE & mKey.interestOps()); } } private ByteBufferList pending = new ByteBufferList(); // private ByteBuffer[] buffers = new ByteBuffer[8]; Allocator allocator; int onReadable() { spitPending(); // even if the socket is paused, // it may end up getting a queued readable event if it is // already in the selector's ready queue. if (mPaused) return 0; int total = 0; boolean closed = false; // ByteBufferList.obtainArray(buffers, Math.min(Math.max(mToAlloc, 2 << 11), maxAlloc)); ByteBuffer b = allocator.allocate(); // keep track of the max mount read during this read cycle // so we can be quicker about allocations during the next // time this socket reads. long read; try { read = mChannel.read(b); } catch (Exception e) { read = -1; closeInternal(); reportEndPending(e); reportClose(e); } if (read < 0) { closeInternal(); closed = true; } else { total += read; } if (read > 0) { allocator.track(read); b.flip(); // for (int i = 0; i < buffers.length; i++) { // ByteBuffer b = buffers[i]; // buffers[i] = null; // b.flip(); // pending.add(b); // } pending.add(b); Util.emitAllData(this, pending); } else { ByteBufferList.reclaim(b); } if (closed) { reportEndPending(null); reportClose(null); } return total; } boolean closeReported; protected void reportClose(Exception e) { if (closeReported) return; closeReported = true; if (mClosedHander != null) { mClosedHander.onCompleted(e); mClosedHander = null; } } @Override public void close() { closeInternal(); reportClose(null); } private void closeInternal() { 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; } CompletedCallback mClosedHander; @Override public void setClosedCallback(CompletedCallback handler) { mClosedHander = handler; } @Override public CompletedCallback getClosedCallback() { return mClosedHander; } @Override public WritableCallback getWriteableCallback() { return mWriteableHandler; } void reportEnd(Exception e) { if (mEndReported) return; mEndReported = true; if (mCompletedCallback != null) mCompletedCallback.onCompleted(e); else if (e != null) { Log.e("NIO", "Unhandled exception", e); } } boolean mEndReported; Exception mPendingEndException; void reportEndPending(Exception e) { if (pending.hasRemaining()) { mPendingEndException = e; return; } reportEnd(e); } private CompletedCallback mCompletedCallback; @Override public void setEndCallback(CompletedCallback callback) { mCompletedCallback = callback; } @Override public CompletedCallback getEndCallback() { return mCompletedCallback; } @Override public boolean isOpen() { return mChannel.isConnected() && mKey.isValid(); } boolean mPaused = false; @Override public void pause() { if (mServer.getAffinity() != Thread.currentThread()) { mServer.run(new Runnable() { @Override public void run() { pause(); } }); return; } if (mPaused) return; mPaused = true; try { mKey.interestOps(~SelectionKey.OP_READ & mKey.interestOps()); } catch (Exception ex) { } } private void spitPending() { if (pending.hasRemaining()) { Util.emitAllData(this, pending); } } @Override public void resume() { if (mServer.getAffinity() != Thread.currentThread()) { mServer.run(new Runnable() { @Override public void run() { resume(); } }); return; } if (!mPaused) return; mPaused = false; try { mKey.interestOps(SelectionKey.OP_READ | mKey.interestOps()); } catch (Exception ex) { } spitPending(); if (!isOpen()) reportEndPending(mPendingEndException); } @Override public boolean isPaused() { return mPaused; } @Override public AsyncServer getServer() { return mServer; } public InetSocketAddress getRemoteAddress() { return socketAddress; } public InetAddress getLocalAddress() { return mChannel.getLocalAddress(); } public int getLocalPort() { return mChannel.getLocalPort(); } public Object getSocket() { return getChannel().getSocket(); } @Override public String charset() { return null; } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/AsyncSSLException.java ================================================ package com.koushikdutta.async; public class AsyncSSLException extends Exception { public AsyncSSLException(Throwable cause) { super("Peer not trusted by any of the system trust managers.", cause); } private boolean mIgnore = false; public void setIgnore(boolean ignore) { mIgnore = ignore; } public boolean getIgnore() { return mIgnore; } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/AsyncSSLServerSocket.java ================================================ package com.koushikdutta.async; import java.security.PrivateKey; import java.security.cert.Certificate; public interface AsyncSSLServerSocket extends AsyncServerSocket { PrivateKey getPrivateKey(); Certificate getCertificate(); } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocket.java ================================================ package com.koushikdutta.async; import java.security.cert.X509Certificate; import javax.net.ssl.SSLEngine; public interface AsyncSSLSocket extends AsyncSocket { public X509Certificate[] getPeerCertificates(); public SSLEngine getSSLEngine(); } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocketWrapper.java ================================================ package com.koushikdutta.async; import android.content.Context; import android.os.Build; import android.util.Base64; import android.util.Log; import android.util.Pair; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.ConnectCallback; import com.koushikdutta.async.callback.DataCallback; import com.koushikdutta.async.callback.ListenCallback; import com.koushikdutta.async.callback.WritableCallback; import com.koushikdutta.async.future.Cancellable; import com.koushikdutta.async.future.SimpleCancellable; import com.koushikdutta.async.http.SSLEngineSNIConfigurator; import com.koushikdutta.async.util.Allocator; import com.koushikdutta.async.util.StreamUtility; import com.koushikdutta.async.wrapper.AsyncSocketWrapper; import org.apache.http.conn.ssl.StrictHostnameVerifier; import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.asn1.x509.BasicConstraints; import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.operator.ContentSigner; import org.bouncycastle.operator.OperatorCreationException; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.math.BigInteger; import java.net.InetAddress; import java.nio.ByteBuffer; import java.security.GeneralSecurityException; import java.security.KeyFactory; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.KeyStore; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.Provider; import java.security.PublicKey; import java.security.Security; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.RSAPublicKeySpec; import java.security.spec.X509EncodedKeySpec; import java.util.Calendar; import java.util.Date; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.KeyManagerFactory; 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.SSLSession; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; public class AsyncSSLSocketWrapper implements AsyncSocketWrapper, AsyncSSLSocket { private static final String LOGTAG = "AsyncSSLSocketWrapper"; public interface HandshakeCallback { public void onHandshakeCompleted(Exception e, AsyncSSLSocket socket); } static SSLContext defaultSSLContext; static SSLContext trustAllSSLContext; static TrustManager[] trustAllManagers; static HostnameVerifier trustAllVerifier; AsyncSocket mSocket; BufferedDataSink mSink; boolean mUnwrapping; SSLEngine engine; boolean finishedHandshake; private int mPort; private String mHost; private boolean mWrapping; HostnameVerifier hostnameVerifier; HandshakeCallback handshakeCallback; X509Certificate[] peerCertificates; WritableCallback mWriteableCallback; DataCallback mDataCallback; TrustManager[] trustManagers; boolean clientMode; static { // following is the "trust the system" certs setup try { // critical extension 2.5.29.15 is implemented improperly prior to 4.0.3. // https://code.google.com/p/android/issues/detail?id=9307 // https://groups.google.com/forum/?fromgroups=#!topic/netty/UCfqPPk5O4s // certs that use this extension will throw in Cipher.java. // fallback is to use a custom SSLContext, and hack around the x509 extension. if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) throw new Exception(); defaultSSLContext = SSLContext.getInstance("Default"); } catch (Exception ex) { try { defaultSSLContext = SSLContext.getInstance("TLS"); TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() { public java.security.cert.X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) { } public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) { for (X509Certificate cert : certs) { if (cert != null && cert.getCriticalExtensionOIDs() != null) cert.getCriticalExtensionOIDs().remove("2.5.29.15"); } } } }; defaultSSLContext.init(null, trustAllCerts, null); } catch (Exception ex2) { ex.printStackTrace(); ex2.printStackTrace(); } } try { trustAllSSLContext = SSLContext.getInstance("TLS"); trustAllManagers = new TrustManager[] { new X509TrustManager() { public java.security.cert.X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) { } public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) { } } }; trustAllSSLContext.init(null, trustAllManagers, null); trustAllVerifier = (hostname, session) -> true; } catch (Exception ex2) { ex2.printStackTrace(); } } public static SSLContext getDefaultSSLContext() { return defaultSSLContext; } public static void handshake(AsyncSocket socket, String host, int port, SSLEngine sslEngine, TrustManager[] trustManagers, HostnameVerifier verifier, boolean clientMode, final HandshakeCallback callback) { AsyncSSLSocketWrapper wrapper = new AsyncSSLSocketWrapper(socket, host, port, sslEngine, trustManagers, verifier, clientMode); wrapper.handshakeCallback = callback; socket.setClosedCallback(new CompletedCallback() { @Override public void onCompleted(Exception ex) { if (ex != null) callback.onHandshakeCompleted(ex, null); else callback.onHandshakeCompleted(new SSLException("socket closed during handshake"), null); } }); try { wrapper.engine.beginHandshake(); wrapper.handleHandshakeStatus(wrapper.engine.getHandshakeStatus()); } catch (SSLException e) { wrapper.report(e); } } public static Cancellable connectSocket(AsyncServer server, String host, int port, ConnectCallback callback) { return connectSocket(server, host, port, false, callback); } public static Cancellable connectSocket(AsyncServer server, String host, int port, boolean trustAllCerts, ConnectCallback callback) { SimpleCancellable cancellable = new SimpleCancellable(); Cancellable connect = server.connectSocket(host, port, (ex, netSocket) -> { if (ex != null) { if (cancellable.setComplete()) callback.onConnectCompleted(ex, null); return; } handshake(netSocket, host, port, (trustAllCerts ? trustAllSSLContext : defaultSSLContext).createSSLEngine(host, port), trustAllCerts ? trustAllManagers : null, trustAllCerts ? trustAllVerifier : null, true, (e, socket) -> { if (!cancellable.setComplete()) { if (socket != null) socket.close(); return; } if (e != null) callback.onConnectCompleted(e, null); else callback.onConnectCompleted(null, socket); }); }); cancellable.setParent(connect); return cancellable; } boolean mEnded; Exception mEndException; final ByteBufferList pending = new ByteBufferList(); private AsyncSSLSocketWrapper(AsyncSocket socket, String host, int port, SSLEngine sslEngine, TrustManager[] trustManagers, HostnameVerifier verifier, boolean clientMode) { mSocket = socket; hostnameVerifier = verifier; this.clientMode = clientMode; this.trustManagers = trustManagers; this.engine = sslEngine; mHost = host; mPort = port; engine.setUseClientMode(clientMode); mSink = new BufferedDataSink(socket); mSink.setWriteableCallback(new WritableCallback() { @Override public void onWriteable() { if (mWriteableCallback != null) mWriteableCallback.onWriteable(); } }); // on pause, the emitter is paused to prevent the buffered // socket and itself from firing. // on resume, emitter is resumed, ssl buffer is flushed as well mSocket.setEndCallback(new CompletedCallback() { @Override public void onCompleted(Exception ex) { if (mEnded) return; mEnded = true; mEndException = ex; if (!pending.hasRemaining() && mEndCallback != null) mEndCallback.onCompleted(ex); } }); mSocket.setDataCallback(dataCallback); } final DataCallback dataCallback = new DataCallback() { final Allocator allocator = new Allocator().setMinAlloc(8192); final ByteBufferList buffered = new ByteBufferList(); @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { if (mUnwrapping) return; try { mUnwrapping = true; bb.get(buffered); if (buffered.hasRemaining()) { ByteBuffer all = buffered.getAll(); buffered.add(all); } ByteBuffer b = ByteBufferList.EMPTY_BYTEBUFFER; while (true) { if (b.remaining() == 0 && buffered.size() > 0) { b = buffered.remove(); } int remaining = b.remaining(); int before = pending.remaining(); SSLEngineResult res; { // wrap to prevent access to the readBuf ByteBuffer readBuf = allocator.allocate(); res = engine.unwrap(b, readBuf); addToPending(pending, readBuf); allocator.track(pending.remaining() - before); } if (res.getStatus() == Status.BUFFER_OVERFLOW) { allocator.setMinAlloc(allocator.getMinAlloc() * 2); remaining = -1; } else if (res.getStatus() == Status.BUFFER_UNDERFLOW) { buffered.addFirst(b); if (buffered.size() <= 1) { break; } // pack it remaining = -1; b = buffered.getAll(); buffered.addFirst(b); b = ByteBufferList.EMPTY_BYTEBUFFER; } handleHandshakeStatus(res.getHandshakeStatus()); if (b.remaining() == remaining && before == pending.remaining()) { buffered.addFirst(b); break; } } AsyncSSLSocketWrapper.this.onDataAvailable(); } catch (SSLException ex) { // ex.printStackTrace(); report(ex); } finally { mUnwrapping = false; } } }; public void onDataAvailable() { Util.emitAllData(this, pending); if (mEnded && !pending.hasRemaining() && mEndCallback != null) mEndCallback.onCompleted(mEndException); } @Override public SSLEngine getSSLEngine() { return engine; } void addToPending(ByteBufferList out, ByteBuffer mReadTmp) { mReadTmp.flip(); if (mReadTmp.hasRemaining()) { out.add(mReadTmp); } else { ByteBufferList.reclaim(mReadTmp); } } @Override public void end() { mSocket.end(); } public String getHost() { return mHost; } public int getPort() { return mPort; } private void handleHandshakeStatus(HandshakeStatus status) { if (status == HandshakeStatus.NEED_TASK) { final Runnable task = engine.getDelegatedTask(); task.run(); } if (status == HandshakeStatus.NEED_WRAP) { write(writeList); } if (status == HandshakeStatus.NEED_UNWRAP) { dataCallback.onDataAvailable(this, new ByteBufferList()); } try { if (!finishedHandshake && (engine.getHandshakeStatus() == HandshakeStatus.NOT_HANDSHAKING || engine.getHandshakeStatus() == HandshakeStatus.FINISHED)) { if (clientMode) { Exception peerUnverifiedCause = null; boolean trusted = false; try { peerCertificates = (X509Certificate[]) engine.getSession().getPeerCertificates(); if (mHost != null) { if (hostnameVerifier == null) { StrictHostnameVerifier verifier = new StrictHostnameVerifier(); verifier.verify(mHost, StrictHostnameVerifier.getCNs(peerCertificates[0]), StrictHostnameVerifier.getDNSSubjectAlts(peerCertificates[0])); } else { if (!hostnameVerifier.verify(mHost, engine.getSession())) { throw new SSLException("hostname <" + mHost + "> has been denied"); } } } trusted = true; } catch (SSLException ex) { peerUnverifiedCause = ex; } finishedHandshake = true; if (!trusted) { AsyncSSLException e = new AsyncSSLException(peerUnverifiedCause); report(e); if (!e.getIgnore()) throw e; } } else { finishedHandshake = true; } handshakeCallback.onHandshakeCompleted(null, this); handshakeCallback = null; mSocket.setClosedCallback(null); // handshake can complete during a wrap, so make sure that the call // stack and wrap flag is cleared before invoking writable getServer().post(new Runnable() { @Override public void run() { if (mWriteableCallback != null) mWriteableCallback.onWriteable(); } }); onDataAvailable(); } } catch (Exception ex) { report(ex); } } int calculateAlloc(int remaining) { // alloc 50% more than we need for writing int alloc = remaining * 3 / 2; if (alloc == 0) alloc = 8192; return alloc; } ByteBufferList writeList = new ByteBufferList(); @Override public void write(ByteBufferList bb) { if (mWrapping) return; if (mSink.remaining() > 0) return; mWrapping = true; int remaining; SSLEngineResult res = null; ByteBuffer writeBuf = ByteBufferList.obtain(calculateAlloc(bb.remaining())); do { // if the handshake is finished, don't send // 0 bytes of data, since that makes the ssl connection die. // it wraps a 0 byte package, and craps out. if (finishedHandshake && bb.remaining() == 0) break; remaining = bb.remaining(); try { ByteBuffer[] arr = bb.getAllArray(); res = engine.wrap(arr, writeBuf); bb.addAll(arr); writeBuf.flip(); writeList.add(writeBuf); if (writeList.remaining() > 0) mSink.write(writeList); int previousCapacity = writeBuf.capacity(); writeBuf = null; if (res.getStatus() == Status.BUFFER_OVERFLOW) { writeBuf = ByteBufferList.obtain(previousCapacity * 2); remaining = -1; } else { writeBuf = ByteBufferList.obtain(calculateAlloc(bb.remaining())); handleHandshakeStatus(res.getHandshakeStatus()); } } catch (SSLException e) { report(e); } } while ((remaining != bb.remaining() || (res != null && res.getHandshakeStatus() == HandshakeStatus.NEED_WRAP)) && mSink.remaining() == 0); mWrapping = false; ByteBufferList.reclaim(writeBuf); } @Override public void setWriteableCallback(WritableCallback handler) { mWriteableCallback = handler; } @Override public WritableCallback getWriteableCallback() { return mWriteableCallback; } private void report(Exception e) { final HandshakeCallback hs = handshakeCallback; if (hs != null) { handshakeCallback = null; mSocket.setDataCallback(new DataCallback.NullDataCallback()); mSocket.end(); // handshake sets this callback. unset it. mSocket.setClosedCallback(null); mSocket.close(); hs.onHandshakeCompleted(e, null); return; } CompletedCallback cb = getEndCallback(); if (cb != null) cb.onCompleted(e); } @Override public void setDataCallback(DataCallback callback) { mDataCallback = callback; } @Override public DataCallback getDataCallback() { return mDataCallback; } @Override public boolean isChunked() { return mSocket.isChunked(); } @Override public boolean isOpen() { return mSocket.isOpen(); } @Override public void close() { mSocket.close(); } @Override public void setClosedCallback(CompletedCallback handler) { mSocket.setClosedCallback(handler); } @Override public CompletedCallback getClosedCallback() { return mSocket.getClosedCallback(); } CompletedCallback mEndCallback; @Override public void setEndCallback(CompletedCallback callback) { mEndCallback = callback; } @Override public CompletedCallback getEndCallback() { return mEndCallback; } @Override public void pause() { mSocket.pause(); } @Override public void resume() { mSocket.resume(); onDataAvailable(); } @Override public boolean isPaused() { return mSocket.isPaused(); } @Override public AsyncServer getServer() { return mSocket.getServer(); } @Override public AsyncSocket getSocket() { return mSocket; } @Override public DataEmitter getDataEmitter() { return mSocket; } @Override public X509Certificate[] getPeerCertificates() { return peerCertificates; } @Override public String charset() { return null; } private static Certificate selfSign(KeyPair keyPair, String subjectDN) throws Exception { Provider bcProvider = new BouncyCastleProvider(); Security.addProvider(bcProvider); long now = System.currentTimeMillis(); Date startDate = new Date(now); X500Name dnName = new X500Name("CN=" + subjectDN); BigInteger certSerialNumber = new BigInteger(Long.toString(now)); // <-- Using the current timestamp as the certificate serial number Calendar calendar = Calendar.getInstance(); calendar.setTime(startDate); calendar.add(Calendar.YEAR, 1); // <-- 1 Yr validity Date endDate = calendar.getTime(); String signatureAlgorithm = "SHA256WithRSA"; // <-- Use appropriate signature algorithm based on your keyPair algorithm. ContentSigner contentSigner = new JcaContentSignerBuilder(signatureAlgorithm).build(keyPair.getPrivate()); JcaX509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(dnName, certSerialNumber, startDate, endDate, dnName, keyPair.getPublic()); // Extensions -------------------------- // Basic Constraints BasicConstraints basicConstraints = new BasicConstraints(true); // <-- true for CA, false for EndEntity certBuilder.addExtension(new ASN1ObjectIdentifier("2.5.29.19"), true, basicConstraints); // Basic Constraints is usually marked as critical. // ------------------------------------- return new JcaX509CertificateConverter().setProvider(bcProvider).getCertificate(certBuilder.build(contentSigner)); } public static Pair selfSignCertificate(final Context context, String subjectName) throws Exception { File keyPath = context.getFileStreamPath(subjectName + "-key.txt"); KeyPair pair; Certificate cert; try { String[] keyParts = StreamUtility.readFile(keyPath).split("\n"); X509EncodedKeySpec pub = new X509EncodedKeySpec(Base64.decode(keyParts[0], 0)); PKCS8EncodedKeySpec priv = new PKCS8EncodedKeySpec(Base64.decode(keyParts[1], 0)); cert = CertificateFactory.getInstance("X.509").generateCertificate(new ByteArrayInputStream(Base64.decode(keyParts[2], 0))); KeyFactory fact = KeyFactory.getInstance("RSA"); pair = new KeyPair(fact.generatePublic(pub), fact.generatePrivate(priv)); } catch (Exception e) { KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); keyGen.initialize(2048); pair = keyGen.generateKeyPair(); cert = selfSign(pair, subjectName); StreamUtility.writeFile(keyPath, Base64.encodeToString(pair.getPublic().getEncoded(), Base64.NO_WRAP) + "\n" + Base64.encodeToString(pair.getPrivate().getEncoded(), Base64.NO_WRAP) + "\n" + Base64.encodeToString(cert.getEncoded(), Base64.NO_WRAP)); } return new Pair<>(pair, cert); } public static AsyncSSLServerSocket listenSecure(final Context context, final AsyncServer server, final String subjectName, final InetAddress host, final int port, final ListenCallback handler) { final ObjectHolder holder = new ObjectHolder<>(); server.run(() -> { try { Pair keyCert = selfSignCertificate(context, subjectName); KeyPair pair = keyCert.first; Certificate cert = keyCert.second; holder.held = listenSecure(server, pair.getPrivate(), cert, host, port, handler); } catch (Exception e) { handler.onCompleted(e); } }); return holder.held; } public static AsyncSSLServerSocket listenSecure(AsyncServer server, String keyDer, String certDer, final InetAddress host, final int port, final ListenCallback handler) { return listenSecure(server, Base64.decode(keyDer, Base64.DEFAULT), Base64.decode(certDer, Base64.DEFAULT), host, port, handler); } private static class ObjectHolder { T held; } public static AsyncSSLServerSocket listenSecure(final AsyncServer server, final byte[] keyDer, final byte[] certDer, final InetAddress host, final int port, final ListenCallback handler) { final ObjectHolder holder = new ObjectHolder<>(); server.run(() -> { try { PKCS8EncodedKeySpec key = new PKCS8EncodedKeySpec(keyDer); Certificate cert = CertificateFactory.getInstance("X.509").generateCertificate(new ByteArrayInputStream(certDer)); PrivateKey pk = KeyFactory.getInstance("RSA").generatePrivate(key); holder.held = listenSecure(server, pk, cert, host, port, handler); } catch (Exception e) { handler.onCompleted(e); } }); return holder.held; } public static AsyncSSLServerSocket listenSecure(final AsyncServer server, final PrivateKey pk, final Certificate cert, final InetAddress host, final int port, final ListenCallback handler) { final ObjectHolder holder = new ObjectHolder<>(); server.run(() -> { try { KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); ks.load(null); ks.setKeyEntry("key", pk, null, new Certificate[] { cert }); KeyManagerFactory kmf = KeyManagerFactory.getInstance("X509"); kmf.init(ks, "".toCharArray()); TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); tmf.init(ks); SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); final AsyncServerSocket socket = listenSecure(server, sslContext, host, port, handler); holder.held = new AsyncSSLServerSocket() { @Override public PrivateKey getPrivateKey() { return pk; } @Override public Certificate getCertificate() { return cert; } @Override public void stop() { socket.stop(); } @Override public int getLocalPort() { return socket.getLocalPort(); } }; } catch (Exception e) { handler.onCompleted(e); } }); return holder.held; } public static AsyncServerSocket listenSecure(AsyncServer server, final SSLContext sslContext, final InetAddress host, final int port, final ListenCallback handler) { final SSLEngineSNIConfigurator conf = new SSLEngineSNIConfigurator() { @Override public SSLEngine createEngine(SSLContext sslContext, String peerHost, int peerPort) { SSLEngine engine = super.createEngine(sslContext, peerHost, peerPort); // String[] ciphers = engine.getEnabledCipherSuites(); // for (String cipher: ciphers) { // Log.i(LOGTAG, cipher); // } // todo: what's this for? some vestigal vysor code i think. required by audio mirroring? engine.setEnabledCipherSuites(new String[] { "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384" }); return engine; } }; return server.listen(host, port, new ListenCallback() { @Override public void onAccepted(final AsyncSocket socket) { AsyncSSLSocketWrapper.handshake(socket, null, port, conf.createEngine(sslContext, null, port), null, null, false, (e, sslSocket) -> { if (e != null) { // chrome seems to do some sort of SSL probe and cancels handshakes. not sure why. // i suspect it is to pick an optimal strong cipher. // seeing a lot of the following in the log (but no actual connection errors) // javax.net.ssl.SSLHandshakeException: error:10000416:SSL routines:OPENSSL_internal:SSLV3_ALERT_CERTIFICATE_UNKNOWN // seen on Shield TV running API 26 // todo fix: conscrypt ssl context? // Log.e(LOGTAG, "Error while handshaking", e); socket.close(); return; } handler.onAccepted(sslSocket); }); } @Override public void onListening(AsyncServerSocket socket) { handler.onListening(socket); } @Override public void onCompleted(Exception ex) { handler.onCompleted(ex); } }); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/AsyncSemaphore.java ================================================ package com.koushikdutta.async; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; public class AsyncSemaphore { Semaphore semaphore = new Semaphore(0); public void acquire() throws InterruptedException { ThreadQueue threadQueue = ThreadQueue.getOrCreateThreadQueue(Thread.currentThread()); AsyncSemaphore last = threadQueue.waiter; threadQueue.waiter = this; Semaphore queueSemaphore = threadQueue.queueSemaphore; try { if (semaphore.tryAcquire()) return; while (true) { // run the queue while (true) { Runnable run = threadQueue.remove(); if (run == null) break; // Log.i(LOGTAG, "Pumping for AsyncSemaphore"); run.run(); } int permits = Math.max(1, queueSemaphore.availablePermits()); queueSemaphore.acquire(permits); if (semaphore.tryAcquire()) break; } } finally { threadQueue.waiter = last; } } public boolean tryAcquire(long timeout, TimeUnit timeunit) throws InterruptedException { long timeoutMs = TimeUnit.MILLISECONDS.convert(timeout, timeunit); ThreadQueue threadQueue = ThreadQueue.getOrCreateThreadQueue(Thread.currentThread()); AsyncSemaphore last = threadQueue.waiter; threadQueue.waiter = this; Semaphore queueSemaphore = threadQueue.queueSemaphore; try { if (semaphore.tryAcquire()) return true; long start = System.currentTimeMillis(); do { // run the queue while (true) { Runnable run = threadQueue.remove(); if (run == null) break; // Log.i(LOGTAG, "Pumping for AsyncSemaphore"); run.run(); } int permits = Math.max(1, queueSemaphore.availablePermits()); if (!queueSemaphore.tryAcquire(permits, timeoutMs, TimeUnit.MILLISECONDS)) return false; if (semaphore.tryAcquire()) return true; } while (System.currentTimeMillis() - start < timeoutMs); return false; } finally { threadQueue.waiter = last; } } public void release() { semaphore.release(); ThreadQueue.release(this); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/AsyncServer.java ================================================ package com.koushikdutta.async; import android.os.Build; import android.os.Handler; import android.os.SystemClock; import android.util.Log; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.ConnectCallback; import com.koushikdutta.async.callback.ListenCallback; import com.koushikdutta.async.callback.SocketCreateCallback; import com.koushikdutta.async.callback.ValueFunction; import com.koushikdutta.async.future.Cancellable; import com.koushikdutta.async.future.Future; import com.koushikdutta.async.future.FutureCallback; import com.koushikdutta.async.future.SimpleCancellable; import com.koushikdutta.async.future.SimpleFuture; import com.koushikdutta.async.util.StreamUtility; import java.io.IOException; import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.nio.channels.CancelledKeyException; import java.nio.channels.ClosedChannelException; import java.nio.channels.ClosedSelectorException; import java.nio.channels.DatagramChannel; import java.nio.channels.SelectionKey; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.nio.channels.spi.SelectorProvider; import java.util.Arrays; import java.util.Comparator; import java.util.PriorityQueue; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.Semaphore; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; public class AsyncServer { public static final String LOGTAG = "NIO"; private static class RunnableWrapper implements Runnable { boolean hasRun; Runnable runnable; ThreadQueue threadQueue; Handler handler; @Override public void run() { synchronized (this) { if (hasRun) return; hasRun = true; } try { runnable.run(); } finally { threadQueue.remove(this); handler.removeCallbacks(this); threadQueue = null; handler = null; runnable = null; } } } public static void post(Handler handler, Runnable runnable) { RunnableWrapper wrapper = new RunnableWrapper(); ThreadQueue threadQueue = ThreadQueue.getOrCreateThreadQueue(handler.getLooper().getThread()); wrapper.threadQueue = threadQueue; wrapper.handler = handler; wrapper.runnable = runnable; // run it in a blocking AsyncSemaphore or a Handler, whichever gets to it first. threadQueue.add(wrapper); handler.post(wrapper); // run the queue if the thread is blocking threadQueue.queueSemaphore.release(); } static { try { if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.FROYO) { java.lang.System.setProperty("java.net.preferIPv4Stack", "true"); java.lang.System.setProperty("java.net.preferIPv6Addresses", "false"); } } catch (Throwable ex) { } } static AsyncServer mInstance = new AsyncServer(); public static AsyncServer getDefault() { return mInstance; } private SelectorWrapper mSelector; public boolean isRunning() { return mSelector != null; } String mName; public AsyncServer() { this(null); } public AsyncServer(String name) { if (name == null) name = "AsyncServer"; mName = name; } private static ExecutorService synchronousWorkers = newSynchronousWorkers("AsyncServer-worker-"); private static void wakeup(final SelectorWrapper selector) { synchronousWorkers.execute(() -> { try { selector.wakeupOnce(); } catch (Exception e) { } }); } boolean killed; public void kill() { synchronized (this) { killed = true; } stop(false); } int postCounter = 0; public Cancellable postDelayed(Runnable runnable, long delay) { Scheduled s; synchronized (this) { if (killed) return SimpleCancellable.CANCELLED; // Calculate when to run this queue item: // If there is a delay (non-zero), add it to the current time // When delay is zero, ensure that this follows all other // zero-delay queue items. This is done by setting the // "time" to the queue size. This will make sure it is before // all time-delayed queue items (for all real world scenarios) // as it will always be less than the current time and also remain // behind all other immediately run queue items. long time; if (delay > 0) time = SystemClock.elapsedRealtime() + delay; else if (delay == 0) time = postCounter++; else if (mQueue.size() > 0) time = Math.min(0, mQueue.peek().time - 1); else time = 0; mQueue.add(s = new Scheduled(this, runnable, time)); // start the server up if necessary if (mSelector == null) run(); if (!isAffinityThread()) { wakeup(mSelector); } } return s; } public Cancellable postImmediate(Runnable runnable) { if (Thread.currentThread() == getAffinity()) { runnable.run(); return null; } return postDelayed(runnable, -1); } public Cancellable post(Runnable runnable) { return postDelayed(runnable, 0); } public Cancellable post(final CompletedCallback callback, final Exception e) { return post(() -> callback.onCompleted(e)); } public void run(final Runnable runnable) { if (Thread.currentThread() == mAffinity) { post(runnable); lockAndRunQueue(this, mQueue); return; } final Semaphore semaphore; synchronized (this) { if (killed) return; semaphore = new Semaphore(0); post(() -> { runnable.run(); semaphore.release(); }); } try { semaphore.acquire(); } catch (InterruptedException e) { Log.e(LOGTAG, "run", e); } } private static class Scheduled implements Cancellable, Runnable { // this constructor is only called when the async execution should not be preserved // ie... AsyncServer.stop. public Scheduled(AsyncServer server, Runnable runnable, long time) { this.server = server; this.runnable = runnable; this.time = time; } public AsyncServer server; public Runnable runnable; public long time; @Override public void run() { this.runnable.run(); } @Override public boolean isDone() { synchronized (server) { return !cancelled && !server.mQueue.contains(this); } } boolean cancelled; @Override public boolean isCancelled() { return cancelled; } @Override public boolean cancel() { synchronized (server) { return cancelled = server.mQueue.remove(this); } } } PriorityQueue mQueue = new PriorityQueue(1, Scheduler.INSTANCE); static class Scheduler implements Comparator { public static Scheduler INSTANCE = new Scheduler(); private Scheduler() { } @Override public int compare(Scheduled s1, Scheduled s2) { // keep the smaller ones at the head, so they get tossed out quicker if (s1.time == s2.time) return 0; if (s1.time > s2.time) return 1; return -1; } } public void stop() { stop(false); } public void stop(boolean wait) { // Log.i(LOGTAG, "****AsyncServer is shutting down.****"); final SelectorWrapper currentSelector; final Semaphore semaphore; final boolean isAffinityThread; synchronized (this) { isAffinityThread = isAffinityThread(); currentSelector = mSelector; if (currentSelector == null) return; semaphore = new Semaphore(0); // post a shutdown and wait mQueue.add(new Scheduled(this, new Runnable() { @Override public void run() { shutdownEverything(currentSelector); semaphore.release(); } }, 0)); synchronousWorkers.execute(() -> { try { currentSelector.wakeupOnce(); } catch (Exception e) { } }); // force any existing connections to die shutdownKeys(currentSelector); mQueue = new PriorityQueue<>(1, Scheduler.INSTANCE); mSelector = null; mAffinity = null; } try { if (!isAffinityThread && wait) semaphore.acquire(); } catch (Exception e) { } } protected void onDataReceived(int transmitted) { } protected void onDataSent(int transmitted) { } private static class ObjectHolder { T held; } public AsyncServerSocket listen(final InetAddress host, final int port, final ListenCallback handler) { final ObjectHolder holder = new ObjectHolder<>(); run(new Runnable() { @Override public void run() { ServerSocketChannel closeableServer = null; ServerSocketChannelWrapper closeableWrapper = null; try { closeableServer = ServerSocketChannel.open(); closeableWrapper = new ServerSocketChannelWrapper( closeableServer); final ServerSocketChannel server = closeableServer; final ServerSocketChannelWrapper wrapper = closeableWrapper; InetSocketAddress isa; if (host == null) isa = new InetSocketAddress(port); else isa = new InetSocketAddress(host, port); server.socket().bind(isa); final SelectionKey key = wrapper.register(mSelector.getSelector()); key.attach(handler); handler.onListening(holder.held = new AsyncServerSocket() { @Override public int getLocalPort() { return server.socket().getLocalPort(); } @Override public void stop() { StreamUtility.closeQuietly(wrapper); try { key.cancel(); } catch (Exception e) { } } }); } catch (IOException e) { Log.e(LOGTAG, "wtf", e); StreamUtility.closeQuietly(closeableWrapper, closeableServer); handler.onCompleted(e); } } }); return holder.held; } private class ConnectFuture extends SimpleFuture { @Override protected void cancelCleanup() { super.cancelCleanup(); try { if (socket != null) socket.close(); } catch (IOException e) { } } SocketChannel socket; ConnectCallback callback; } public Cancellable connectResolvedInetSocketAddress(final InetSocketAddress address, final ConnectCallback callback) { return connectResolvedInetSocketAddress(address, callback, null); } public ConnectFuture connectResolvedInetSocketAddress(final InetSocketAddress address, final ConnectCallback callback, final SocketCreateCallback createCallback) { final ConnectFuture cancel = new ConnectFuture(); post(new Runnable() { @Override public void run() { if (cancel.isCancelled()) return; cancel.callback = callback; SelectionKey ckey = null; SocketChannel socket = null; try { socket = cancel.socket = SocketChannel.open(); socket.configureBlocking(false); ckey = socket.register(mSelector.getSelector(), SelectionKey.OP_CONNECT); ckey.attach(cancel); if (createCallback != null) createCallback.onSocketCreated(socket.socket().getLocalPort()); socket.connect(address); } catch (Throwable e) { if (ckey != null) ckey.cancel(); StreamUtility.closeQuietly(socket); cancel.setComplete(new RuntimeException(e)); } } }); return cancel; } public Cancellable connectSocket(final InetSocketAddress remote, final ConnectCallback callback) { if (!remote.isUnresolved()) return connectResolvedInetSocketAddress(remote, callback); final SimpleFuture ret = new SimpleFuture(); Future lookup = getByName(remote.getHostName()); ret.setParent(lookup); lookup .setCallback(new FutureCallback() { @Override public void onCompleted(Exception e, InetAddress result) { if (e != null) { callback.onConnectCompleted(e, null); ret.setComplete(e); return; } ret.setComplete((ConnectFuture)connectResolvedInetSocketAddress(new InetSocketAddress(result, remote.getPort()), callback)); } }); return ret; } public Cancellable connectSocket(final String host, final int port, final ConnectCallback callback) { return connectSocket(InetSocketAddress.createUnresolved(host, port), callback); } private static ExecutorService newSynchronousWorkers(String prefix) { ThreadFactory tf = new NamedThreadFactory(prefix); ThreadPoolExecutor tpe = new ThreadPoolExecutor(0, 4, 10L, TimeUnit.SECONDS, new LinkedBlockingQueue(), tf); return tpe; } private static final Comparator ipSorter = new Comparator() { @Override public int compare(InetAddress lhs, InetAddress rhs) { if (lhs instanceof Inet4Address && rhs instanceof Inet4Address) return 0; if (lhs instanceof Inet6Address && rhs instanceof Inet6Address) return 0; if (lhs instanceof Inet4Address && rhs instanceof Inet6Address) return -1; return 1; } }; private static ExecutorService synchronousResolverWorkers = newSynchronousWorkers("AsyncServer-resolver-"); public Future getAllByName(final String host) { final SimpleFuture ret = new SimpleFuture(); synchronousResolverWorkers.execute(new Runnable() { @Override public void run() { try { final InetAddress[] result = InetAddress.getAllByName(host); Arrays.sort(result, ipSorter); if (result == null || result.length == 0) throw new HostnameResolutionException("no addresses for host"); post(new Runnable() { @Override public void run() { ret.setComplete(null, result); } }); } catch (final Exception e) { post(new Runnable() { @Override public void run() { ret.setComplete(e, null); } }); } } }); return ret; } public Future getByName(String host) { return getAllByName(host).thenConvert(addresses -> addresses[0]); } private void handleSocket(final AsyncNetworkSocket handler) throws ClosedChannelException { final ChannelWrapper sc = handler.getChannel(); SelectionKey ckey = sc.register(mSelector.getSelector()); ckey.attach(handler); handler.setup(this, ckey); } public AsyncDatagramSocket connectDatagram(final String host, final int port) throws IOException { final DatagramChannel socket = DatagramChannel.open(); final AsyncDatagramSocket handler = new AsyncDatagramSocket(); handler.attach(socket); // ugh.. this should really be post to make it nonblocking... // but i want datagrams to be immediately writable. // they're not really used anyways. run(new Runnable() { @Override public void run() { try { final SocketAddress remote = new InetSocketAddress(host, port); handleSocket(handler); socket.connect(remote); } catch (IOException e) { Log.e(LOGTAG, "Datagram error", e); StreamUtility.closeQuietly(socket); } } }); return handler; } public AsyncDatagramSocket openDatagram() { return openDatagram(null, 0, false); } public Cancellable createDatagram(String address, int port, boolean reuseAddress, FutureCallback callback) { return createDatagram(() -> InetAddress.getByName(address), port, reuseAddress, callback); } public Cancellable createDatagram(InetAddress address, int port, boolean reuseAddress, FutureCallback callback) { return createDatagram(() -> address, port, reuseAddress, callback); } private Cancellable createDatagram(ValueFunction inetAddressValueFunction, final int port, final boolean reuseAddress, FutureCallback callback) { SimpleFuture ret = new SimpleFuture<>(); ret.setCallback(callback); post(() -> { DatagramChannel socket = null; try { socket = DatagramChannel.open(); final AsyncDatagramSocket handler = new AsyncDatagramSocket(); handler.attach(socket); InetSocketAddress address; if (inetAddressValueFunction == null) address = new InetSocketAddress(port); else address = new InetSocketAddress(inetAddressValueFunction.getValue(), port); if (reuseAddress) socket.socket().setReuseAddress(reuseAddress); socket.socket().bind(address); handleSocket(handler); if (!ret.setComplete(handler)) socket.close(); } catch (Exception e) { StreamUtility.closeQuietly(socket); ret.setComplete(e); } }); return ret; } public AsyncDatagramSocket openDatagram(final InetAddress host, final int port, final boolean reuseAddress) { final AsyncDatagramSocket handler = new AsyncDatagramSocket(); // ugh.. this should really be post to make it nonblocking... // but i want datagrams to be immediately writable. // they're not really used anyways. Runnable runnable = () -> { final DatagramChannel socket; try { socket = DatagramChannel.open(); } catch (Exception e) { return; } try { handler.attach(socket); InetSocketAddress address; if (host == null) address = new InetSocketAddress(port); else address = new InetSocketAddress(host, port); if (reuseAddress) socket.socket().setReuseAddress(reuseAddress); socket.socket().bind(address); handleSocket(handler); } catch (IOException e) { Log.e(LOGTAG, "Datagram error", e); StreamUtility.closeQuietly(socket); } }; if (getAffinity() != Thread.currentThread()) { run(runnable); return handler; } runnable.run(); return handler; } public AsyncDatagramSocket connectDatagram(final SocketAddress remote) throws IOException { final AsyncDatagramSocket handler = new AsyncDatagramSocket(); final DatagramChannel socket = DatagramChannel.open(); handler.attach(socket); // ugh.. this should really be post to make it nonblocking... // but i want datagrams to be immediately writable. // they're not really used anyways. Runnable runnable = () -> { try { handleSocket(handler); socket.connect(remote); } catch (IOException e) { StreamUtility.closeQuietly(socket); } }; if (getAffinity() != Thread.currentThread()) { run(runnable); return handler; } runnable.run(); return handler; } final private static ThreadLocal threadServer = new ThreadLocal<>(); public static AsyncServer getCurrentThreadServer() { return threadServer.get(); } Thread mAffinity; private void run() { final SelectorWrapper selector; final PriorityQueue queue; synchronized (this) { if (mSelector == null) { try { selector = mSelector = new SelectorWrapper(SelectorProvider.provider().openSelector()); queue = mQueue; } catch (IOException e) { throw new RuntimeException("unable to create selector?", e); } mAffinity = new Thread(mName) { public void run() { try { threadServer.set(AsyncServer.this); AsyncServer.run(AsyncServer.this, selector, queue); } finally { threadServer.remove(); } } }; mAffinity.start(); // kicked off the new thread, let's bail. return; } // this is a reentrant call selector = mSelector; queue = mQueue; // fall through to outside of the synchronization scope // to allow the thread to run without locking. } try { runLoop(this, selector, queue); } catch (AsyncSelectorException e) { Log.i(LOGTAG, "Selector closed", e); try { // StreamUtility.closeQuiety is throwing ArrayStoreException? selector.getSelector().close(); } catch (Exception ex) { } } } private static void run(final AsyncServer server, final SelectorWrapper selector, final PriorityQueue queue) { // Log.i(LOGTAG, "****AsyncServer is starting.****"); // at this point, this local queue and selector are owned // by this thread. // if a stop is called, the instance queue and selector // will be replaced and nulled respectively. // this will allow the old queue and selector to shut down // gracefully, while also allowing a new selector thread // to start up while the old one is still shutting down. while(true) { try { runLoop(server, selector, queue); } catch (AsyncSelectorException e) { if (!(e.getCause() instanceof ClosedSelectorException)) Log.i(LOGTAG, "Selector exception, shutting down", e); StreamUtility.closeQuietly(selector); } // see if we keep looping, this must be in a synchronized block since the queue is accessed. synchronized (server) { if (selector.isOpen() && (selector.keys().size() > 0 || queue.size() > 0)) continue; shutdownEverything(selector); if (server.mSelector == selector) { server.mQueue = new PriorityQueue(1, Scheduler.INSTANCE); server.mSelector = null; server.mAffinity = null; } break; } } // Log.i(LOGTAG, "****AsyncServer has shut down.****"); } private static void shutdownKeys(SelectorWrapper selector) { try { for (SelectionKey key: selector.keys()) { StreamUtility.closeQuietly(key.channel()); try { key.cancel(); } catch (Exception e) { } } } catch (Exception ex) { } } private static void shutdownEverything(SelectorWrapper selector) { shutdownKeys(selector); // SHUT. DOWN. EVERYTHING. StreamUtility.closeQuietly(selector); } private static final long QUEUE_EMPTY = Long.MAX_VALUE; private static long lockAndRunQueue(final AsyncServer server, final PriorityQueue queue) { long wait = QUEUE_EMPTY; // find the first item we can actually run while (true) { Scheduled run = null; synchronized (server) { long now = SystemClock.elapsedRealtime(); if (queue.size() > 0) { Scheduled s = queue.remove(); if (s.time <= now) { run = s; } else { wait = s.time - now; queue.add(s); } } } if (run == null) break; run.run(); } server.postCounter = 0; return wait; } private static class AsyncSelectorException extends IOException { public AsyncSelectorException(Exception e) { super(e); } } private static void runLoop(final AsyncServer server, final SelectorWrapper selector, final PriorityQueue queue) throws AsyncSelectorException { // Log.i(LOGTAG, "Keys: " + selector.keys().size()); boolean needsSelect = true; // run the queue to populate the selector with keys long wait = lockAndRunQueue(server, queue); try { synchronized (server) { // select now to see if anything is ready immediately. this // also clears the canceled key queue. int readyNow = selector.selectNow(); if (readyNow == 0) { // if there is nothing to select now, make sure we don't have an empty key set // which means it would be time to turn this thread off. if (selector.keys().size() == 0 && wait == QUEUE_EMPTY) { // Log.i(LOGTAG, "Shutting down. keys: " + selector.keys().size() + " keepRunning: " + keepRunning); return; } } else { needsSelect = false; } } if (needsSelect) { if (wait == QUEUE_EMPTY) { // wait until woken up selector.select(); } else { // nothing to select immediately but there's something pending so let's block that duration and wait. selector.select(wait); } } } catch (Exception e) { throw new AsyncSelectorException(e); } // process whatever keys are ready Set readyKeys = selector.selectedKeys(); for (SelectionKey key: readyKeys) { try { if (key.isAcceptable()) { ServerSocketChannel nextReady = (ServerSocketChannel) key.channel(); SocketChannel sc = null; SelectionKey ckey = null; try { sc = nextReady.accept(); if (sc == null) continue; sc.configureBlocking(false); ckey = sc.register(selector.getSelector(), SelectionKey.OP_READ); ListenCallback serverHandler = (ListenCallback) key.attachment(); AsyncNetworkSocket handler = new AsyncNetworkSocket(); handler.attach(sc, (InetSocketAddress)sc.socket().getRemoteSocketAddress()); handler.setup(server, ckey); ckey.attach(handler); serverHandler.onAccepted(handler); } catch (IOException e) { StreamUtility.closeQuietly(sc); if (ckey != null) ckey.cancel(); } } else if (key.isReadable()) { AsyncNetworkSocket handler = (AsyncNetworkSocket) key.attachment(); int transmitted = handler.onReadable(); server.onDataReceived(transmitted); } else if (key.isWritable()) { AsyncNetworkSocket handler = (AsyncNetworkSocket) key.attachment(); handler.onDataWritable(); } else if (key.isConnectable()) { ConnectFuture cancel = (ConnectFuture) key.attachment(); SocketChannel sc = (SocketChannel) key.channel(); key.interestOps(SelectionKey.OP_READ); AsyncNetworkSocket newHandler; try { sc.finishConnect(); newHandler = new AsyncNetworkSocket(); newHandler.setup(server, key); newHandler.attach(sc, (InetSocketAddress)sc.socket().getRemoteSocketAddress()); key.attach(newHandler); } catch (IOException ex) { key.cancel(); StreamUtility.closeQuietly(sc); if (cancel.setComplete(ex)) cancel.callback.onConnectCompleted(ex, null); continue; } if (cancel.setComplete(newHandler)) cancel.callback.onConnectCompleted(null, newHandler); } else { Log.i(LOGTAG, "wtf"); throw new RuntimeException("Unknown key state."); } } catch (CancelledKeyException ex) { } } readyKeys.clear(); } public void dump() { post(new Runnable() { @Override public void run() { if (mSelector == null) { Log.i(LOGTAG, "Server dump not possible. No selector?"); return; } Log.i(LOGTAG, "Key Count: " + mSelector.keys().size()); for (SelectionKey key: mSelector.keys()) { Log.i(LOGTAG, "Key: " + key); } } }); } public Thread getAffinity() { return mAffinity; } public boolean isAffinityThread() { return mAffinity == Thread.currentThread(); } public boolean isAffinityThreadOrStopped() { Thread affinity = mAffinity; return affinity == null || affinity == Thread.currentThread(); } private static class NamedThreadFactory implements ThreadFactory { private final ThreadGroup group; private final AtomicInteger threadNumber = new AtomicInteger(1); private final String namePrefix; NamedThreadFactory(String namePrefix) { SecurityManager s = System.getSecurityManager(); group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup(); this.namePrefix = namePrefix; } public Thread newThread(Runnable r) { Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0); if (t.isDaemon()) t.setDaemon(false); if (t.getPriority() != Thread.NORM_PRIORITY) { t.setPriority(Thread.NORM_PRIORITY); } return t; } } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/AsyncServerSocket.java ================================================ package com.koushikdutta.async; public interface AsyncServerSocket { public void stop(); public int getLocalPort(); } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/AsyncSocket.java ================================================ package com.koushikdutta.async; public interface AsyncSocket extends DataEmitter, DataSink { public AsyncServer getServer(); } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/BufferedDataSink.java ================================================ package com.koushikdutta.async; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.WritableCallback; public class BufferedDataSink implements DataSink { DataSink mDataSink; public BufferedDataSink(DataSink datasink) { setDataSink(datasink); } public boolean isBuffering() { return mPendingWrites.hasRemaining() || forceBuffering; } public boolean isWritable() { synchronized (mPendingWrites) { return mPendingWrites.remaining() < mMaxBuffer; } } public DataSink getDataSink() { return mDataSink; } boolean forceBuffering; public void forceBuffering(boolean forceBuffering) { this.forceBuffering = forceBuffering; if (!forceBuffering) writePending(); } public void setDataSink(DataSink datasink) { mDataSink = datasink; mDataSink.setWriteableCallback(this::writePending); } private void writePending() { if (forceBuffering) return; // Log.i("NIO", "Writing to buffer..."); boolean empty; synchronized (mPendingWrites) { mDataSink.write(mPendingWrites); empty = mPendingWrites.isEmpty(); } if (empty) { if (endPending) mDataSink.end(); } if (empty && mWritable != null) mWritable.onWriteable(); } final ByteBufferList mPendingWrites = new ByteBufferList(); // before the data is queued, let inheritors know. allows for filters, without // issues with having to filter before writing which may fail in the buffer. protected void onDataAccepted(ByteBufferList bb) { } @Override public void write(final ByteBufferList bb) { if (getServer().getAffinity() != Thread.currentThread()) { synchronized (mPendingWrites) { if (mPendingWrites.remaining() >= mMaxBuffer) return; onDataAccepted(bb); bb.get(mPendingWrites); } getServer().post(this::writePending); return; } onDataAccepted(bb); if (!isBuffering()) mDataSink.write(bb); synchronized (mPendingWrites) { bb.get(mPendingWrites); } } WritableCallback mWritable; @Override public void setWriteableCallback(WritableCallback handler) { mWritable = handler; } @Override public WritableCallback getWriteableCallback() { return mWritable; } public int remaining() { return mPendingWrites.remaining(); } int mMaxBuffer = Integer.MAX_VALUE; public int getMaxBuffer() { return mMaxBuffer; } public void setMaxBuffer(int maxBuffer) { mMaxBuffer = maxBuffer; } @Override public boolean isOpen() { return mDataSink.isOpen(); } boolean endPending; @Override public void end() { if (getServer().getAffinity() != Thread.currentThread()) { getServer().post(this::end); return; } synchronized (mPendingWrites) { if (mPendingWrites.hasRemaining()) { endPending = true; return; } } mDataSink.end(); } @Override public void setClosedCallback(CompletedCallback handler) { mDataSink.setClosedCallback(handler); } @Override public CompletedCallback getClosedCallback() { return mDataSink.getClosedCallback(); } @Override public AsyncServer getServer() { return mDataSink.getServer(); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/ByteBufferList.java ================================================ package com.koushikdutta.async; import android.annotation.TargetApi; import android.os.Build; import android.os.Looper; import com.koushikdutta.async.util.ArrayDeque; import com.koushikdutta.async.util.Charsets; import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.charset.Charset; import java.util.Comparator; import java.util.PriorityQueue; @TargetApi(Build.VERSION_CODES.GINGERBREAD) public class ByteBufferList { ArrayDeque mBuffers = new ArrayDeque(); ByteOrder order = ByteOrder.BIG_ENDIAN; public ByteOrder order() { return order; } public ByteBufferList order(ByteOrder order) { this.order = order; return this; } public ByteBufferList() { } public ByteBufferList(ByteBuffer... b) { addAll(b); } public ByteBufferList(byte[] buf) { super(); ByteBuffer b = ByteBuffer.wrap(buf); add(b); } public ByteBufferList addAll(ByteBuffer... bb) { for (ByteBuffer b: bb) add(b); return this; } public ByteBufferList addAll(ByteBufferList... bb) { for (ByteBufferList b: bb) b.get(this); return this; } public byte[] getBytes(int length) { byte[] ret = new byte[length]; get(ret); return ret; } public byte[] getAllByteArray() { byte[] ret = new byte[remaining()]; get(ret); return ret; } public ByteBuffer[] getAllArray() { ByteBuffer[] ret = new ByteBuffer[mBuffers.size()]; ret = mBuffers.toArray(ret); mBuffers.clear(); remaining = 0; return ret; } public boolean isEmpty() { return remaining == 0; } private int remaining = 0; public int remaining() { return remaining; } public boolean hasRemaining() { return remaining() > 0; } public short peekShort() { return read(2).getShort(mBuffers.peekFirst().position()); } public byte peek() { return read(1).get(mBuffers.peekFirst().position()); } public int peekInt() { return read(4).getInt(mBuffers.peekFirst().position()); } public long peekLong() { return read(8).getLong(mBuffers.peekFirst().position()); } public byte[] peekBytes(int size) { byte[] ret = new byte[size]; read(size).get(ret, mBuffers.peekFirst().position(), ret.length); return ret; } public ByteBufferList skip(int length) { get(null, 0, length); return this; } public int getInt() { int ret = read(4).getInt(); remaining -= 4; return ret; } public char getByteChar() { char ret = (char)read(1).get(); remaining--; return ret; } public short getShort() { short ret = read(2).getShort(); remaining -= 2; return ret; } public byte get() { byte ret = read(1).get(); remaining--; return ret; } public long getLong() { long ret = read(8).getLong(); remaining -= 8; return ret; } public void get(byte[] bytes) { get(bytes, 0, bytes.length); } public void get(byte[] bytes, int offset, int length) { if (remaining() < length) throw new IllegalArgumentException("length"); int need = length; while (need > 0) { ByteBuffer b = mBuffers.peek(); int read = Math.min(b.remaining(), need); if (bytes != null){ b.get(bytes, offset, read); } else { //when bytes is null, just skip data. b.position(b.position() + read); } need -= read; offset += read; if (b.remaining() == 0) { ByteBuffer removed = mBuffers.remove(); reclaim(b); } } remaining -= length; } public void get(ByteBufferList into, int length) { if (remaining() < length) throw new IllegalArgumentException("length"); int offset = 0; while (offset < length) { ByteBuffer b = mBuffers.remove(); int remaining = b.remaining(); if (remaining == 0) { reclaim(b); continue; } if (offset + remaining > length) { int need = length - offset; // this is shared between both ByteBuffer subset = obtain(need); subset.limit(need); b.get(subset.array(), 0, need); into.add(subset); mBuffers.addFirst(b); break; } else { // this belongs to the new list into.add(b); } offset += remaining; } remaining -= length; } public void get(ByteBufferList into) { get(into, remaining()); } public ByteBufferList get(int length) { ByteBufferList ret = new ByteBufferList(); get(ret, length); return ret.order(order); } public ByteBuffer getAll() { if (remaining() == 0) return EMPTY_BYTEBUFFER; read(remaining()); return remove(); } private ByteBuffer read(int count) { if (remaining() < count) throw new IllegalArgumentException("count : " + remaining() + "/" + count); ByteBuffer first = mBuffers.peek(); while (first != null && !first.hasRemaining()) { reclaim(mBuffers.remove()); first = mBuffers.peek(); } if (first == null) { return EMPTY_BYTEBUFFER; } if (first.remaining() >= count) { return first.order(order); } ByteBuffer ret = obtain(count); ret.limit(count); byte[] bytes = ret.array(); 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; if (bb.remaining() == 0) { reclaim(bb); bb = null; } } // if there was still data left in the last buffer we popped // toss it back into the head if (bb != null && bb.remaining() > 0) mBuffers.addFirst(bb); mBuffers.addFirst(ret); return ret.order(order); } public void trim() { // this clears out buffers that are empty in the beginning of the list read(0); } public ByteBufferList add(ByteBufferList b) { b.get(this); return this; } public ByteBufferList add(ByteBuffer b) { if (b.remaining() <= 0) { // System.out.println("reclaiming remaining: " + b.remaining()); reclaim(b); return this; } addRemaining(b.remaining()); // see if we can fit the entirety of the buffer into the end // of the current last buffer if (mBuffers.size() > 0) { ByteBuffer last = mBuffers.getLast(); if (last.capacity() - last.limit() >= b.remaining()) { last.mark(); last.position(last.limit()); last.limit(last.capacity()); last.put(b); last.limit(last.position()); last.reset(); reclaim(b); trim(); return this; } } mBuffers.add(b); trim(); return this; } public void addFirst(ByteBuffer b) { if (b.remaining() <= 0) { reclaim(b); return; } addRemaining(b.remaining()); // see if we can fit the entirety of the buffer into the beginning // of the current first buffer if (mBuffers.size() > 0) { ByteBuffer first = mBuffers.getFirst(); if (first.position() >= b.remaining()) { first.position(first.position() - b.remaining()); first.mark(); first.put(b); first.reset(); reclaim(b); return; } } mBuffers.addFirst(b); } private void addRemaining(int remaining) { if (this.remaining() >= 0) this.remaining += remaining; } public void recycle() { while (mBuffers.size() > 0) { reclaim(mBuffers.remove()); } remaining = 0; } public ByteBuffer remove() { ByteBuffer ret = mBuffers.remove(); remaining -= ret.remaining(); return ret; } public int size() { return mBuffers.size(); } public void spewString() { System.out.println(peekString()); } public String peekString() { return peekString(null); } // not doing toString as this is really nasty in the debugger... public String peekString(Charset charset) { if (charset == null) charset = Charsets.UTF_8; StringBuilder builder = new StringBuilder(); for (ByteBuffer bb: mBuffers) { byte[] bytes; int offset; int length; if (bb.isDirect()) { bytes = new byte[bb.remaining()]; offset = 0; length = bb.remaining(); bb.get(bytes); } else { bytes = bb.array(); offset = bb.arrayOffset() + bb.position(); length = bb.remaining(); } builder.append(new String(bytes, offset, length, charset)); } return builder.toString(); } public String readString() { return readString(null); } public String readString(Charset charset) { String ret = peekString(charset); recycle(); return ret; } static class Reclaimer implements Comparator { @Override public int compare(ByteBuffer byteBuffer, ByteBuffer byteBuffer2) { // keep the smaller ones at the head, so they get tossed out quicker if (byteBuffer.capacity() == byteBuffer2.capacity()) return 0; if (byteBuffer.capacity() > byteBuffer2.capacity()) return 1; return -1; } } static PriorityQueue reclaimed = new PriorityQueue(8, new Reclaimer()); private static PriorityQueue getReclaimed() { Looper mainLooper = Looper.getMainLooper(); if (mainLooper != null) { if (Thread.currentThread() == mainLooper.getThread()) return null; } return reclaimed; } private static int MAX_SIZE = 1024 * 1024; public static int MAX_ITEM_SIZE = 1024 * 256; static int currentSize = 0; static int maxItem = 0; public static void setMaxPoolSize(int size) { MAX_SIZE = size; } public static void setMaxItemSize(int size) { MAX_ITEM_SIZE = size; } private static boolean reclaimedContains(ByteBuffer b) { for (ByteBuffer other: reclaimed) { if (other == b) return true; } return false; } public static void reclaim(ByteBuffer b) { if (b == null || b.isDirect()) return; if (b.arrayOffset() != 0 || b.array().length != b.capacity()) return; if (b.capacity() < 8192) return; if (b.capacity() > MAX_ITEM_SIZE) return; PriorityQueue r = getReclaimed(); if (r == null) return; synchronized (LOCK) { while (currentSize > MAX_SIZE && r.size() > 0 && r.peek().capacity() < b.capacity()) { // System.out.println("removing for better: " + b.capacity()); ByteBuffer head = r.remove(); currentSize -= head.capacity(); } if (currentSize > MAX_SIZE) { // System.out.println("too full: " + b.capacity()); return; } b.position(0); b.limit(b.capacity()); currentSize += b.capacity(); r.add(b); maxItem = Math.max(maxItem, b.capacity()); } } private static final Object LOCK = new Object(); public static ByteBuffer obtain(int size) { if (size <= maxItem) { PriorityQueue r = getReclaimed(); if (r != null) { synchronized (LOCK) { while (r.size() > 0) { ByteBuffer ret = r.remove(); if (r.size() == 0) maxItem = 0; currentSize -= ret.capacity(); if (ret.capacity() >= size) { // System.out.println("using " + ret.capacity()); return ret; } // System.out.println("dumping " + ret.capacity()); } } } } // System.out.println("alloc for " + size); ByteBuffer ret = ByteBuffer.allocate(Math.max(8192, size)); return ret; } public static void obtainArray(ByteBuffer[] arr, int size) { PriorityQueue r = getReclaimed(); int index = 0; int total = 0; if (r != null) { synchronized (LOCK) { while (r.size() > 0 && total < size && index < arr.length - 1) { ByteBuffer b = r.remove(); currentSize -= b.capacity(); int needed = Math.min(size - total, b.capacity()); total += needed; arr[index++] = b; } } } if (total < size) { ByteBuffer b = ByteBuffer.allocate(Math.max(8192, size - total)); arr[index++] = b; } for (int i = index; i < arr.length; i++) { arr[i] = EMPTY_BYTEBUFFER; } } public static ByteBuffer deepCopy(ByteBuffer copyOf) { if (copyOf == null) return null; return (ByteBuffer)obtain(copyOf.remaining()).put(copyOf.duplicate()).flip(); } public static final ByteBuffer EMPTY_BYTEBUFFER = ByteBuffer.allocate(0); public static void writeOutputStream(OutputStream out, ByteBuffer b) throws IOException { byte[] bytes; int offset; int length; if (b.isDirect()) { bytes = new byte[b.remaining()]; offset = 0; length = b.remaining(); b.get(bytes); } else { bytes = b.array(); offset = b.arrayOffset() + b.position(); length = b.remaining(); } out.write(bytes, offset, length); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/ChannelWrapper.java ================================================ package com.koushikdutta.async; import java.io.IOException; import java.net.InetAddress; import java.nio.ByteBuffer; import java.nio.channels.ClosedChannelException; import java.nio.channels.ReadableByteChannel; import java.nio.channels.ScatteringByteChannel; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.spi.AbstractSelectableChannel; abstract class ChannelWrapper implements ReadableByteChannel, ScatteringByteChannel { private AbstractSelectableChannel mChannel; ChannelWrapper(AbstractSelectableChannel channel) throws IOException { channel.configureBlocking(false); mChannel = channel; } public abstract void shutdownInput(); public abstract void shutdownOutput(); 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(); } public abstract int getLocalPort(); public abstract InetAddress getLocalAddress(); public abstract Object getSocket(); } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/DataEmitter.java ================================================ package com.koushikdutta.async; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.DataCallback; public interface DataEmitter { void setDataCallback(DataCallback callback); DataCallback getDataCallback(); boolean isChunked(); void pause(); void resume(); void close(); boolean isPaused(); void setEndCallback(CompletedCallback callback); CompletedCallback getEndCallback(); AsyncServer getServer(); String charset(); } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/DataEmitterBase.java ================================================ package com.koushikdutta.async; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.DataCallback; /** * Created by koush on 5/27/13. */ public abstract class DataEmitterBase implements DataEmitter { private boolean ended; protected void report(Exception e) { if (ended) return; ended = true; if (getEndCallback() != null) getEndCallback().onCompleted(e); } @Override public final void setEndCallback(CompletedCallback callback) { endCallback = callback; } CompletedCallback endCallback; @Override public final CompletedCallback getEndCallback() { return endCallback; } DataCallback mDataCallback; @Override public void setDataCallback(DataCallback callback) { mDataCallback = callback; } @Override public DataCallback getDataCallback() { return mDataCallback; } @Override public String charset() { return null; } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/DataEmitterReader.java ================================================ package com.koushikdutta.async; import com.koushikdutta.async.callback.DataCallback; public class DataEmitterReader implements com.koushikdutta.async.callback.DataCallback { DataCallback mPendingRead; int mPendingReadLength; ByteBufferList mPendingData = new ByteBufferList(); public void read(int count, DataCallback callback) { mPendingReadLength = count; mPendingRead = callback; mPendingData.recycle(); } private boolean handlePendingData(DataEmitter emitter) { if (mPendingReadLength > mPendingData.remaining()) return false; DataCallback pendingRead = mPendingRead; mPendingRead = null; pendingRead.onDataAvailable(emitter, mPendingData); return true; } public DataEmitterReader() { } @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { // if we're registered for data, we must be waiting for a read do { int need = Math.min(bb.remaining(), mPendingReadLength - mPendingData.remaining()); bb.get(mPendingData, need); bb.remaining(); } while (handlePendingData(emitter) && mPendingRead != null); bb.remaining(); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/DataSink.java ================================================ package com.koushikdutta.async; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.WritableCallback; public interface DataSink { public void write(ByteBufferList bb); public void setWriteableCallback(WritableCallback handler); public WritableCallback getWriteableCallback(); public boolean isOpen(); public void end(); public void setClosedCallback(CompletedCallback handler); public CompletedCallback getClosedCallback(); public AsyncServer getServer(); } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/DataTrackingEmitter.java ================================================ package com.koushikdutta.async; /** * Created by koush on 5/28/13. */ public interface DataTrackingEmitter extends DataEmitter { interface DataTracker { void onData(int totalBytesRead); } void setDataTracker(DataTracker tracker); DataTracker getDataTracker(); int getBytesRead(); void setDataEmitter(DataEmitter emitter); } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/DatagramChannelWrapper.java ================================================ package com.koushikdutta.async; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; 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; @Override public InetAddress getLocalAddress() { return mChannel.socket().getLocalAddress(); } @Override public int getLocalPort() { return mChannel.socket().getLocalPort(); } InetSocketAddress address; public InetSocketAddress getRemoteAddress() { return address; } public void disconnect() throws IOException { mChannel.disconnect(); } DatagramChannelWrapper(DatagramChannel channel) throws IOException { super(channel); mChannel = channel; } @Override public int read(ByteBuffer buffer) throws IOException { if (!isConnected()) { int position = buffer.position(); address = (InetSocketAddress)mChannel.receive(buffer); if (address == null) return -1; return buffer.position() - position; } address = null; 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); } @Override public void shutdownOutput() { } @Override public void shutdownInput() { } @Override public long read(ByteBuffer[] byteBuffers) throws IOException { return mChannel.read(byteBuffers); } @Override public long read(ByteBuffer[] byteBuffers, int i, int i2) throws IOException { return mChannel.read(byteBuffers, i, i2); } @Override public Object getSocket() { return mChannel.socket(); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/FileDataEmitter.java ================================================ package com.koushikdutta.async; import com.koushikdutta.async.callback.DataCallback; import com.koushikdutta.async.util.StreamUtility; import java.io.File; import java.io.FileInputStream; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; /** * Created by koush on 5/22/13. */ public class FileDataEmitter extends DataEmitterBase { AsyncServer server; File file; public FileDataEmitter(AsyncServer server, File file) { this.server = server; this.file = file; paused = !server.isAffinityThread(); if (!paused) doResume(); } DataCallback callback; @Override public void setDataCallback(DataCallback callback) { this.callback = callback; } @Override public DataCallback getDataCallback() { return callback; } @Override public boolean isChunked() { return false; } boolean paused; @Override public void pause() { paused = true; } @Override public void resume() { paused = false; doResume(); } @Override protected void report(Exception e) { StreamUtility.closeQuietly(channel); super.report(e); } ByteBufferList pending = new ByteBufferList(); FileChannel channel; Runnable pumper = new Runnable() { @Override public void run() { try { if (channel == null) channel = new FileInputStream(file).getChannel(); if (!pending.isEmpty()) { Util.emitAllData(FileDataEmitter.this, pending); if (!pending.isEmpty()) return; } ByteBuffer b; do { b = ByteBufferList.obtain(8192); if (-1 == channel.read(b)) { report(null); return; } b.flip(); pending.add(b); Util.emitAllData(FileDataEmitter.this, pending); } while (pending.remaining() == 0 && !isPaused()); } catch (Exception e) { report(e); } } }; private void doResume() { server.post(pumper); } @Override public boolean isPaused() { return paused; } @Override public AsyncServer getServer() { return server; } @Override public void close() { try { channel.close(); } catch (Exception e) { } } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/FilteredDataEmitter.java ================================================ package com.koushikdutta.async; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.DataCallback; import com.koushikdutta.async.wrapper.DataEmitterWrapper; public class FilteredDataEmitter extends DataEmitterBase implements DataEmitter, DataCallback, DataEmitterWrapper, DataTrackingEmitter { private DataEmitter mEmitter; @Override public DataEmitter getDataEmitter() { return mEmitter; } @Override public void setDataEmitter(DataEmitter emitter) { if (mEmitter != null) { mEmitter.setDataCallback(null); } mEmitter = emitter; mEmitter.setDataCallback(this); mEmitter.setEndCallback(new CompletedCallback() { @Override public void onCompleted(Exception ex) { report(ex); } }); } @Override public int getBytesRead() { return totalRead; } @Override public DataTracker getDataTracker() { return tracker; } @Override public void setDataTracker(DataTracker tracker) { this.tracker = tracker; } private DataTracker tracker; private int totalRead; @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { if (closed) { // this emitter was closed but for some reason data is still being spewed... // eat it, nom nom. bb.recycle(); return; } if (bb != null) totalRead += bb.remaining(); Util.emitAllData(this, bb); if (bb != null) totalRead -= bb.remaining(); if (tracker != null && bb != null) tracker.onData(totalRead); // if there's data after the emitting, and it is paused... the underlying implementation // is obligated to cache the byte buffer list. } @Override public boolean isChunked() { return mEmitter.isChunked(); } @Override public void pause() { mEmitter.pause(); } @Override public void resume() { mEmitter.resume(); } @Override public boolean isPaused() { return mEmitter.isPaused(); } @Override public AsyncServer getServer() { return mEmitter.getServer(); } boolean closed; @Override public void close() { closed = true; if (mEmitter != null) mEmitter.close(); } @Override public String charset() { if (mEmitter == null) return null; return mEmitter.charset(); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/FilteredDataSink.java ================================================ package com.koushikdutta.async; public class FilteredDataSink extends BufferedDataSink { public FilteredDataSink(DataSink sink) { super(sink); setMaxBuffer(0); } public ByteBufferList filter(ByteBufferList bb) { return bb; } @Override protected void onDataAccepted(ByteBufferList bb) { ByteBufferList filtered = filter(bb); // filtering may return the same byte buffer, so watch for that. if (filtered != bb) { bb.recycle(); filtered.get(bb); } } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/HostnameResolutionException.java ================================================ package com.koushikdutta.async; public class HostnameResolutionException extends Exception { public HostnameResolutionException(String message) { super(message); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/LineEmitter.java ================================================ package com.koushikdutta.async; import com.koushikdutta.async.callback.DataCallback; import java.nio.ByteBuffer; import java.nio.charset.Charset; public class LineEmitter implements DataCallback { public interface StringCallback { void onStringAvailable(String s); } public LineEmitter() { this(null); } public LineEmitter(Charset charset) { this.charset = charset; } Charset charset; ByteBufferList data = new ByteBufferList(); StringCallback mLineCallback; public void setLineCallback(StringCallback callback) { mLineCallback = callback; } public StringCallback getLineCallback() { return mLineCallback; } @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { ByteBuffer buffer = ByteBuffer.allocate(bb.remaining()); while (bb.remaining() > 0) { byte b = bb.get(); if (b == '\n') { buffer.flip(); data.add(buffer); mLineCallback.onStringAvailable(data.readString(charset)); data = new ByteBufferList(); return; } else { buffer.put(b); } } buffer.flip(); data.add(buffer); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/PushParser.java ================================================ package com.koushikdutta.async; import android.util.Log; import com.koushikdutta.async.callback.DataCallback; import java.lang.reflect.Method; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; import java.util.Hashtable; import java.util.LinkedList; public class PushParser implements DataCallback { public interface ParseCallback { public void parsed(T data); } static abstract class Waiter { int length; public Waiter(int length) { this.length = length; } /** * Consumes received data, and/or returns next waiter to continue reading instead of this waiter. * @param bb received data, bb.remaining >= length * @return - a waiter that should continue reading right away, or null if this waiter is finished */ public abstract Waiter onDataAvailable(DataEmitter emitter, ByteBufferList bb); } static class IntWaiter extends Waiter { ParseCallback callback; public IntWaiter(ParseCallback callback) { super(4); this.callback = callback; } @Override public Waiter onDataAvailable(DataEmitter emitter, ByteBufferList bb) { callback.parsed(bb.getInt()); return null; } } static class ByteArrayWaiter extends Waiter { ParseCallback callback; public ByteArrayWaiter(int length, ParseCallback callback) { super(length); if (length <= 0) throw new IllegalArgumentException("length should be > 0"); this.callback = callback; } @Override public Waiter onDataAvailable(DataEmitter emitter, ByteBufferList bb) { byte[] bytes = new byte[length]; bb.get(bytes); callback.parsed(bytes); return null; } } static class LenByteArrayWaiter extends Waiter { private final ParseCallback callback; public LenByteArrayWaiter(ParseCallback callback) { super(4); this.callback = callback; } @Override public Waiter onDataAvailable(DataEmitter emitter, ByteBufferList bb) { int length = bb.getInt(); if (length == 0) { callback.parsed(new byte[0]); return null; } return new ByteArrayWaiter(length, callback); } } static class ByteBufferListWaiter extends Waiter { ParseCallback callback; public ByteBufferListWaiter(int length, ParseCallback callback) { super(length); if (length <= 0) throw new IllegalArgumentException("length should be > 0"); this.callback = callback; } @Override public Waiter onDataAvailable(DataEmitter emitter, ByteBufferList bb) { callback.parsed(bb.get(length)); return null; } } static class LenByteBufferListWaiter extends Waiter { private final ParseCallback callback; public LenByteBufferListWaiter(ParseCallback callback) { super(4); this.callback = callback; } @Override public Waiter onDataAvailable(DataEmitter emitter, ByteBufferList bb) { int length = bb.getInt(); return new ByteBufferListWaiter(length, callback); } } static class UntilWaiter extends Waiter { byte value; DataCallback callback; public UntilWaiter(byte value, DataCallback callback) { super(1); this.value = value; this.callback = callback; } @Override public Waiter onDataAvailable(DataEmitter emitter, ByteBufferList bb) { boolean found = true; ByteBufferList cb = new ByteBufferList(); while (bb.size() > 0) { ByteBuffer b = bb.remove(); b.mark(); int index = 0; while (b.remaining() > 0 && !(found = (b.get() == value))) { index++; } b.reset(); if (found) { bb.addFirst(b); bb.get(cb, index); // eat the one we're waiting on bb.get(); break; } else { cb.add(b); } } callback.onDataAvailable(emitter, cb); if (found) { return null; } else { return this; } } } private class TapWaiter extends Waiter { private final TapCallback callback; public TapWaiter(TapCallback callback) { super(0); this.callback = callback; } @Override public Waiter onDataAvailable(DataEmitter emitter, ByteBufferList bb) { Method method = getTap(callback); method.setAccessible(true); try { method.invoke(callback, args.toArray()); } catch (Exception e) { Log.e("PushParser", "Error while invoking tap callback", e); } args.clear(); return null; } } private Waiter noopArgWaiter = new Waiter(0) { @Override public Waiter onDataAvailable(DataEmitter emitter, ByteBufferList bb) { args.add(null); return null; } }; private Waiter byteArgWaiter = new Waiter(1) { @Override public Waiter onDataAvailable(DataEmitter emitter, ByteBufferList bb) { args.add(bb.get()); return null; } }; private Waiter shortArgWaiter = new Waiter(2) { @Override public Waiter onDataAvailable(DataEmitter emitter, ByteBufferList bb) { args.add(bb.getShort()); return null; } }; private Waiter intArgWaiter = new Waiter(4) { @Override public Waiter onDataAvailable(DataEmitter emitter, ByteBufferList bb) { args.add(bb.getInt()); return null; } }; private Waiter longArgWaiter = new Waiter(8) { @Override public Waiter onDataAvailable(DataEmitter emitter, ByteBufferList bb) { args.add(bb.getLong()); return null; } }; private ParseCallback byteArrayArgCallback = new ParseCallback() { @Override public void parsed(byte[] data) { args.add(data); } }; private ParseCallback byteBufferListArgCallback = new ParseCallback() { @Override public void parsed(ByteBufferList data) { args.add(data); } }; private ParseCallback stringArgCallback = new ParseCallback() { @Override public void parsed(byte[] data) { args.add(new String(data)); } }; DataEmitter mEmitter; private LinkedList mWaiting = new LinkedList(); private ArrayList args = new ArrayList(); ByteOrder order = ByteOrder.BIG_ENDIAN; public PushParser setOrder(ByteOrder order) { this.order = order; return this; } public PushParser(DataEmitter s) { mEmitter = s; mEmitter.setDataCallback(this); } public PushParser readInt(ParseCallback callback) { mWaiting.add(new IntWaiter(callback)); return this; } public PushParser readByteArray(int length, ParseCallback callback) { mWaiting.add(new ByteArrayWaiter(length, callback)); return this; } public PushParser readByteBufferList(int length, ParseCallback callback) { mWaiting.add(new ByteBufferListWaiter(length, callback)); return this; } public PushParser until(byte b, DataCallback callback) { mWaiting.add(new UntilWaiter(b, callback)); return this; } public PushParser readByte() { mWaiting.add(byteArgWaiter); return this; } public PushParser readShort() { mWaiting.add(shortArgWaiter); return this; } public PushParser readInt() { mWaiting.add(intArgWaiter); return this; } public PushParser readLong() { mWaiting.add(longArgWaiter); return this; } public PushParser readByteArray(int length) { return (length == -1) ? readLenByteArray() : readByteArray(length, byteArrayArgCallback); } public PushParser readLenByteArray() { mWaiting.add(new LenByteArrayWaiter(byteArrayArgCallback)); return this; } public PushParser readByteBufferList(int length) { return (length == -1) ? readLenByteBufferList() : readByteBufferList(length, byteBufferListArgCallback); } public PushParser readLenByteBufferList() { return readLenByteBufferList(byteBufferListArgCallback); } public PushParser readLenByteBufferList(ParseCallback callback) { mWaiting.add(new LenByteBufferListWaiter(callback)); return this; } public PushParser readString() { mWaiting.add(new LenByteArrayWaiter(stringArgCallback)); return this; } public PushParser noop() { mWaiting.add(noopArgWaiter); return this; } ByteBufferList pending = new ByteBufferList(); @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { bb.get(pending); while (mWaiting.size() > 0 && pending.remaining() >= mWaiting.peek().length) { pending.order(order); Waiter next = mWaiting.poll().onDataAvailable(emitter, pending); if (next != null) mWaiting.addFirst(next); } if (mWaiting.size() == 0) pending.get(bb); } public void tap(TapCallback callback) { mWaiting.add(new TapWaiter(callback)); } static Hashtable mTable = new Hashtable(); static Method getTap(TapCallback callback) { Method found = mTable.get(callback.getClass()); if (found != null) return found; for (Method method : callback.getClass().getMethods()) { if ("tap".equals(method.getName())) { mTable.put(callback.getClass(), method); return method; } } // try the proguard friendly route, take the first/only method // in case "tap" has been renamed Method[] candidates = callback.getClass().getDeclaredMethods(); if (candidates.length == 1) return candidates[0]; String fail = "-keep class * extends com.koushikdutta.async.TapCallback {\n" + " *;\n" + "}\n"; //null != "AndroidAsync: tap callback could not be found. Proguard? Use this in your proguard config:\n" + fail; throw new AssertionError(fail); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/SelectorWrapper.java ================================================ package com.koushikdutta.async; import java.io.Closeable; import java.io.IOException; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.util.Set; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; /** * Created by koush on 2/13/14. */ class SelectorWrapper implements Closeable { private Selector selector; public AtomicBoolean isWaking = new AtomicBoolean(false); Semaphore semaphore = new Semaphore(0); public Selector getSelector() { return selector; } public SelectorWrapper(Selector selector) { this.selector = selector; } public int selectNow() throws IOException { return selector.selectNow(); } public void select() throws IOException { select(0); } public void select(long timeout) throws IOException { try { semaphore.drainPermits(); selector.select(timeout); } finally { semaphore.release(Integer.MAX_VALUE); } } public Set keys() { return selector.keys(); } public Set selectedKeys() { return selector.selectedKeys(); } @Override public void close() throws IOException { selector.close(); } public boolean isOpen() { return selector.isOpen(); } public void wakeupOnce() { // see if it is selecting, ie, can't acquire a permit boolean selecting = !semaphore.tryAcquire(); selector.wakeup(); // if it was selecting, then the wakeup definitely worked. if (selecting) return; // now, we NEED to wait for the select to start to forcibly wake it. if (isWaking.getAndSet(true)) { selector.wakeup(); return; } try { waitForSelect(); selector.wakeup(); } finally { isWaking.set(false); } } public boolean waitForSelect() { // try to wake up 10 times for (int i = 0; i < 100; i++) { try { if (semaphore.tryAcquire(10, TimeUnit.MILLISECONDS)) { // successfully acquiring means the selector is NOT selecting, since select // will drain all permits. continue; } } catch (InterruptedException e) { // an InterruptedException means the acquire failed a select is in progress, // since it holds all permits return true; } } return false; } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/ServerSocketChannelWrapper.java ================================================ package com.koushikdutta.async; import java.io.IOException; import java.net.InetAddress; import java.nio.ByteBuffer; import java.nio.channels.ClosedChannelException; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; class ServerSocketChannelWrapper extends ChannelWrapper { ServerSocketChannel mChannel; @Override public void shutdownOutput() { } @Override public void shutdownInput() { } @Override public InetAddress getLocalAddress() { return mChannel.socket().getInetAddress(); } @Override public int getLocalPort() { return mChannel.socket().getLocalPort(); } ServerSocketChannelWrapper(ServerSocketChannel channel) throws IOException { super(channel); mChannel = channel; } @Override public int read(ByteBuffer buffer) throws IOException { final String msg = "Can't read ServerSocketChannel"; throw new IOException(msg); } @Override public boolean isConnected() { return false; } @Override public int write(ByteBuffer src) throws IOException { final String msg = "Can't write ServerSocketChannel"; 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"; throw new IOException(msg); } @Override public long read(ByteBuffer[] byteBuffers) throws IOException { final String msg = "Can't read ServerSocketChannel"; throw new IOException(msg); } @Override public long read(ByteBuffer[] byteBuffers, int i, int i2) throws IOException { final String msg = "Can't read ServerSocketChannel"; throw new IOException(msg); } @Override public Object getSocket() { return mChannel.socket(); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/SocketChannelWrapper.java ================================================ package com.koushikdutta.async; import java.io.IOException; import java.net.InetAddress; 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; @Override public InetAddress getLocalAddress() { return mChannel.socket().getLocalAddress(); } @Override public int getLocalPort() { return mChannel.socket().getLocalPort(); } 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); } @Override public void shutdownOutput() { try { mChannel.socket().shutdownOutput(); } catch (Exception e) { } } @Override public void shutdownInput() { try { mChannel.socket().shutdownInput(); } catch (Exception e) { } } @Override public long read(ByteBuffer[] byteBuffers) throws IOException { return mChannel.read(byteBuffers); } @Override public long read(ByteBuffer[] byteBuffers, int i, int i2) throws IOException { return mChannel.read(byteBuffers, i, i2); } @Override public Object getSocket() { return mChannel.socket(); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/TapCallback.java ================================================ package com.koushikdutta.async; public interface TapCallback { } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/ThreadQueue.java ================================================ package com.koushikdutta.async; import java.util.LinkedList; import java.util.WeakHashMap; import java.util.concurrent.Semaphore; class ThreadQueue extends LinkedList { final private static WeakHashMap mThreadQueues = new WeakHashMap(); static ThreadQueue getOrCreateThreadQueue(Thread thread) { ThreadQueue queue; synchronized (mThreadQueues) { queue = mThreadQueues.get(thread); if (queue == null) { queue = new ThreadQueue(); mThreadQueues.put(thread, queue); } } return queue; } static void release(AsyncSemaphore semaphore) { synchronized (mThreadQueues) { for (ThreadQueue threadQueue: mThreadQueues.values()) { if (threadQueue.waiter == semaphore) threadQueue.queueSemaphore.release(); } } } AsyncSemaphore waiter; Semaphore queueSemaphore = new Semaphore(0); @Override public boolean add(Runnable object) { synchronized (this) { return super.add(object); } } @Override public boolean remove(Object object) { synchronized (this) { return super.remove(object); } } @Override public Runnable remove() { synchronized (this) { if (this.isEmpty()) return null; return super.remove(); } } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/Util.java ================================================ package com.koushikdutta.async; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.DataCallback; import com.koushikdutta.async.callback.WritableCallback; import com.koushikdutta.async.util.Allocator; import com.koushikdutta.async.util.StreamUtility; import com.koushikdutta.async.wrapper.AsyncSocketWrapper; import com.koushikdutta.async.wrapper.DataEmitterWrapper; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; public class Util { public static boolean SUPRESS_DEBUG_EXCEPTIONS = false; public static void emitAllData(DataEmitter emitter, ByteBufferList list) { int remaining; DataCallback handler = null; while (!emitter.isPaused() && (handler = emitter.getDataCallback()) != null && (remaining = list.remaining()) > 0) { handler.onDataAvailable(emitter, list); if (remaining == list.remaining() && handler == emitter.getDataCallback() && !emitter.isPaused()) { // this is generally indicative of failure... // 1) The data callback has not changed // 2) no data was consumed // 3) the data emitter was not paused // call byteBufferList.recycle() or read all the data to prevent this assertion. // this is nice to have, as it identifies protocol or parsing errors. // System.out.println("Data: " + list.peekString()); System.out.println("handler: " + handler); list.recycle(); if (SUPRESS_DEBUG_EXCEPTIONS) return; throw new RuntimeException("mDataHandler failed to consume data, yet remains the mDataHandler."); } } if (list.remaining() != 0 && !emitter.isPaused()) { // not all the data was consumed... // call byteBufferList.recycle() or read all the data to prevent this assertion. // this is nice to have, as it identifies protocol or parsing errors. // System.out.println("Data: " + list.peekString()); System.out.println("handler: " + handler); System.out.println("emitter: " + emitter); list.recycle(); if (SUPRESS_DEBUG_EXCEPTIONS) return; // throw new AssertionError("Not all data was consumed by Util.emitAllData"); } } public static void pump(final InputStream is, final DataSink ds, final CompletedCallback callback) { pump(is, Integer.MAX_VALUE, ds, callback); } public static void pump(final InputStream is, final long max, final DataSink ds, final CompletedCallback callback) { final CompletedCallback wrapper = new CompletedCallback() { boolean reported; @Override public void onCompleted(Exception ex) { if (reported) return; reported = true; callback.onCompleted(ex); } }; final WritableCallback cb = new WritableCallback() { int totalRead = 0; private void cleanup() { ds.setClosedCallback(null); ds.setWriteableCallback(null); pending.recycle(); StreamUtility.closeQuietly(is); } ByteBufferList pending = new ByteBufferList(); Allocator allocator = new Allocator().setMinAlloc((int)Math.min(2 << 19, max)); @Override public void onWriteable() { try { do { if (!pending.hasRemaining()) { ByteBuffer b = allocator.allocate(); long toRead = Math.min(max - totalRead, b.capacity()); int read = is.read(b.array(), 0, (int)toRead); if (read == -1 || totalRead == max) { cleanup(); wrapper.onCompleted(null); return; } allocator.track(read); totalRead += read; b.position(0); b.limit(read); pending.add(b); } ds.write(pending); } while (!pending.hasRemaining()); } catch (Exception e) { cleanup(); wrapper.onCompleted(e); } } }; ds.setWriteableCallback(cb); ds.setClosedCallback(wrapper); cb.onWriteable(); } public static void pump(final DataEmitter emitter, final DataSink sink, final CompletedCallback callback) { final DataCallback dataCallback = new DataCallback() { @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { sink.write(bb); if (bb.remaining() > 0) emitter.pause(); } }; emitter.setDataCallback(dataCallback); sink.setWriteableCallback(new WritableCallback() { @Override public void onWriteable() { emitter.resume(); } }); final CompletedCallback wrapper = new CompletedCallback() { boolean reported; @Override public void onCompleted(Exception ex) { if (reported) return; reported = true; emitter.setDataCallback(null); emitter.setEndCallback(null); sink.setClosedCallback(null); sink.setWriteableCallback(null); callback.onCompleted(ex); } }; emitter.setEndCallback(wrapper); sink.setClosedCallback(new CompletedCallback() { @Override public void onCompleted(Exception ex) { if (ex == null) ex = new IOException("sink was closed before emitter ended"); wrapper.onCompleted(ex); } }); } public static void stream(AsyncSocket s1, AsyncSocket s2, CompletedCallback callback) { pump(s1, s2, callback); pump(s2, s1, callback); } public static void pump(final File file, final DataSink ds, final CompletedCallback callback) { try { if (file == null || ds == null) { callback.onCompleted(null); return; } final InputStream is = new FileInputStream(file); pump(is, ds, new CompletedCallback() { @Override public void onCompleted(Exception ex) { try { is.close(); callback.onCompleted(ex); } catch (IOException e) { callback.onCompleted(e); } } }); } catch (Exception e) { callback.onCompleted(e); } } public static void writeAll(final DataSink sink, final ByteBufferList bb, final CompletedCallback callback) { WritableCallback wc; sink.setWriteableCallback(wc = new WritableCallback() { @Override public void onWriteable() { sink.write(bb); if (bb.remaining() == 0 && callback != null) { sink.setWriteableCallback(null); callback.onCompleted(null); } } }); wc.onWriteable(); } public static void writeAll(DataSink sink, byte[] bytes, CompletedCallback callback) { ByteBuffer bb = ByteBufferList.obtain(bytes.length); bb.put(bytes); bb.flip(); ByteBufferList bbl = new ByteBufferList(); bbl.add(bb); writeAll(sink, bbl, callback); } public static T getWrappedSocket(AsyncSocket socket, Class wrappedClass) { if (wrappedClass.isInstance(socket)) return (T)socket; while (socket instanceof AsyncSocketWrapper) { socket = ((AsyncSocketWrapper)socket).getSocket(); if (wrappedClass.isInstance(socket)) return (T)socket; } return null; } public static DataEmitter getWrappedDataEmitter(DataEmitter emitter, Class wrappedClass) { if (wrappedClass.isInstance(emitter)) return emitter; while (emitter instanceof DataEmitterWrapper) { emitter = ((AsyncSocketWrapper)emitter).getSocket(); if (wrappedClass.isInstance(emitter)) return emitter; } return null; } public static void end(DataEmitter emitter, Exception e) { if (emitter == null) return; end(emitter.getEndCallback(), e); } public static void end(CompletedCallback end, Exception e) { if (end != null) end.onCompleted(e); } public static void writable(DataSink emitter) { if (emitter == null) return; writable(emitter.getWriteableCallback()); } public static void writable(WritableCallback writable) { if (writable != null) writable.onWriteable(); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/ZipDataSink.java ================================================ package com.koushikdutta.async; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import com.koushikdutta.async.callback.CompletedCallback; public class ZipDataSink extends FilteredDataSink { public ZipDataSink(DataSink sink) { super(sink); } ByteArrayOutputStream bout = new ByteArrayOutputStream(); ZipOutputStream zop = new ZipOutputStream(bout); public void putNextEntry(ZipEntry ze) throws IOException { zop.putNextEntry(ze); } public void closeEntry() throws IOException { zop.closeEntry(); } protected void report(Exception e) { CompletedCallback closed = getClosedCallback(); if (closed != null) closed.onCompleted(e); } @Override public void end() { try { zop.close(); } catch (IOException e) { report(e); return; } setMaxBuffer(Integer.MAX_VALUE); write(new ByteBufferList()); super.end(); } @Override public ByteBufferList filter(ByteBufferList bb) { try { if (bb != null) { while (bb.size() > 0) { ByteBuffer b = bb.remove(); ByteBufferList.writeOutputStream(zop, b); ByteBufferList.reclaim(b); } } ByteBufferList ret = new ByteBufferList(bout.toByteArray()); bout.reset(); return ret; } catch (IOException e) { report(e); return null; } finally { if (bb != null) bb.recycle(); } } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/callback/CompletedCallback.java ================================================ package com.koushikdutta.async.callback; public interface CompletedCallback { public class NullCompletedCallback implements CompletedCallback { @Override public void onCompleted(Exception ex) { } } public void onCompleted(Exception ex); } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/callback/ConnectCallback.java ================================================ package com.koushikdutta.async.callback; import com.koushikdutta.async.AsyncSocket; public interface ConnectCallback { public void onConnectCompleted(Exception ex, AsyncSocket socket); } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/callback/ContinuationCallback.java ================================================ package com.koushikdutta.async.callback; import com.koushikdutta.async.future.Continuation; public interface ContinuationCallback { public void onContinue(Continuation continuation, CompletedCallback next) throws Exception; } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/callback/DataCallback.java ================================================ package com.koushikdutta.async.callback; import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.DataEmitter; public interface DataCallback { public class NullDataCallback implements DataCallback { @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { bb.recycle(); } } public void onDataAvailable(DataEmitter emitter, ByteBufferList bb); } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/callback/ListenCallback.java ================================================ package com.koushikdutta.async.callback; import com.koushikdutta.async.AsyncServerSocket; import com.koushikdutta.async.AsyncSocket; public interface ListenCallback extends CompletedCallback { public void onAccepted(AsyncSocket socket); public void onListening(AsyncServerSocket socket); } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/callback/ResultCallback.java ================================================ package com.koushikdutta.async.callback; public interface ResultCallback { public void onCompleted(Exception e, S source, T result); } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/callback/SocketCreateCallback.java ================================================ package com.koushikdutta.async.callback; import com.koushikdutta.async.AsyncNetworkSocket; public interface SocketCreateCallback { void onSocketCreated(int localPort); } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/callback/ValueCallback.java ================================================ package com.koushikdutta.async.callback; /** * Created by koush on 7/5/16. */ public interface ValueCallback { void onResult(T value); } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/callback/ValueFunction.java ================================================ package com.koushikdutta.async.callback; public interface ValueFunction { T getValue() throws Exception; } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/callback/WritableCallback.java ================================================ package com.koushikdutta.async.callback; public interface WritableCallback { public void onWriteable(); } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/dns/Dns.java ================================================ package com.koushikdutta.async.dns; import com.koushikdutta.async.AsyncDatagramSocket; import com.koushikdutta.async.AsyncServer; import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.callback.DataCallback; import com.koushikdutta.async.future.Cancellable; import com.koushikdutta.async.future.Future; import com.koushikdutta.async.future.FutureCallback; import com.koushikdutta.async.future.SimpleFuture; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Random; /** * Created by koush on 10/20/13. */ public class Dns { public static Future lookup(String host) { return lookup(AsyncServer.getDefault(), host, false, null); } private static int setFlag(int flags, int value, int offset) { return flags | (value << offset); } private static int setQuery(int flags) { return setFlag(flags, 0, 0); } private static int setRecursion(int flags) { return setFlag(flags, 1, 8); } private static void addName(ByteBuffer bb, String name) { String[] parts = name.split("\\."); for (String part: parts) { bb.put((byte)part.length()); bb.put(part.getBytes()); } bb.put((byte)0); } public static Future lookup(AsyncServer server, String host) { return lookup(server, host, false, null); } public static Cancellable multicastLookup(AsyncServer server, String host, FutureCallback callback) { return lookup(server, host, true, callback); } public static Cancellable multicastLookup(String host, FutureCallback callback) { return multicastLookup(AsyncServer.getDefault(), host, callback); } public static Future lookup(AsyncServer server, String host, final boolean multicast, final FutureCallback callback) { if (!server.isAffinityThread()) { SimpleFuture ret = new SimpleFuture<>(); server.post(() -> ret.setComplete(lookup(server, host, multicast, callback))); return ret; } ByteBuffer packet = ByteBufferList.obtain(1024).order(ByteOrder.BIG_ENDIAN); short id = (short)new Random().nextInt(); short flags = (short)setQuery(0); if (!multicast) flags = (short)setRecursion(flags); packet.putShort(id); packet.putShort(flags); // number questions packet.putShort(multicast ? (short)1 : (short)2); // number answer rr packet.putShort((short)0); // number authority rr packet.putShort((short)0); // number additional rr packet.putShort((short)0); addName(packet, host); // query packet.putShort(multicast ? (short)12 : (short)1); // request internet address packet.putShort((short)1); if (!multicast) { addName(packet, host); // AAAA query packet.putShort((short) 28); // request internet address packet.putShort((short)1); } packet.flip(); try { final AsyncDatagramSocket dgram; // todo, use the dns server... if (!multicast) { dgram = server.connectDatagram(new InetSocketAddress("8.8.8.8", 53)); } else { // System.out.println("multicast dns..."); dgram = AsyncServer.getDefault().openDatagram(null, 0, true); Field field = DatagramSocket.class.getDeclaredField("impl"); field.setAccessible(true); Object impl = field.get(dgram.getSocket()); Method method = impl.getClass().getDeclaredMethod("join", InetAddress.class); method.setAccessible(true); method.invoke(impl, InetAddress.getByName("224.0.0.251")); ((DatagramSocket)dgram.getSocket()).setBroadcast(true); } final SimpleFuture ret = new SimpleFuture() { @Override protected void cleanup() { super.cleanup(); // System.out.println("multicast dns cleanup..."); dgram.close(); } }; dgram.setDataCallback(new DataCallback() { @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { try { // System.out.println(dgram.getRemoteAddress()); DnsResponse response = DnsResponse.parse(bb); // System.out.println(response); response.source = dgram.getRemoteAddress(); if (!multicast) { dgram.close(); ret.setComplete(response); } else { callback.onCompleted(null, response); } } catch (Exception e) { } bb.recycle(); } }); if (!multicast) dgram.write(new ByteBufferList(packet)); else dgram.send(new InetSocketAddress("224.0.0.251", 5353), packet); return ret; } catch (Exception e) { SimpleFuture ret = new SimpleFuture(); ret.setComplete(e); if (multicast) callback.onCompleted(e, null); return ret; } } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/dns/DnsResponse.java ================================================ package com.koushikdutta.async.dns; import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.http.Multimap; import java.net.InetAddress; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; /** * Created by koush on 10/20/13. */ public class DnsResponse { public ArrayList addresses = new ArrayList(); public ArrayList names = new ArrayList(); public Multimap txt = new Multimap(); public InetSocketAddress source; private static String parseName(ByteBufferList bb, ByteBuffer backReference) { bb.order(ByteOrder.BIG_ENDIAN); String ret = ""; int len; while (0 != (len = bb.get() & 0x00FF)) { // compressed if ((len & 0x00c0) == 0x00c0) { int offset = ((len & ~0xFFFFFFc0) << 8) | (bb.get() & 0x00FF); if (ret.length() > 0) ret += "."; ByteBufferList sub = new ByteBufferList(); ByteBuffer duplicate = backReference.duplicate(); duplicate.get(new byte[offset]); sub.add(duplicate); return ret + parseName(sub, backReference); } byte[] bytes = new byte[len]; bb.get(bytes); if (ret.length() > 0) ret += "."; ret += new String(bytes); } return ret; } public static DnsResponse parse(ByteBufferList bb) { ByteBuffer b = bb.getAll(); bb.add(b.duplicate()); // naive parsing... bb.order(ByteOrder.BIG_ENDIAN); // id bb.getShort(); // flags bb.getShort(); // number questions int questions = bb.getShort(); // number answer rr int answers = bb.getShort(); // number authority rr int authorities = bb.getShort(); // number additional rr int additionals = bb.getShort(); for (int i = 0; i < questions; i++) { parseName(bb, b); // type bb.getShort(); // class bb.getShort(); } DnsResponse response = new DnsResponse(); for (int i = 0; i < answers; i++) { String name = parseName(bb, b); // type int type = bb.getShort(); // class int clazz = bb.getShort(); // ttl int ttl = bb.getInt(); // length of address int length = bb.getShort(); try { if (type == 1) { // data byte[] data = new byte[length]; bb.get(data); response.addresses.add(InetAddress.getByAddress(data)); } else if (type == 0x000c) { response.names.add(parseName(bb, b)); } else if (type == 16) { ByteBufferList txt = new ByteBufferList(); bb.get(txt, length); response.parseTxt(txt); } else { bb.get(new byte[length]); } } catch (Exception e) { // e.printStackTrace(); } } // authorities for (int i = 0; i < authorities; i++) { String name = parseName(bb, b); // type int type = bb.getShort(); // class int clazz = bb.getShort(); // ttl int ttl = bb.getInt(); // length of address int length = bb.getShort(); try { bb.get(new byte[length]); } catch (Exception e) { // e.printStackTrace(); } } // additionals for (int i = 0; i < additionals; i++) { String name = parseName(bb, b); // type int type = bb.getShort(); // class int clazz = bb.getShort(); // ttl int ttl = bb.getInt(); // length of address int length = bb.getShort(); try { if (type == 16) { ByteBufferList txt = new ByteBufferList(); bb.get(txt, length); response.parseTxt(txt); } else { bb.get(new byte[length]); } } catch (Exception e) { // e.printStackTrace(); } } return response; } void parseTxt(ByteBufferList bb) { while (bb.hasRemaining()) { int length = (int)bb.get() & 0x00FF; byte [] bytes = new byte[length]; bb.get(bytes); String string = new String(bytes); String[] pair = string.split("="); txt.add(pair[0], pair[1]); } } @Override public String toString() { String ret = "addresses:\n"; for (InetAddress address: addresses) ret += address.toString() + "\n"; ret += "names:\n"; for (String name: names) ret += name + "\n"; return ret; } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/future/Cancellable.java ================================================ package com.koushikdutta.async.future; public interface Cancellable { /** * Check whether this asynchronous operation completed successfully. * @return */ boolean isDone(); /** * Check whether this asynchronous operation has been cancelled. * @return */ boolean isCancelled(); /** * Attempt to cancel this asynchronous operation. * @return The return value is whether the operation cancelled successfully. */ boolean cancel(); } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/future/Continuation.java ================================================ package com.koushikdutta.async.future; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.ContinuationCallback; import java.util.LinkedList; public class Continuation extends SimpleCancellable implements ContinuationCallback, Runnable, Cancellable { CompletedCallback callback; Runnable cancelCallback; public CompletedCallback getCallback() { return callback; } public void setCallback(CompletedCallback callback) { this.callback = callback; } public Runnable getCancelCallback() { return cancelCallback; } public void setCancelCallback(Runnable cancelCallback) { this.cancelCallback = cancelCallback; } public void setCancelCallback(final Cancellable cancel) { if (cancel == null) { this.cancelCallback = null; return; } this.cancelCallback = new Runnable() { @Override public void run() { cancel.cancel(); } }; } public Continuation() { this(null); } public Continuation(CompletedCallback callback) { this(callback, null); } public Continuation(CompletedCallback callback, Runnable cancelCallback) { this.cancelCallback = cancelCallback; this.callback = callback; } private CompletedCallback wrap() { return new CompletedCallback() { boolean mThisCompleted; @Override public void onCompleted(Exception ex) { // onCompleted may be called more than once... buggy code. // only accept the first (timeouts, etc) if (mThisCompleted) return; mThisCompleted = true; waiting = false; if (ex == null) { next(); return; } reportCompleted(ex); } }; } void reportCompleted(Exception ex) { if (!setComplete()) return; if (callback != null) callback.onCompleted(ex); } LinkedList mCallbacks = new LinkedList(); private ContinuationCallback hook(ContinuationCallback callback) { if (callback instanceof DependentCancellable) { DependentCancellable child = (DependentCancellable)callback; child.setParent(this); } return callback; } public Continuation add(ContinuationCallback callback) { mCallbacks.add(hook(callback)); return this; } public Continuation insert(ContinuationCallback callback) { mCallbacks.add(0, hook(callback)); return this; } public Continuation add(final DependentFuture future) { future.setParent(this); add(new ContinuationCallback() { @Override public void onContinue(Continuation continuation, CompletedCallback next) throws Exception { future.get(); next.onCompleted(null); } }); return this; } private boolean inNext; private boolean waiting; private void next() { if (inNext) return; while (mCallbacks.size() > 0 && !waiting && !isDone() && !isCancelled()) { ContinuationCallback cb = mCallbacks.remove(); try { inNext = true; waiting = true; cb.onContinue(this, wrap()); } catch (Exception e) { reportCompleted(e); } finally { inNext = false; } } if (waiting) return; if (isDone()) return; if (isCancelled()) return; reportCompleted(null); } @Override public boolean cancel() { if (!super.cancel()) return false; if (cancelCallback != null) cancelCallback.run(); return true; } boolean started; public Continuation start() { if (started) throw new IllegalStateException("already started"); started = true; next(); return this; } @Override public void onContinue(Continuation continuation, CompletedCallback next) throws Exception { setCallback(next); start(); } @Override public void run() { start(); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/future/Converter.java ================================================ package com.koushikdutta.async.future; import android.text.TextUtils; import com.koushikdutta.async.ByteBufferList; import org.json.JSONObject; import java.io.InvalidObjectException; import java.nio.ByteBuffer; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.HashSet; import java.util.LinkedHashMap; public class Converter { public static Converter convert(Future future, String mime) { return new Converter<>(future, mime); } public static Converter convert(Future future) { return convert(future, null); } static class MimedData { public MimedData(T data, String mime) { this.data = data; this.mime = mime; } T data; String mime; } static class MultiTransformer extends MultiTransformFuture>, MimedData>> { TypeConverter converter; String converterMime; int distance; public MultiTransformer(TypeConverter converter, String converterMime, int distance) { this.converter = converter; this.converterMime = converterMime; this.distance = distance; } @Override protected void transform(MimedData> converting) { // transform will only ever be called once, and is called immediately, // the transform is on the future itself, and not a pending value. // so there's no risk of running the converter twice. final String mime = converting.mime; // this future will receive the eventual actual value. final MultiFuture converted = new MultiFuture<>(); // this marks the conversion as "complete". the conversion will start // as soon as the value is ready. setComplete(new MimedData<>(converted, mimeReplace(mime, converterMime))); // wait on the incoming value and convert it converting.data.thenConvert(data -> converter.convert(data, mime)). setCallback((e, result1) -> { if (e != null) converted.setComplete(e); else converted.setComplete(result1); }); } } static abstract class EnsureHashMap extends LinkedHashMap { synchronized V ensure(K k) { if (!containsKey(k)) { put(k, makeDefault()); } return get(k); } protected abstract V makeDefault(); } static class MimedType { MimedType(Class type, String mime) { this.type = type; this.mime = mime; } Class type; String mime; @Override public int hashCode() { return type.hashCode() ^ mime.hashCode(); } @Override public boolean equals(Object obj) { MimedType other = (MimedType)obj; return type.equals(other.type) && mime.equals(other.mime); } // check if this mimed type is the same or more specific than this mimed type public boolean isTypeOf(MimedType other) { // check the type, this type must be less specific than the other type if (!this.type.isAssignableFrom(other.type)) return false; return isTypeOf(other.mime); } public String primary() { return mime.split("/")[0]; } public String secondary() { return mime.split("/")[1]; } // check if this mimed type is convertible to another mimed type public boolean isTypeOf(String mime) { String[] otherParts = mime.split("/"); String[] myParts = this.mime.split("/"); // ensure the other type is the same OR this type is fine with a wildcard if (!"*".equals(myParts[0]) && !otherParts[0].equals(myParts[0])) return false; if (!"*".equals(myParts[1]) && !otherParts[1].equals(myParts[1])) return false; return true; } @Override public String toString() { return type.getSimpleName() + " " + mime; } } static class ConverterTransformers extends LinkedHashMap, MultiTransformer> { } static class Converters extends EnsureHashMap, ConverterTransformers> { @Override protected ConverterTransformers makeDefault() { return new ConverterTransformers(); } private static void add(ConverterTransformers set, ConverterTransformers more) { if (more == null) return; set.putAll(more); } public ConverterTransformers getAll(MimedType mimedType) { ConverterTransformers ret = new ConverterTransformers<>(); for (MimedType candidate: keySet()) { if (candidate.isTypeOf(mimedType)) add(ret, get(candidate)); } return ret; } } Converters outputs; protected ConverterEntries getConverters() { return new ConverterEntries(Converters); } MultiFuture future = new MultiFuture<>(); String futureMime; protected Converter(Future future, String mime) { if (TextUtils.isEmpty(mime)) mime = MIME_ALL; this.futureMime = mime; this.future.setComplete(future); } synchronized private final Future to(Object value, Class clazz, String mime) { if (clazz.isInstance(value)) return new SimpleFuture<>((T) value); return to(value.getClass(), clazz, mime); } synchronized private final Future to(Class fromClass, Class clazz, String mime) { if (TextUtils.isEmpty(mime)) mime = MIME_ALL; if (outputs == null) { outputs = new Converters<>(); ConverterEntries converters = getConverters(); for (ConverterEntry entry: converters.list) { outputs.ensure(entry.from).put(entry.to, new MultiTransformer<>(entry.typeConverter, entry.to.mime, entry.distance)); } } MimedType target = new MimedType<>(clazz, mime); ArrayDeque bestMatch = new ArrayDeque<>(); ArrayDeque currentPath = new ArrayDeque<>(); if (search(target, bestMatch, currentPath, new MimedType(fromClass, futureMime), new HashSet())) { PathInfo current = bestMatch.removeFirst(); new SimpleFuture<>(new MimedData<>((Future)future, futureMime)).setCallback(current.transformer); while (!bestMatch.isEmpty()) { PathInfo next = bestMatch.removeFirst(); current.transformer.setCallback(next.transformer); current = next; } return ((MultiTransformer)current.transformer).then(from -> from.data); } return new SimpleFuture<>(new InvalidObjectException("unable to find converter")); } static class PathInfo { MultiTransformer transformer; String mime; MimedType candidate; static int distance(ArrayDeque path) { int distance = 0; for (PathInfo entry: path) { distance += entry.transformer.distance; } return distance; } } static String mimeReplace(String mime1, String mime2) { String[] parts = mime2.split("/"); String[] myParts = mime1.split("/"); // a wildcard mime converter adopts the mime of the converted type String primary = !"*".equals(parts[0]) ? parts[0] : myParts[0]; String secondary = !"*".equals(parts[1]) ? parts[1] : myParts[1]; return primary + "/" + secondary; } public final Future to(Class clazz) { return to(clazz, null); } private boolean search(MimedType target, ArrayDeque bestMatch, ArrayDeque currentPath, MimedType currentSearch, HashSet searched) { if (target.isTypeOf(currentSearch)) { bestMatch.clear(); bestMatch.addAll(currentPath); return true; } // the current path must have potential to be better than the best match if (!bestMatch.isEmpty() && PathInfo.distance(currentPath) >= PathInfo.distance(bestMatch)) return false; // prevent reentrancy if (searched.contains(currentSearch)) return false; boolean found = false; searched.add(currentSearch); ConverterTransformers converterTransformers = outputs.getAll(currentSearch); for (MimedType candidate: converterTransformers.keySet()) { // this simulates the mime results of a transform MimedType newSearch = new MimedType(candidate.type, mimeReplace(currentSearch.mime, candidate.mime)); PathInfo path = new PathInfo(); path.transformer = converterTransformers.get(candidate); path.mime = newSearch.mime; path.candidate = candidate; currentPath.addLast(path); try { found |= search(target, bestMatch, currentPath, newSearch, searched); } finally { currentPath.removeLast(); } } if (found) { // if this resulted in a success, // clear this from the currentSearch list, because we know this leads // to a potential solution. maybe we can arrive here faster. searched.remove(currentSearch); } return found; } private static final String MIME_ALL = "*/*"; public Future to(Class clazz, String mime) { return future.then(from -> to(from, clazz, mime)); } static class ConverterEntry { ConverterEntry(Class from, String fromMime, Class to, String toMime, int distance, TypeConverter typeConverter) { this.from = new MimedType<>(from, fromMime); this.to = new MimedType<>(to, toMime); this.distance = distance; this.typeConverter = typeConverter; } MimedType from; MimedType to; int distance; TypeConverter typeConverter; @Override public int hashCode() { return from.hashCode() ^ to.hashCode(); } @Override public boolean equals(Object obj) { ConverterEntry other = (ConverterEntry)obj; return from.equals(other.from) && to.equals(other.to); } } public static class ConverterEntries { public ArrayList list = new ArrayList<>(); public ConverterEntries() { } public ConverterEntries(ConverterEntries other) { list.addAll(other.list); } public synchronized void addConverter(Class from, String fromMime, Class to, String toMime, TypeConverter typeConverter) { addConverter(from, fromMime, to, toMime, 1, typeConverter); } public synchronized void addConverter(Class from, String fromMime, Class to, String toMime, int distance, TypeConverter typeConverter) { if (TextUtils.isEmpty(fromMime)) fromMime = MIME_ALL; if (TextUtils.isEmpty(toMime)) toMime = MIME_ALL; list.add(new ConverterEntry<>(from, fromMime, to, toMime, distance, typeConverter)); } public synchronized boolean removeConverter(TypeConverter typeConverter) { for (ConverterEntry entry: list) { if (entry.typeConverter == typeConverter) return list.remove(entry); } return false; } } public final static ConverterEntries Converters = new ConverterEntries(); static { // ensure byte buffer operations are idempotent. do deep copies. final TypeConverter ByteArrayToByteBufferList = (from, fromMime) -> new SimpleFuture<>(new ByteBufferList(ByteBufferList.deepCopy(ByteBuffer.wrap(from)))); final TypeConverter ByteBufferListToByteArray = (from, fromMime) -> new SimpleFuture<>(from.getAllByteArray()); final TypeConverter ByteBufferListToByteBuffer = (from, fromMime) -> new SimpleFuture<>(from.getAll()); final TypeConverter ByteBufferListToString = (from, fromMime) -> new SimpleFuture<>(from.peekString()); final TypeConverter ByteArrayToByteBuffer = (from, fromMime) -> new SimpleFuture<>(ByteBufferList.deepCopy(ByteBuffer.wrap(from))); final TypeConverter ByteBufferToByteBufferList = (from, fromMime) -> new SimpleFuture<>(new ByteBufferList(ByteBufferList.deepCopy(from))); final TypeConverter StringToByteArray = (from, fromMime) -> new SimpleFuture<>(from.getBytes()); final TypeConverter StringToJSONObject = (from, fromMime) -> new SimpleFuture<>(from).thenConvert(JSONObject::new); final TypeConverter JSONObjectToString = (from, fromMime) -> new SimpleFuture<>(from).thenConvert(JSONObject::toString); final TypeConverter ByteArrayToString = (from, fromMime) -> new SimpleFuture<>(new String(from)); Converters.addConverter(ByteBuffer.class, null, ByteBufferList.class, null, ByteBufferToByteBufferList); Converters.addConverter(String.class, null, byte[].class, "text/plain", StringToByteArray); Converters.addConverter(byte[].class, null, ByteBufferList.class, null, ByteArrayToByteBufferList); Converters.addConverter(ByteBufferList.class, null, byte[].class, null, ByteBufferListToByteArray); Converters.addConverter(ByteBufferList.class, null, ByteBuffer.class, null, ByteBufferListToByteBuffer); Converters.addConverter(ByteBufferList.class, "text/plain", String.class, null, ByteBufferListToString); Converters.addConverter(byte[].class, null, ByteBuffer.class, null, ByteArrayToByteBuffer); Converters.addConverter(String.class, "application/json", JSONObject.class, null, StringToJSONObject); Converters.addConverter(JSONObject.class, null, String.class, "application/json", JSONObjectToString); Converters.addConverter(byte[].class, "text/plain", String.class, null, ByteArrayToString); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/future/DependentCancellable.java ================================================ package com.koushikdutta.async.future; public interface DependentCancellable extends Cancellable { boolean setParent(Cancellable parent); } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/future/DependentFuture.java ================================================ package com.koushikdutta.async.future; public interface DependentFuture extends Future, DependentCancellable { } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/future/DoneCallback.java ================================================ package com.koushikdutta.async.future; public interface DoneCallback { void done(Exception e, T result) throws Exception; } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/future/FailCallback.java ================================================ package com.koushikdutta.async.future; public interface FailCallback { /** * Callback that is invoked when a future completes with an error. * The error should be rethrown to pass it along. * @param e * @throws Exception */ void fail(Exception e) throws Exception; } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/future/FailConvertCallback.java ================================================ package com.koushikdutta.async.future; public interface FailConvertCallback { /** * Callback that is invoked when a future completes with an error. * The error should be rethrown, or a new value should be returned. * @param e * @return * @throws Exception */ T fail(Exception e) throws Exception; } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/future/FailRecoverCallback.java ================================================ package com.koushikdutta.async.future; public interface FailRecoverCallback { /** * Callback that is invoked when a future completes with an error. * The error should be rethrown, or a new future value should be returned. * @param e * @return * @throws Exception */ Future fail(Exception e) throws Exception; } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/future/Future.java ================================================ package com.koushikdutta.async.future; import java.util.concurrent.Executor; public interface Future extends Cancellable, java.util.concurrent.Future { /** * Set a callback to be invoked when this Future completes. * @param callback * @return */ void setCallback(FutureCallback callback); /** * Set a callback to be invoked when the Future completes * with an error or a result. * The existing error or result will be passed down the chain, or a new error * may be thrown. * @param done * @return */ Future done(DoneCallback done); /** * Set a callback to be invoked when this Future completes successfully. * @param callback * @return A future that will resolve once the success callback completes, * which may contain any errors thrown by the success callback. */ Future success(SuccessCallback callback); /** * Set a callback to be invoked when this Future completes successfully. * @param then * @param * @return A future containing all exceptions that happened prior or during * the callback, or the successful result. */ Future then(ThenFutureCallback then); /** * Set a callback to be invoked when this Future completes successfully. * @param then * @param * @return A future containing all exceptions that happened prior or during * the callback, or the successful result. */ Future thenConvert(ThenCallback then); /** * Set a callback to be invoked when this future completes with a failure. * The failure can be observered and rethrown, otherwise it is considered handled. * The exception will be nulled for subsequent callbacks in the chain. * @param fail * @return */ Future fail(FailCallback fail); /** * Set a callback to be invoked when this future completes with a failure. * The failure can be observered and rethrown, or handled by returning * a new fallback value of the same type. * @param fail * @return */ Future failConvert(FailConvertCallback fail); /** * Set a callback to be invoked when this future completes with a failure. * The failure should be observered and rethrown, or handled by returning * a new future of the same type. * @param fail * @return */ Future failRecover(FailRecoverCallback fail); /** * Get the result, if any. Returns null if still in progress. * @return */ T tryGet(); /** * Get the exception, if any. Returns null if still in progress. * @return */ Exception tryGetException(); /** * Get the result on the executor thread. * @param executor * @return */ default Future executorThread(Executor executor) { SimpleFuture ret = new SimpleFuture<>(); executor.execute(() -> ret.setComplete(Future.this)); return ret; } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/future/FutureCallback.java ================================================ package com.koushikdutta.async.future; /** * Created by koush on 5/20/13. */ public interface FutureCallback { /** * onCompleted is called by the Future with the result or exception of the asynchronous operation. * @param e Exception encountered by the operation * @param result Result returned from the operation */ public void onCompleted(Exception e, T result); } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/future/FutureRunnable.java ================================================ package com.koushikdutta.async.future; /** * Created by koush on 12/22/13. */ public interface FutureRunnable { T run() throws Exception; } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/future/FutureThread.java ================================================ package com.koushikdutta.async.future; import java.util.concurrent.ExecutorService; /** * Created by koush on 12/22/13. */ public class FutureThread extends SimpleFuture { public FutureThread(final FutureRunnable runnable) { this(runnable, "FutureThread"); } public FutureThread(final ExecutorService pool, final FutureRunnable runnable) { pool.submit(new Runnable() { @Override public void run() { try { setComplete(runnable.run()); } catch (Exception e) { setComplete(e); } } }); } public FutureThread(final FutureRunnable runnable, String name) { new Thread(new Runnable() { @Override public void run() { try { setComplete(runnable.run()); } catch (Exception e) { setComplete(e); } } }, name).start(); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/future/Futures.java ================================================ package com.koushikdutta.async.future; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import java.util.List; public class Futures { public static Future> waitAll(final List> futures) { final ArrayList results = new ArrayList<>(); final SimpleFuture> ret = new SimpleFuture<>(); if (futures.isEmpty()) { ret.setComplete(results); return ret; } FutureCallback cb = new FutureCallback() { int count = 0; @Override public void onCompleted(Exception e, T result) { results.add(result); count++; if (count < futures.size()) futures.get(count).setCallback(this); else ret.setComplete(results); } }; futures.get(0).setCallback(cb); return ret; } public static Future> waitAll(final Future... futures) { return waitAll(Arrays.asList(futures)); } private static void loopUntil(final Iterator values, ThenFutureCallback callback, SimpleFuture ret, Exception lastException) { while (values.hasNext()) { try { callback.then(values.next()) .success(ret::setComplete) .fail(e -> loopUntil(values, callback, ret, e)); return; } catch (Exception e) { lastException = e; } } if (lastException == null) ret.setComplete(new Exception("empty list")); else ret.setComplete(lastException); } public static Future loopUntil(final Iterable values, ThenFutureCallback callback) { SimpleFuture ret = new SimpleFuture<>(); loopUntil(values.iterator(), callback, ret, null); return ret; } public static Future loopUntil(final F[] values, ThenFutureCallback callback) { return loopUntil(Arrays.asList(values), callback); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/future/HandlerFuture.java ================================================ package com.koushikdutta.async.future; import android.os.Handler; import android.os.Looper; /** * Created by koush on 12/25/13. */ public class HandlerFuture extends SimpleFuture { Handler handler; public HandlerFuture() { Looper looper = Looper.myLooper(); if (looper == null) looper = Looper.getMainLooper(); handler = new Handler(looper); } @Override public void setCallback(final FutureCallback callback) { FutureCallback wrapped = new FutureCallback() { @Override public void onCompleted(final Exception e, final T result) { if (Looper.myLooper() == handler.getLooper()) { callback.onCompleted(e, result); return; } handler.post(new Runnable() { @Override public void run() { onCompleted(e, result); } }); } }; super.setCallback(wrapped); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/future/MultiFuture.java ================================================ package com.koushikdutta.async.future; import java.util.ArrayList; /** * Created by koush on 2/25/14. */ public class MultiFuture extends SimpleFuture { private ArrayList> internalCallbacks; public MultiFuture() { } public MultiFuture(T value) { super(value); } public MultiFuture(Exception e) { super(e); } public MultiFuture(Future future) { super(future); } private final FutureCallbackInternal internalCallback = (e, result, callsite) -> { ArrayList> callbacks; synchronized (MultiFuture.this) { callbacks = MultiFuture.this.internalCallbacks; MultiFuture.this.internalCallbacks = null; } if (callbacks == null) return; for (FutureCallbackInternal cb : callbacks) { cb.onCompleted(e, result, callsite); } }; @Override protected void setCallbackInternal(FutureCallsite callsite, FutureCallbackInternal internalCallback) { synchronized (this) { if (internalCallback != null) { if (internalCallbacks == null) internalCallbacks = new ArrayList<>(); internalCallbacks.add(internalCallback); } } // so, there is a race condition where this internal callback could get // executed twice, if two callbacks are added at the same time. // however, it doesn't matter, as the actual retrieval and nulling // of the callback list is done in another sync block. // one of the invocations will actually invoke all the callbacks, // while the other will not get a list back. // race: // 1-ADD // 2-ADD // 1-INVOKE LIST // 2-INVOKE NULL super.setCallbackInternal(callsite, this.internalCallback); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/future/MultiTransformFuture.java ================================================ package com.koushikdutta.async.future; public abstract class MultiTransformFuture extends MultiFuture implements FutureCallback { @Override public void onCompleted(Exception e, F result) { if (isCancelled()) return; if (e != null) { error(e); return; } try { transform(result); } catch (Exception ex) { error(ex); } } protected void error(Exception e) { setComplete(e); } protected abstract void transform(F result) throws Exception; } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/future/SimpleCancellable.java ================================================ package com.koushikdutta.async.future; public class SimpleCancellable implements DependentCancellable { boolean complete; @Override public boolean isDone() { return complete; } protected void cancelCleanup() { } protected void cleanup() { } protected void completeCleanup() { } public boolean setComplete() { synchronized (this) { if (cancelled) return false; if (complete) { // don't allow a Cancellable to complete twice... return false; } complete = true; parent = null; } completeCleanup(); cleanup(); return true; } @Override public boolean cancel() { Cancellable parent; synchronized (this) { if (complete) return false; if (cancelled) return true; cancelled = true; parent = this.parent; // null out the parent to allow garbage collection this.parent = null; } if (parent != null) parent.cancel(); cancelCleanup(); cleanup(); return true; } boolean cancelled; private Cancellable parent; @Override public boolean setParent(Cancellable parent) { synchronized (this) { if (isDone()) return false; this.parent = parent; return true; } } @Override public boolean isCancelled() { synchronized (this) { return cancelled || (parent != null && parent.isCancelled()); } } public static final Cancellable COMPLETED = new SimpleCancellable() { { setComplete(); } }; public static final Cancellable CANCELLED = new SimpleCancellable() { { cancel(); } }; public Cancellable reset() { cancel(); complete = false; cancelled = false; return this; } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/future/SimpleFuture.java ================================================ package com.koushikdutta.async.future; import com.koushikdutta.async.AsyncSemaphore; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; public class SimpleFuture extends SimpleCancellable implements DependentFuture { private AsyncSemaphore waiter; private Exception exception; private T result; private boolean silent; private FutureCallbackInternal internalCallback; protected interface FutureCallbackInternal { void onCompleted(Exception e, T result, FutureCallsite next); } public SimpleFuture() { } public SimpleFuture(T value) { setComplete(value); } public SimpleFuture(Exception e) { setComplete(e); } public SimpleFuture(Future future) { setComplete(future); } @Override public boolean cancel(boolean mayInterruptIfRunning) { return cancel(); } private boolean cancelInternal(boolean silent) { if (!super.cancel()) return false; // still need to release any pending waiters FutureCallbackInternal internalCallback; synchronized (this) { exception = new CancellationException(); releaseWaiterLocked(); internalCallback = handleInternalCompleteLocked(); this.silent = silent; } handleCallbackUnlocked(null, internalCallback); return true; } public boolean cancelSilently() { return cancelInternal(true); } @Override public boolean cancel() { return cancelInternal(silent); } @Override public T get() throws InterruptedException, ExecutionException { AsyncSemaphore waiter; synchronized (this) { if (isCancelled() || isDone()) return getResultOrThrow(); waiter = ensureWaiterLocked(); } waiter.acquire(); return getResultOrThrow(); } private T getResultOrThrow() throws ExecutionException { if (exception != null) throw new ExecutionException(exception); return result; } @Override public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { AsyncSemaphore waiter; synchronized (this) { if (isCancelled() || isDone()) return getResultOrThrow(); waiter = ensureWaiterLocked(); } if (!waiter.tryAcquire(timeout, unit)) throw new TimeoutException(); return getResultOrThrow(); } @Override public boolean setComplete() { return setComplete((T)null); } private FutureCallbackInternal handleInternalCompleteLocked() { // don't execute the callback inside the sync block... possible hangup // read the callback value, and then call it outside the block. // can't simply call this.callback.onCompleted directly outside the block, // because that may result in a race condition where the callback changes once leaving // the block. FutureCallbackInternal callback = this.internalCallback; // null out members to allow garbage collection this.internalCallback = null; return callback; } static class FutureCallsite { Exception e; Object result; FutureCallbackInternal callback; void loop() { while (callback != null) { // these values always start non null. FutureCallbackInternal callback = this.callback; Exception e = this.e; Object result = this.result; // null them out for reentrancy this.callback = null; this.e = null; this.result = null; callback.onCompleted(e, result, this); } } } private void handleCallbackUnlocked(FutureCallsite callsite, FutureCallbackInternal internalCallback) { if (silent) return; if (internalCallback == null) return; boolean needsLoop = false; if (callsite == null) { needsLoop = true; callsite = new FutureCallsite(); } callsite.callback = internalCallback; callsite.e = exception; callsite.result = result; if (needsLoop) callsite.loop(); } void releaseWaiterLocked() { if (waiter != null) { waiter.release(); waiter = null; } } AsyncSemaphore ensureWaiterLocked() { if (waiter == null) waiter = new AsyncSemaphore(); return waiter; } public boolean setComplete(Exception e) { return setComplete(e, null, null); } public boolean setCompleteException(Exception e) { return setComplete(e, null, null); } public boolean setComplete(T value) { return setComplete(null, value, null); } public boolean setCompleteValue(T value) { return setComplete(null, value, null); } public boolean setComplete(Exception e, T value) { return setComplete(e, value, null); } private boolean setComplete(Exception e, T value, FutureCallsite callsite) { FutureCallbackInternal internalCallback; synchronized (this) { if (!super.setComplete()) return false; result = value; exception = e; releaseWaiterLocked(); internalCallback = handleInternalCompleteLocked(); } handleCallbackUnlocked(callsite, internalCallback); return true; } void setCallbackInternal(FutureCallsite callsite, FutureCallbackInternal internalCallback) { // callback can only be changed or read/used inside a sync block synchronized (this) { this.internalCallback = internalCallback; if (!isDone() && !isCancelled()) return; internalCallback = handleInternalCompleteLocked(); } handleCallbackUnlocked(callsite, internalCallback); } @Override public void setCallback(FutureCallback callback) { if (callback == null) setCallbackInternal(null, null); else setCallbackInternal(null, (e, result, next) -> callback.onCompleted(e, result)); } private Future setComplete(Future future, FutureCallsite callsite) { setParent(future); SimpleFuture ret = new SimpleFuture<>(); if (future instanceof SimpleFuture) { ((SimpleFuture)future).setCallbackInternal(callsite, (e, result, next) -> ret.setComplete(SimpleFuture.this.setComplete(e, result, next) ? null : new CancellationException(), result, next)); } else { future.setCallback((e, result) -> ret.setComplete(SimpleFuture.this.setComplete(e, result, null) ? null : new CancellationException())); } return ret; } /** * Complete a future with another future. Returns a future that reports whether the completion * was successful. If the future was not completed due to cancellation, the callback * will be called with a CancellationException, and the original future result, if one was provided. * @param future * @return */ public Future setComplete(Future future) { return setComplete(future, null); } public Future setCompleteFuture(Future future) { return setComplete(future, null); } /** * THIS METHOD IS FOR TEST USE ONLY * @return */ @Deprecated public Object getCallback() { return internalCallback; } @Override public Future done(DoneCallback done) { final SimpleFuture ret = new SimpleFuture<>(); ret.setParent(this); setCallbackInternal(null, (e, result, next) -> { if (e == null) { try { done.done(e, result); } catch (Exception callbackException) { e = callbackException; // note that the result is not nulled out. this is useful for managed resources, like sockets. // for example: a successful socket connection was made, but the request can be cancelled. // so, returning an error along with a socket object allows for failure cleanup. } } ret.setComplete(e, result, next); }); return ret; } @Override public Future success(SuccessCallback callback) { final SimpleFuture ret = new SimpleFuture<>(); ret.setParent(this); setCallbackInternal(null, (e, result, next) -> { if (e == null) { try { callback.success(result); } catch (Exception callbackException) { e = callbackException; // note that the result is not nulled out. this is useful for managed resources, like sockets. // for example: a successful socket connection was made, but the request can be cancelled. // so, returning an error along with a socket object allows for failure cleanup. } } ret.setComplete(e, result, next); }); return ret; } @Override public Future then(ThenFutureCallback then) { final SimpleFuture ret = new SimpleFuture<>(); ret.setParent(this); setCallbackInternal(null, (e, result, next) -> { if (e != null) { ret.setComplete(e, null, next); return; } Future out; try { out = then.then(result); } catch (Exception callbackException) { ret.setComplete(callbackException, null, next); return; } ret.setComplete(out, next); }); return ret; } @Override public Future thenConvert(final ThenCallback callback) { return then(from -> new SimpleFuture<>(callback.then(from))); } @Override public Future fail(FailCallback fail) { return failRecover(e -> { fail.fail(e); return new SimpleFuture<>((T)null); }); } @Override public Future failRecover(FailRecoverCallback fail) { SimpleFuture ret = new SimpleFuture<>(); ret.setParent(this); setCallbackInternal(null, (e, result, next) -> { if (e == null) { ret.setComplete(e, result, next); return; } Future out; try { out = fail.fail(e); } catch (Exception callbackException) { ret.setComplete(callbackException, null, next); return; } ret.setComplete(out, next); }); return ret; } @Override public Future failConvert(FailConvertCallback fail) { return failRecover(e -> new SimpleFuture<>(fail.fail(e))); } @Override public boolean setParent(Cancellable parent) { return super.setParent(parent); } /** * Reset the future for reuse. * @return */ public SimpleFuture reset() { super.reset(); result = null; exception = null; waiter = null; internalCallback = null; silent = false; return this; } @Override public Exception tryGetException() { return exception; } @Override public T tryGet() { return result; } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/future/SuccessCallback.java ================================================ package com.koushikdutta.async.future; public interface SuccessCallback { void success(T value) throws Exception; } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/future/ThenCallback.java ================================================ package com.koushikdutta.async.future; public interface ThenCallback { /** * Callback that is invoked when Future.then completes, * and converts a value F to value T. * @param from * @return * @throws Exception */ T then(F from) throws Exception; } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/future/ThenFutureCallback.java ================================================ package com.koushikdutta.async.future; public interface ThenFutureCallback { /** * Callback that is invoked when Future.then completes, * and converts a value F to a Future. * @param from * @return * @throws Exception */ Future then(F from) throws Exception; } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/future/TransformFuture.java ================================================ package com.koushikdutta.async.future; public abstract class TransformFuture extends SimpleFuture implements FutureCallback { public TransformFuture(F from) { onCompleted(null, from); } public TransformFuture() { } @Override public void onCompleted(Exception e, F result) { if (isCancelled()) return; if (e != null) { error(e); return; } try { transform(result); } catch (Exception ex) { error(ex); } } protected void error(Exception e) { setComplete(e); } protected abstract void transform(F result) throws Exception; } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/future/TypeConverter.java ================================================ package com.koushikdutta.async.future; public interface TypeConverter { Future convert(F from, String fromMime) throws Exception; } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClient.java ================================================ package com.koushikdutta.async.http; import android.annotation.SuppressLint; import android.net.Uri; import android.os.Build; import android.text.TextUtils; import com.koushikdutta.async.AsyncSSLException; import com.koushikdutta.async.AsyncServer; import com.koushikdutta.async.AsyncSocket; import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.ConnectCallback; import com.koushikdutta.async.callback.DataCallback; import com.koushikdutta.async.future.Cancellable; import com.koushikdutta.async.future.Future; import com.koushikdutta.async.future.SimpleFuture; import com.koushikdutta.async.http.callback.HttpConnectCallback; import com.koushikdutta.async.http.callback.RequestCallback; import com.koushikdutta.async.parser.AsyncParser; import com.koushikdutta.async.parser.ByteBufferListParser; import com.koushikdutta.async.parser.JSONArrayParser; import com.koushikdutta.async.parser.JSONObjectParser; import com.koushikdutta.async.parser.StringParser; import com.koushikdutta.async.stream.OutputStreamDataCallback; import org.json.JSONArray; import org.json.JSONObject; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Proxy; import java.net.ProxySelector; import java.net.URI; import java.net.URL; import java.util.Collection; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeoutException; public class AsyncHttpClient { private static AsyncHttpClient mDefaultInstance; public static AsyncHttpClient getDefaultInstance() { if (mDefaultInstance == null) mDefaultInstance = new AsyncHttpClient(AsyncServer.getDefault()); return mDefaultInstance; } final List mMiddleware = new CopyOnWriteArrayList<>(); public Collection getMiddleware() { return mMiddleware; } public void insertMiddleware(AsyncHttpClientMiddleware middleware) { mMiddleware.add(0, middleware); } AsyncSSLSocketMiddleware sslSocketMiddleware; AsyncSocketMiddleware socketMiddleware; HttpTransportMiddleware httpTransportMiddleware; AsyncServer mServer; public AsyncHttpClient(AsyncServer server) { mServer = server; insertMiddleware(socketMiddleware = new AsyncSocketMiddleware(this)); insertMiddleware(sslSocketMiddleware = new AsyncSSLSocketMiddleware(this)); insertMiddleware(httpTransportMiddleware = new HttpTransportMiddleware()); sslSocketMiddleware.addEngineConfigurator(new SSLEngineSNIConfigurator()); } @SuppressLint("NewApi") private static void setupAndroidProxy(AsyncHttpRequest request) { // using a explicit proxy? if (request.proxyHost != null) return; List proxies; try { proxies = ProxySelector.getDefault().select(URI.create(request.getUri().toString())); } catch (Exception e) { // uri parsing craps itself sometimes. return; } if (proxies.isEmpty()) return; Proxy proxy = proxies.get(0); if (proxy.type() != Proxy.Type.HTTP) return; if (!(proxy.address() instanceof InetSocketAddress)) return; InetSocketAddress proxyAddress = (InetSocketAddress) proxy.address(); String proxyHost; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { proxyHost = proxyAddress.getHostString(); } else { InetAddress address = proxyAddress.getAddress(); if (address!=null) proxyHost = address.getHostAddress(); else proxyHost = proxyAddress.getHostName(); } request.enableProxy(proxyHost, proxyAddress.getPort()); } public AsyncSocketMiddleware getSocketMiddleware() { return socketMiddleware; } public AsyncSSLSocketMiddleware getSSLSocketMiddleware() { return sslSocketMiddleware; } public Future execute(final AsyncHttpRequest request, final HttpConnectCallback callback) { FutureAsyncHttpResponse ret; execute(request, 0, ret = new FutureAsyncHttpResponse(), callback); return ret; } public Future execute(String uri, final HttpConnectCallback callback) { return execute(new AsyncHttpGet(uri), callback); } private static final String LOGTAG = "AsyncHttp"; private class FutureAsyncHttpResponse extends SimpleFuture { public AsyncSocket socket; public Cancellable scheduled; public Runnable timeoutRunnable; @Override public boolean cancel() { if (!super.cancel()) return false; if (socket != null) { socket.setDataCallback(new DataCallback.NullDataCallback()); socket.close(); } if (scheduled != null) scheduled.cancel(); return true; } } private void reportConnectedCompleted(FutureAsyncHttpResponse cancel, Exception ex, AsyncHttpResponseImpl response, AsyncHttpRequest request, final HttpConnectCallback callback) { cancel.scheduled.cancel(); boolean complete; if (ex != null) { request.loge("Connection error", ex); complete = cancel.setComplete(ex); } else { request.logd("Connection successful"); complete = cancel.setComplete(response); } if (complete) { callback.onConnectCompleted(ex, response); return; } if (response != null) { // the request was cancelled, so close up shop, and eat any pending data response.setDataCallback(new DataCallback.NullDataCallback()); response.close(); } } private void execute(final AsyncHttpRequest request, final int redirectCount, final FutureAsyncHttpResponse cancel, final HttpConnectCallback callback) { if (mServer.isAffinityThread()) { executeAffinity(request, redirectCount, cancel, callback); } else { mServer.post(new Runnable() { @Override public void run() { executeAffinity(request, redirectCount, cancel, callback); } }); } } private static long getTimeoutRemaining(AsyncHttpRequest request) { // need a better way to calculate this. // a timer of sorts that stops/resumes. return request.getTimeout(); } private static void copyHeader(AsyncHttpRequest from, AsyncHttpRequest to, String header) { String value = from.getHeaders().get(header); if (!TextUtils.isEmpty(value)) to.getHeaders().set(header, value); } private void executeAffinity(final AsyncHttpRequest request, final int redirectCount, final FutureAsyncHttpResponse cancel, final HttpConnectCallback callback) { if (redirectCount > 15) { reportConnectedCompleted(cancel, new RedirectLimitExceededException("too many redirects"), null, request, callback); return; } final Uri uri = request.getUri(); final AsyncHttpClientMiddleware.OnResponseCompleteData data = new AsyncHttpClientMiddleware.OnResponseCompleteData(); request.executionTime = System.currentTimeMillis(); data.request = request; request.logd("Executing request."); for (AsyncHttpClientMiddleware middleware: mMiddleware) { middleware.onRequest(data); } // flow: // 1) set a connect timeout // 2) wait for connect // 3) on connect, cancel timeout // 4) wait for request to be sent fully // 5) after request is sent, set a header timeout // 6) wait for headers // 7) on headers, cancel timeout // 8) TODO: response can take as long as it wants to arrive? if (request.getTimeout() > 0) { // set connect timeout cancel.timeoutRunnable = new Runnable() { @Override public void run() { // we've timed out, kill the connections if (data.socketCancellable != null) { data.socketCancellable.cancel(); if (data.socket != null) data.socket.close(); } reportConnectedCompleted(cancel, new TimeoutException(), null, request, callback); } }; cancel.scheduled = mServer.postDelayed(cancel.timeoutRunnable, getTimeoutRemaining(request)); } // 2) wait for a connect data.connectCallback = new ConnectCallback() { boolean reported; @Override public void onConnectCompleted(Exception ex, AsyncSocket socket) { if (reported) { if (socket != null) { socket.setDataCallback(new DataCallback.NullDataCallback()); socket.setEndCallback(new CompletedCallback.NullCompletedCallback()); socket.close(); throw new AssertionError("double connect callback"); } } reported = true; request.logv("socket connected"); if (cancel.isCancelled()) { if (socket != null) socket.close(); return; } // 3) on connect, cancel timeout if (cancel.timeoutRunnable != null) cancel.scheduled.cancel(); if (ex != null) { reportConnectedCompleted(cancel, ex, null, request, callback); return; } data.socket = socket; cancel.socket = socket; executeSocket(request, redirectCount, cancel, callback, data); } }; // set up the system default proxy and connect setupAndroidProxy(request); // set the implicit content type if (request.getBody() != null) { if (request.getHeaders().get("Content-Type") == null) request.getHeaders().set("Content-Type", request.getBody().getContentType()); } final Exception unsupportedURI; for (AsyncHttpClientMiddleware middleware: mMiddleware) { Cancellable socketCancellable = middleware.getSocket(data); if (socketCancellable != null) { data.socketCancellable = socketCancellable; cancel.setParent(socketCancellable); return; } } unsupportedURI = new IllegalArgumentException("invalid uri="+request.getUri()+" middlewares="+mMiddleware); reportConnectedCompleted(cancel, unsupportedURI, null, request, callback); } private void executeSocket(final AsyncHttpRequest request, final int redirectCount, final FutureAsyncHttpResponse cancel, final HttpConnectCallback callback, final AsyncHttpClientMiddleware.OnResponseCompleteData data) { // 4) wait for request to be sent fully // and // 6) wait for headers final AsyncHttpResponseImpl ret = new AsyncHttpResponseImpl(request) { @Override protected void onRequestCompleted(Exception ex) { if (ex != null) { reportConnectedCompleted(cancel, ex, null, request, callback); return; } request.logv("request completed"); if (cancel.isCancelled()) return; // 5) after request is sent, set a header timeout if (cancel.timeoutRunnable != null && mHeaders == null) { cancel.scheduled.cancel(); cancel.scheduled = mServer.postDelayed(cancel.timeoutRunnable, getTimeoutRemaining(request)); } for (AsyncHttpClientMiddleware middleware: mMiddleware) { middleware.onRequestSent(data); } } @Override public void setDataEmitter(DataEmitter emitter) { data.bodyEmitter = emitter; for (AsyncHttpClientMiddleware middleware: mMiddleware) { middleware.onBodyDecoder(data); } super.setDataEmitter(data.bodyEmitter); for (AsyncHttpClientMiddleware middleware: mMiddleware) { AsyncHttpRequest newReq = middleware.onResponseReady(data); if (newReq != null) { newReq.executionTime = request.executionTime; newReq.logLevel = request.logLevel; newReq.LOGTAG = request.LOGTAG; newReq.proxyHost = request.proxyHost; newReq.proxyPort = request.proxyPort; setupAndroidProxy(newReq); request.logi("Response intercepted by middleware"); newReq.logi("Request initiated by middleware intercept by middleware"); // post to allow reuse of socket. mServer.post(() -> execute(newReq, redirectCount, cancel, callback)); setDataCallback(new NullDataCallback()); return; } } Headers headers = mHeaders; int responseCode = code(); if ((responseCode == HttpURLConnection.HTTP_MOVED_PERM || responseCode == HttpURLConnection.HTTP_MOVED_TEMP || responseCode == 307) && request.getFollowRedirect()) { String location = headers.get("Location"); Uri redirect; try { redirect = Uri.parse(location); if (redirect.getScheme() == null) { redirect = Uri.parse(new URL(new URL(request.getUri().toString()), location).toString()); } } catch (Exception e) { reportConnectedCompleted(cancel, e, this, request, callback); return; } final String method = request.getMethod().equals(AsyncHttpHead.METHOD) ? AsyncHttpHead.METHOD : AsyncHttpGet.METHOD; AsyncHttpRequest newReq = new AsyncHttpRequest(redirect, method); newReq.executionTime = request.executionTime; newReq.logLevel = request.logLevel; newReq.LOGTAG = request.LOGTAG; newReq.proxyHost = request.proxyHost; newReq.proxyPort = request.proxyPort; setupAndroidProxy(newReq); copyHeader(request, newReq, "User-Agent"); copyHeader(request, newReq, "Range"); request.logi("Redirecting"); newReq.logi("Redirected"); mServer.post(() -> execute(newReq, redirectCount + 1, cancel, callback)); setDataCallback(new NullDataCallback()); return; } request.logv("Final (post cache response) headers:\n" + toString()); // at this point the headers are done being modified reportConnectedCompleted(cancel, null, this, request, callback); } protected void onHeadersReceived() { super.onHeadersReceived(); if (cancel.isCancelled()) return; // 7) on headers, cancel timeout if (cancel.timeoutRunnable != null) cancel.scheduled.cancel(); // allow the middleware to massage the headers before the body is decoded request.logv("Received headers:\n" + toString()); for (AsyncHttpClientMiddleware middleware: mMiddleware) { middleware.onHeadersReceived(data); } // drop through, and setDataEmitter will be called for the body decoder. // headers will be further massaged in there. } @Override protected void report(Exception ex) { if (ex != null) request.loge("exception during response", ex); if (cancel.isCancelled()) return; if (ex instanceof AsyncSSLException) { request.loge("SSL Exception", ex); AsyncSSLException ase = (AsyncSSLException)ex; request.onHandshakeException(ase); if (ase.getIgnore()) return; } final AsyncSocket socket = socket(); if (socket == null) return; super.report(ex); if (!socket.isOpen() || ex != null) { if (headers() == null && ex != null) reportConnectedCompleted(cancel, ex, null, request, callback); } data.exception = ex; for (AsyncHttpClientMiddleware middleware: mMiddleware) { middleware.onResponseComplete(data); } } @Override public AsyncSocket detachSocket() { request.logd("Detaching socket"); AsyncSocket socket = socket(); if (socket == null) return null; socket.setWriteableCallback(null); socket.setClosedCallback(null); socket.setEndCallback(null); socket.setDataCallback(null); setSocket(null); return socket; } }; data.sendHeadersCallback = new CompletedCallback() { @Override public void onCompleted(Exception ex) { if (ex != null) ret.report(ex); else ret.onHeadersSent(); } }; data.receiveHeadersCallback = new CompletedCallback() { @Override public void onCompleted(Exception ex) { if (ex != null) ret.report(ex); else ret.onHeadersReceived(); } }; data.response = ret; ret.setSocket(data.socket); for (AsyncHttpClientMiddleware middleware : mMiddleware) { if (middleware.exchangeHeaders(data)) break; } } public static abstract class RequestCallbackBase implements RequestCallback { @Override public void onProgress(AsyncHttpResponse response, long downloaded, long total) { } @Override public void onConnect(AsyncHttpResponse response) { } } public static abstract class DownloadCallback extends RequestCallbackBase { } public static abstract class StringCallback extends RequestCallbackBase { } public static abstract class JSONObjectCallback extends RequestCallbackBase { } public static abstract class JSONArrayCallback extends RequestCallbackBase { } public static abstract class FileCallback extends RequestCallbackBase { } public Future executeByteBufferList(AsyncHttpRequest request, DownloadCallback callback) { return execute(request, new ByteBufferListParser(), callback); } public Future executeString(AsyncHttpRequest req, final StringCallback callback) { return execute(req, new StringParser(), callback); } public Future executeJSONObject(AsyncHttpRequest req, final JSONObjectCallback callback) { return execute(req, new JSONObjectParser(), callback); } public Future executeJSONArray(AsyncHttpRequest req, final JSONArrayCallback callback) { return execute(req, new JSONArrayParser(), callback); } private void invokeWithAffinity(final RequestCallback callback, SimpleFuture future, final AsyncHttpResponse response, final Exception e, final T result) { boolean complete; if (e != null) complete = future.setComplete(e); else complete = future.setComplete(result); if (!complete) return; if (callback != null) callback.onCompleted(e, response, result); } private void invoke(final RequestCallback callback, final SimpleFuture future, final AsyncHttpResponse response, final Exception e, final T result) { Runnable runnable = new Runnable() { @Override public void run() { invokeWithAffinity(callback, future, response, e, result); } }; mServer.post(runnable); } private void invokeProgress(final RequestCallback callback, final AsyncHttpResponse response, final long downloaded, final long total) { if (callback != null) callback.onProgress(response, downloaded, total); } private void invokeConnect(final RequestCallback callback, final AsyncHttpResponse response) { if (callback != null) callback.onConnect(response); } public Future executeFile(AsyncHttpRequest req, final String filename, final FileCallback callback) { final File file = new File(filename); file.getParentFile().mkdirs(); final OutputStream fout; try { fout = new BufferedOutputStream(new FileOutputStream(file), 8192); } catch (FileNotFoundException e) { SimpleFuture ret = new SimpleFuture(); ret.setComplete(e); return ret; } final FutureAsyncHttpResponse cancel = new FutureAsyncHttpResponse(); final SimpleFuture ret = new SimpleFuture() { @Override public void cancelCleanup() { try { cancel.get().setDataCallback(new DataCallback.NullDataCallback()); cancel.get().close(); } catch (Exception e) { } try { fout.close(); } catch (Exception e) { } file.delete(); } }; ret.setParent(cancel); execute(req, 0, cancel, new HttpConnectCallback() { long mDownloaded = 0; @Override public void onConnectCompleted(Exception ex, final AsyncHttpResponse response) { if (ex != null) { try { fout.close(); } catch (IOException e) { } file.delete(); invoke(callback, ret, response, ex, null); return; } invokeConnect(callback, response); final long contentLength = HttpUtil.contentLength(response.headers()); response.setDataCallback(new OutputStreamDataCallback(fout) { @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { mDownloaded += bb.remaining(); super.onDataAvailable(emitter, bb); invokeProgress(callback, response, mDownloaded, contentLength); } }); response.setEndCallback(new CompletedCallback() { @Override public void onCompleted(Exception ex) { try { fout.close(); } catch (IOException e) { ex = e; } if (ex != null) { file.delete(); invoke(callback, ret, response, ex, null); } else { invoke(callback, ret, response, null, file); } } }); } }); return ret; } public SimpleFuture execute(AsyncHttpRequest req, final AsyncParser parser, final RequestCallback callback) { final FutureAsyncHttpResponse cancel = new FutureAsyncHttpResponse(); final SimpleFuture ret = new SimpleFuture(); execute(req, 0, cancel, (ex, response) -> { if (ex != null) { invoke(callback, ret, response, ex, null); return; } invokeConnect(callback, response); Future parsed = parser.parse(response); parsed.setCallback((e, result) -> invoke(callback, ret, response, e, result)); // reparent to the new parser future ret.setParent(parsed); }); ret.setParent(cancel); return ret; } public interface WebSocketConnectCallback { void onCompleted(Exception ex, WebSocket webSocket); } public Future websocket(final AsyncHttpRequest req, String protocol, final WebSocketConnectCallback callback) { return websocket(req, protocol != null ? new String[] { protocol } : null, callback); } public Future websocket(final AsyncHttpRequest req, String[] protocols, final WebSocketConnectCallback callback) { WebSocketImpl.addWebSocketUpgradeHeaders(req, protocols); final SimpleFuture ret = new SimpleFuture<>(); Cancellable connect = execute(req, (ex, response) -> { if (ex != null) { if (ret.setComplete(ex)) { if (callback != null) callback.onCompleted(ex, null); } return; } WebSocket ws = WebSocketImpl.finishHandshake(req.getHeaders(), response); if (ws == null) { ex = new WebSocketHandshakeException("Unable to complete websocket handshake"); response.close(); if (!ret.setComplete(ex)) return; } else { if (!ret.setComplete(ws)) return; } if (callback != null) callback.onCompleted(ex, ws); }); ret.setParent(connect); return ret; } public Future websocket(String uri, String protocol, final WebSocketConnectCallback callback) { final AsyncHttpGet get = new AsyncHttpGet(uri.replace("ws://", "http://").replace("wss://", "https://")); return websocket(get, protocol, callback); } public Future websocket(String uri, String[] protocols, final WebSocketConnectCallback callback) { final AsyncHttpGet get = new AsyncHttpGet(uri.replace("ws://", "http://").replace("wss://", "https://")); return websocket(get, protocols, callback); } public AsyncServer getServer() { return mServer; } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClientMiddleware.java ================================================ package com.koushikdutta.async.http; import com.koushikdutta.async.AsyncSocket; import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.DataSink; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.ConnectCallback; import com.koushikdutta.async.future.Cancellable; import com.koushikdutta.async.util.UntypedHashtable; /** * AsyncHttpClientMiddleware is used by AsyncHttpClient to * inspect, manipulate, and handle http requests. */ public interface AsyncHttpClientMiddleware { interface ResponseHead { AsyncSocket socket(); String protocol(); String message(); int code(); ResponseHead protocol(String protocol); ResponseHead message(String message); ResponseHead code(int code); Headers headers(); ResponseHead headers(Headers headers); DataSink sink(); ResponseHead sink(DataSink sink); DataEmitter emitter(); ResponseHead emitter(DataEmitter emitter); } class OnRequestData { public UntypedHashtable state = new UntypedHashtable(); public AsyncHttpRequest request; } class GetSocketData extends OnRequestData { public ConnectCallback connectCallback; public Cancellable socketCancellable; public String protocol; } class OnExchangeHeaderData extends GetSocketData { public AsyncSocket socket; public ResponseHead response; public CompletedCallback sendHeadersCallback; public CompletedCallback receiveHeadersCallback; } class OnRequestSentData extends OnExchangeHeaderData { } class OnHeadersReceivedData extends OnRequestSentData { } class OnBodyDecoderData extends OnHeadersReceivedData { public DataEmitter bodyEmitter; } class OnResponseReadyData extends OnBodyDecoderData { } class OnResponseCompleteData extends OnResponseReadyData { public Exception exception; } /** * Called immediately upon request execution * @param data */ void onRequest(OnRequestData data); /** * Called to retrieve the socket that will fulfill this request * @param data * @return */ Cancellable getSocket(GetSocketData data); /** * Called before when the headers are sent and received via the socket. * Implementers return true to denote they will manage header exchange. * @param data * @return */ boolean exchangeHeaders(OnExchangeHeaderData data); /** * Called once the headers and any optional request body has * been sent * @param data */ void onRequestSent(OnRequestSentData data); /** * Called once the headers have been received via the socket * @param data */ void onHeadersReceived(OnHeadersReceivedData data); /** * Called before the body is decoded * @param data */ void onBodyDecoder(OnBodyDecoderData data); /** * Called before the response is returned to the client. Return a new AsyncHttpRequest * to end the current request and start a new one. Can be used to implement redirect strategies * or multileg authentication, such as digest. * @param data * @return */ AsyncHttpRequest onResponseReady(OnResponseReadyData data); /** * Called once the request is complete and response has been received, * or if an error occurred * @param data */ void onResponseComplete(OnResponseCompleteData data); } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpDelete.java ================================================ package com.koushikdutta.async.http; import android.net.Uri; public class AsyncHttpDelete extends AsyncHttpRequest { public static final String METHOD = "DELETE"; public AsyncHttpDelete(String uri) { this(Uri.parse(uri)); } public AsyncHttpDelete(Uri uri) { super(uri, METHOD); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpGet.java ================================================ package com.koushikdutta.async.http; import android.net.Uri; public class AsyncHttpGet extends AsyncHttpRequest { public static final String METHOD = "GET"; public AsyncHttpGet(String uri) { super(Uri.parse(uri), METHOD); } public AsyncHttpGet(Uri uri) { super(uri, METHOD); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpHead.java ================================================ package com.koushikdutta.async.http; import android.net.Uri; /** * Created by koush on 8/25/13. */ public class AsyncHttpHead extends AsyncHttpRequest { public AsyncHttpHead(Uri uri) { super(uri, METHOD); } @Override public boolean hasBody() { return false; } public static final String METHOD = "HEAD"; } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpPost.java ================================================ package com.koushikdutta.async.http; import android.net.Uri; public class AsyncHttpPost extends AsyncHttpRequest { public static final String METHOD = "POST"; public AsyncHttpPost(String uri) { this(Uri.parse(uri)); } public AsyncHttpPost(Uri uri) { super(uri, METHOD); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpPut.java ================================================ package com.koushikdutta.async.http; import android.net.Uri; public class AsyncHttpPut extends AsyncHttpRequest { public static final String METHOD = "PUT"; public AsyncHttpPut(String uri) { this(Uri.parse(uri)); } public AsyncHttpPut(Uri uri) { super(uri, METHOD); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpRequest.java ================================================ package com.koushikdutta.async.http; import android.net.Uri; import android.util.Log; import com.koushikdutta.async.AsyncSSLException; import com.koushikdutta.async.http.body.AsyncHttpRequestBody; import java.util.Locale; public class AsyncHttpRequest { public RequestLine getRequestLine() { return new RequestLine() { @Override public String getUri() { return AsyncHttpRequest.this.getUri().toString(); } @Override public ProtocolVersion getProtocolVersion() { return new ProtocolVersion("HTTP", 1, 1); } @Override public String getMethod() { return mMethod; } @Override public String toString() { if (proxyHost != null) return String.format(Locale.ENGLISH, "%s %s %s", mMethod, AsyncHttpRequest.this.getUri(), requestLineProtocol); String path = getPath(); if (path == null || path.length() == 0) path = "/"; String query = AsyncHttpRequest.this.getUri().getEncodedQuery(); if (query != null && query.length() != 0) { path += "?" + query; } return String.format(Locale.ENGLISH, "%s %s %s", mMethod, path, requestLineProtocol); } }; } public boolean hasBody() { return true; } public String getPath() { return AsyncHttpRequest.this.getUri().getEncodedPath(); } protected static String getDefaultUserAgent() { String agent = System.getProperty("http.agent"); return agent != null ? agent : ("Java" + System.getProperty("java.version")); } private String requestLineProtocol = "HTTP/1.1"; private String mMethod; public String getMethod() { return mMethod; } public void setRequestLineProtocol(String scheme) { this.requestLineProtocol = scheme; } public String getRequestLineProtocol() { return requestLineProtocol; } public AsyncHttpRequest setMethod(String method) { if (getClass() != AsyncHttpRequest.class) throw new UnsupportedOperationException("can't change method on a subclass of AsyncHttpRequest"); mMethod = method; return this; } public AsyncHttpRequest(Uri uri, String method) { this(uri, method, null); } public static void setDefaultHeaders(Headers ret, Uri uri) { if (uri != null) { String host = uri.getHost(); if (uri.getPort() != -1) host = host + ":" + uri.getPort(); if (host != null) ret.set("Host", host); } ret.set("User-Agent", getDefaultUserAgent()); ret.set("Accept-Encoding", "gzip, deflate"); ret.set("Connection", "keep-alive"); ret.set("Accept", HEADER_ACCEPT_ALL); } public static final String HEADER_ACCEPT_ALL = "*/*"; public AsyncHttpRequest(Uri uri, String method, Headers headers) { mMethod = method; this.uri = uri; if (headers == null) mRawHeaders = new Headers(); else mRawHeaders = headers; if (headers == null) setDefaultHeaders(mRawHeaders, uri); } Uri uri; public Uri getUri() { return uri; } private Headers mRawHeaders = new Headers(); public Headers getHeaders() { return mRawHeaders; } private boolean mFollowRedirect = true; public boolean getFollowRedirect() { return mFollowRedirect; } public AsyncHttpRequest setFollowRedirect(boolean follow) { mFollowRedirect = follow; return this; } private AsyncHttpRequestBody mBody; public void setBody(AsyncHttpRequestBody body) { mBody = body; } public AsyncHttpRequestBody getBody() { return mBody; } public void onHandshakeException(AsyncSSLException e) { } public static final int DEFAULT_TIMEOUT = 30000; int mTimeout = DEFAULT_TIMEOUT; public int getTimeout() { return mTimeout; } public AsyncHttpRequest setTimeout(int timeout) { mTimeout = timeout; return this; } public AsyncHttpRequest setHeader(String name, String value) { getHeaders().set(name, value); return this; } public AsyncHttpRequest addHeader(String name, String value) { getHeaders().add(name, value); return this; } String proxyHost; int proxyPort = -1; public void enableProxy(String host, int port) { proxyHost = host; proxyPort = port; } public void disableProxy() { proxyHost = null; proxyPort = -1; } public String getProxyHost() { return proxyHost; } public int getProxyPort() { return proxyPort; } @Override public String toString() { if (mRawHeaders == null) return super.toString(); return mRawHeaders.toPrefixString(uri.toString()); } public void setLogging(String tag, int level) { LOGTAG = tag; logLevel = level; } // request level logging String LOGTAG; int logLevel; public int getLogLevel() { return logLevel; } public String getLogTag() { return LOGTAG; } long executionTime; private String getLogMessage(String message) { long elapsed; if (executionTime != 0) elapsed = System.currentTimeMillis() - executionTime; else elapsed = 0; return String.format(Locale.ENGLISH, "(%d ms) %s: %s", elapsed, getUri(), message); } public void logi(String message) { if (LOGTAG == null) return; if (logLevel > Log.INFO) return; Log.i(LOGTAG, getLogMessage(message)); } public void logv(String message) { if (LOGTAG == null) return; if (logLevel > Log.VERBOSE) return; Log.v(LOGTAG, getLogMessage(message)); } public void logw(String message) { if (LOGTAG == null) return; if (logLevel > Log.WARN) return; Log.w(LOGTAG, getLogMessage(message)); } public void logd(String message) { if (LOGTAG == null) return; if (logLevel > Log.DEBUG) return; Log.d(LOGTAG, getLogMessage(message)); } public void logd(String message, Exception e) { if (LOGTAG == null) return; if (logLevel > Log.DEBUG) return; Log.d(LOGTAG, getLogMessage(message)); Log.d(LOGTAG, e.getMessage(), e); } public void loge(String message) { if (LOGTAG == null) return; if (logLevel > Log.ERROR) return; Log.e(LOGTAG, getLogMessage(message)); } public void loge(String message, Exception e) { if (LOGTAG == null) return; if (logLevel > Log.ERROR) return; Log.e(LOGTAG, getLogMessage(message)); Log.e(LOGTAG, e.getMessage(), e); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponse.java ================================================ package com.koushikdutta.async.http; import com.koushikdutta.async.AsyncSocket; import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.callback.CompletedCallback; public interface AsyncHttpResponse extends DataEmitter { public String protocol(); public String message(); public int code(); public Headers headers(); public AsyncSocket detachSocket(); public AsyncHttpRequest getRequest(); } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponseImpl.java ================================================ package com.koushikdutta.async.http; import com.koushikdutta.async.AsyncServer; import com.koushikdutta.async.AsyncSocket; import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.DataSink; import com.koushikdutta.async.FilteredDataEmitter; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.WritableCallback; import com.koushikdutta.async.http.body.AsyncHttpRequestBody; import java.nio.charset.Charset; abstract class AsyncHttpResponseImpl extends FilteredDataEmitter implements DataEmitter, AsyncHttpResponse, AsyncHttpClientMiddleware.ResponseHead { public AsyncSocket socket() { return mSocket; } @Override public AsyncHttpRequest getRequest() { return mRequest; } void setSocket(AsyncSocket exchange) { mSocket = exchange; if (mSocket == null) return; mSocket.setEndCallback(mReporter); } protected void onHeadersSent() { AsyncHttpRequestBody requestBody = mRequest.getBody(); if (requestBody != null) { requestBody.write(mRequest, mSink, new CompletedCallback() { @Override public void onCompleted(Exception ex) { onRequestCompleted(ex); } }); } else { onRequestCompleted(null); } } protected void onRequestCompleted(Exception ex) { } private CompletedCallback mReporter = new CompletedCallback() { @Override public void onCompleted(Exception error) { if (headers() == null) { report(new ConnectionClosedException("connection closed before headers received.", error)); } else if (error != null && !mCompleted) { report(new ConnectionClosedException("connection closed before response completed.", error)); } else { report(error); } } }; protected void onHeadersReceived() { } @Override public DataEmitter emitter() { return getDataEmitter(); } @Override public AsyncHttpClientMiddleware.ResponseHead emitter(DataEmitter emitter) { setDataEmitter(emitter); return this; } private void terminate() { // DISCONNECT. EVERYTHING. // should not get any data after this point... // if so, eat it and disconnect. mSocket.setDataCallback(new NullDataCallback() { @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { super.onDataAvailable(emitter, bb); mSocket.close(); } }); } @Override protected void report(Exception e) { super.report(e); terminate(); mSocket.setWriteableCallback(null); mSocket.setClosedCallback(null); mSocket.setEndCallback(null); mCompleted = true; } @Override public void close() { super.close(); terminate(); } private AsyncHttpRequest mRequest; private AsyncSocket mSocket; protected Headers mHeaders; public AsyncHttpResponseImpl(AsyncHttpRequest request) { mRequest = request; } boolean mCompleted = false; @Override public Headers headers() { return mHeaders; } @Override public AsyncHttpClientMiddleware.ResponseHead headers(Headers headers) { mHeaders = headers; return this; } int code; @Override public int code() { return code; } @Override public AsyncHttpClientMiddleware.ResponseHead code(int code) { this.code = code; return this; } @Override public AsyncHttpClientMiddleware.ResponseHead protocol(String protocol) { this.protocol = protocol; return this; } @Override public AsyncHttpClientMiddleware.ResponseHead message(String message) { this.message = message; return this; } String protocol; @Override public String protocol() { return protocol; } String message; @Override public String message() { return message; } @Override public String toString() { if (mHeaders == null) return super.toString(); return mHeaders.toPrefixString(protocol + " " + code + " " + message); } private boolean mFirstWrite = true; private void assertContent() { if (!mFirstWrite) return; mFirstWrite = false; } DataSink mSink; @Override public DataSink sink() { return mSink; } @Override public AsyncHttpClientMiddleware.ResponseHead sink(DataSink sink) { mSink = sink; return this; } @Override public AsyncServer getServer() { return mSocket.getServer(); } @Override public String charset() { Multimap mm = Multimap.parseSemicolonDelimited(headers().get("Content-Type")); String cs; if (mm != null && null != (cs = mm.getString("charset")) && Charset.isSupported(cs)) { return cs; } return null; } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/AsyncSSLEngineConfigurator.java ================================================ package com.koushikdutta.async.http; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; public interface AsyncSSLEngineConfigurator { SSLEngine createEngine(SSLContext sslContext, String peerHost, int peerPort); void configureEngine(SSLEngine engine, AsyncHttpClientMiddleware.GetSocketData data, String host, int port); } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/AsyncSSLSocketMiddleware.java ================================================ package com.koushikdutta.async.http; import android.net.Uri; import android.os.Build; import android.text.TextUtils; import com.koushikdutta.async.AsyncSSLSocket; import com.koushikdutta.async.AsyncSSLSocketWrapper; import com.koushikdutta.async.AsyncSocket; import com.koushikdutta.async.LineEmitter; import com.koushikdutta.async.Util; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.ConnectCallback; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Locale; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; import javax.net.ssl.TrustManager; public class AsyncSSLSocketMiddleware extends AsyncSocketMiddleware { public AsyncSSLSocketMiddleware(AsyncHttpClient client) { super(client, "https", 443); } protected SSLContext sslContext; public void setSSLContext(SSLContext sslContext) { this.sslContext = sslContext; } public SSLContext getSSLContext() { return sslContext != null ? sslContext : AsyncSSLSocketWrapper.getDefaultSSLContext(); } protected TrustManager[] trustManagers; public void setTrustManagers(TrustManager[] trustManagers) { this.trustManagers = trustManagers; } protected HostnameVerifier hostnameVerifier; public void setHostnameVerifier(HostnameVerifier hostnameVerifier) { this.hostnameVerifier = hostnameVerifier; } protected List engineConfigurators = new ArrayList(); public void addEngineConfigurator(AsyncSSLEngineConfigurator engineConfigurator) { engineConfigurators.add(engineConfigurator); } public void clearEngineConfigurators() { engineConfigurators.clear(); } protected SSLEngine createConfiguredSSLEngine(GetSocketData data, String host, int port) { SSLContext sslContext = getSSLContext(); SSLEngine sslEngine = null; for (AsyncSSLEngineConfigurator configurator : engineConfigurators) { sslEngine = configurator.createEngine(sslContext, host, port); if (sslEngine != null) break; } for (AsyncSSLEngineConfigurator configurator : engineConfigurators) { configurator.configureEngine(sslEngine, data, host, port); } return sslEngine; } protected AsyncSSLSocketWrapper.HandshakeCallback createHandshakeCallback(final GetSocketData data, final ConnectCallback callback) { return new AsyncSSLSocketWrapper.HandshakeCallback() { @Override public void onHandshakeCompleted(Exception e, AsyncSSLSocket socket) { callback.onConnectCompleted(e, socket); } }; } protected void tryHandshake(AsyncSocket socket, GetSocketData data, final Uri uri, final int port, final ConnectCallback callback) { AsyncSSLSocketWrapper.handshake(socket, uri.getHost(), port, createConfiguredSSLEngine(data, uri.getHost(), port), trustManagers, hostnameVerifier, true, createHandshakeCallback(data, callback)); } @Override protected ConnectCallback wrapCallback(final GetSocketData data, final Uri uri, final int port, final boolean proxied, final ConnectCallback callback) { return new ConnectCallback() { @Override public void onConnectCompleted(Exception ex, final AsyncSocket socket) { if (ex != null) { callback.onConnectCompleted(ex, socket); return; } if (!proxied) { tryHandshake(socket, data, uri, port, callback); return; } // this SSL connection is proxied, must issue a CONNECT request to the proxy server // http://stackoverflow.com/a/6594880/704837 // some proxies also require 'Host' header, it should be safe to provide it every time String connect = String.format(Locale.ENGLISH, "CONNECT %s:%s HTTP/1.1\r\nHost: %s\r\n\r\n", uri.getHost(), port, uri.getHost()); data.request.logv("Proxying: " + connect); Util.writeAll(socket, connect.getBytes(), new CompletedCallback() { @Override public void onCompleted(Exception ex) { if (ex != null) { callback.onConnectCompleted(ex, socket); return; } LineEmitter liner = new LineEmitter(); liner.setLineCallback(new LineEmitter.StringCallback() { String statusLine; @Override public void onStringAvailable(String s) { data.request.logv(s); if (statusLine == null) { statusLine = s.trim(); if (!statusLine.matches("HTTP/1.\\d 2\\d\\d .*")) { // connect response is allowed to have any 2xx status code socket.setDataCallback(null); socket.setEndCallback(null); callback.onConnectCompleted(new IOException("non 2xx status line: " + statusLine), socket); } } else if (TextUtils.isEmpty(s.trim())) { // skip all headers, complete handshake once empty line is received socket.setDataCallback(null); socket.setEndCallback(null); tryHandshake(socket, data, uri, port, callback); } } }); socket.setDataCallback(liner); socket.setEndCallback(new CompletedCallback() { @Override public void onCompleted(Exception ex) { if (!socket.isOpen() && ex == null) ex = new IOException("socket closed before proxy connect response"); callback.onConnectCompleted(ex, socket); } }); } }); } }; } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/AsyncSocketMiddleware.java ================================================ package com.koushikdutta.async.http; import android.net.Uri; import com.koushikdutta.async.AsyncSocket; import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.ConnectCallback; import com.koushikdutta.async.callback.DataCallback; import com.koushikdutta.async.future.Cancellable; import com.koushikdutta.async.future.Future; import com.koushikdutta.async.future.Futures; import com.koushikdutta.async.future.SimpleCancellable; import com.koushikdutta.async.future.SimpleFuture; import com.koushikdutta.async.util.ArrayDeque; import java.net.InetSocketAddress; import java.util.Hashtable; import java.util.Locale; public class AsyncSocketMiddleware extends SimpleMiddleware { String scheme; int port; // 5 min idle timeout int idleTimeoutMs = 300 * 1000; public AsyncSocketMiddleware(AsyncHttpClient client, String scheme, int port) { mClient = client; this.scheme = scheme; this.port = port; } public void setIdleTimeoutMs(int idleTimeoutMs) { this.idleTimeoutMs = idleTimeoutMs; } public int getSchemePort(Uri uri) { if (uri.getScheme() == null || !uri.getScheme().equals(scheme)) return -1; if (uri.getPort() == -1) { return port; } else { return uri.getPort(); } } public AsyncSocketMiddleware(AsyncHttpClient client) { this(client, "http", 80); } protected AsyncHttpClient mClient; protected ConnectCallback wrapCallback(GetSocketData data, Uri uri, int port, boolean proxied, ConnectCallback callback) { return callback; } boolean connectAllAddresses; public boolean getConnectAllAddresses() { return connectAllAddresses; } public void setConnectAllAddresses(boolean connectAllAddresses) { this.connectAllAddresses = connectAllAddresses; } String proxyHost; int proxyPort; InetSocketAddress proxyAddress; public void disableProxy() { proxyPort = -1; proxyHost = null; proxyAddress = null; } public void enableProxy(String host, int port) { proxyHost = host; proxyPort = port; proxyAddress = null; } String computeLookup(Uri uri, int port, String proxyHost, int proxyPort) { String proxy; if (proxyHost != null) proxy = proxyHost + ":" + proxyPort; else proxy = ""; if (proxyHost != null) proxy = proxyHost + ":" + proxyPort; return uri.getScheme() + "//" + uri.getHost() + ":" + port + "?proxy=" + proxy; } class IdleSocketHolder { public IdleSocketHolder(AsyncSocket socket) { this.socket = socket; } AsyncSocket socket; long idleTime = System.currentTimeMillis(); } static class ConnectionInfo { int openCount; ArrayDeque queue = new ArrayDeque(); ArrayDeque sockets = new ArrayDeque(); } Hashtable connectionInfo = new Hashtable(); int maxConnectionCount = Integer.MAX_VALUE; public int getMaxConnectionCount() { return maxConnectionCount; } public void setMaxConnectionCount(int maxConnectionCount) { this.maxConnectionCount = maxConnectionCount; } @Override public Cancellable getSocket(final GetSocketData data) { final Uri uri = data.request.getUri(); final int port = getSchemePort(data.request.getUri()); if (port == -1) { return null; } data.state.put("socket-owner", this); final String lookup = computeLookup(uri, port, data.request.getProxyHost(), data.request.getProxyPort()); ConnectionInfo info = getOrCreateConnectionInfo(lookup); synchronized (AsyncSocketMiddleware.this) { if (info.openCount >= maxConnectionCount) { // wait for a connection queue to free up SimpleCancellable queueCancel = new SimpleCancellable(); info.queue.add(data); return queueCancel; } info.openCount++; while (!info.sockets.isEmpty()) { IdleSocketHolder idleSocketHolder = info.sockets.pop(); final AsyncSocket socket = idleSocketHolder.socket; if (idleSocketHolder.idleTime + idleTimeoutMs < System.currentTimeMillis()) { socket.setClosedCallback(null); socket.close(); continue; } if (!socket.isOpen()) continue; data.request.logd("Reusing keep-alive socket"); data.connectCallback.onConnectCompleted(null, socket); // just a noop/dummy, as this can't actually be cancelled. SimpleCancellable ret = new SimpleCancellable(); ret.setComplete(); return ret; } } if (!connectAllAddresses || proxyHost != null || data.request.getProxyHost() != null) { // just default to connecting to a single address data.request.logd("Connecting socket"); String unresolvedHost; int unresolvedPort; boolean proxied = false; if (data.request.getProxyHost() == null && proxyHost != null) data.request.enableProxy(proxyHost, proxyPort); if (data.request.getProxyHost() != null) { unresolvedHost = data.request.getProxyHost(); unresolvedPort = data.request.getProxyPort(); proxied = true; } else { unresolvedHost = uri.getHost(); unresolvedPort = port; } if (proxied) { data.request.logv("Using proxy: " + unresolvedHost + ":" + unresolvedPort); } return mClient.getServer().connectSocket(unresolvedHost, unresolvedPort, wrapCallback(data, uri, port, proxied, data.connectCallback)); } // try to connect to everything... data.request.logv("Resolving domain and connecting to all available addresses"); final SimpleFuture checkedReturnValue = new SimpleFuture<>(); Future socket = mClient.getServer().getAllByName(uri.getHost()) .then(addresses -> Futures.loopUntil(addresses, address -> { SimpleFuture loopResult = new SimpleFuture<>(); final String inetSockAddress = String.format(Locale.ENGLISH, "%s:%s", address, port); data.request.logv("attempting connection to " + inetSockAddress); mClient.getServer().connectSocket(new InetSocketAddress(address, port), loopResult::setComplete); return loopResult; })) // handle failures here (wrap the callback) .fail(e -> wrapCallback(data, uri, port, false, data.connectCallback).onConnectCompleted(e, null)); checkedReturnValue.setComplete(socket) .setCallback((e, successfulSocket) -> { if (successfulSocket == null) return; // SimpleFuture.setComplete(Future) returns a future as to whether // the completion was successful, or the future has been cancelled, // thus the completion failed. // The exception value will only ever be a CancellationException. if (e == null) { // handle successes here (wrap the callback) wrapCallback(data, uri, port, false, data.connectCallback).onConnectCompleted(null, successfulSocket); return; } data.request.logd("Recycling extra socket leftover from cancelled operation"); idleSocket(successfulSocket); recycleSocket(successfulSocket, data.request); }); return checkedReturnValue; } private ConnectionInfo getOrCreateConnectionInfo(String lookup) { ConnectionInfo info = connectionInfo.get(lookup); if (info == null) { info = new ConnectionInfo(); connectionInfo.put(lookup, info); } return info; } private void maybeCleanupConnectionInfo(String lookup) { ConnectionInfo info = connectionInfo.get(lookup); if (info == null) return; while (!info.sockets.isEmpty()) { IdleSocketHolder idleSocketHolder = info.sockets.peekLast(); AsyncSocket socket = idleSocketHolder.socket; if (idleSocketHolder.idleTime + idleTimeoutMs > System.currentTimeMillis()) break; info.sockets.pop(); // remove the callback, prevent reentrancy. socket.setClosedCallback(null); socket.close(); } if (info.openCount == 0 && info.queue.isEmpty() && info.sockets.isEmpty()) connectionInfo.remove(lookup); } private void recycleSocket(final AsyncSocket socket, AsyncHttpRequest request) { if (socket == null) return; Uri uri = request.getUri(); int port = getSchemePort(uri); final String lookup = computeLookup(uri, port, request.getProxyHost(), request.getProxyPort()); final ArrayDeque sockets; final IdleSocketHolder idleSocketHolder = new IdleSocketHolder(socket); synchronized (AsyncSocketMiddleware.this) { ConnectionInfo info = getOrCreateConnectionInfo(lookup); sockets = info.sockets; sockets.push(idleSocketHolder); } socket.setClosedCallback(new CompletedCallback() { @Override public void onCompleted(Exception ex) { synchronized (AsyncSocketMiddleware.this) { sockets.remove(idleSocketHolder); maybeCleanupConnectionInfo(lookup); } } }); } private void idleSocket(final AsyncSocket socket) { // must listen for socket close, otherwise log will get spammed. socket.setEndCallback(new CompletedCallback() { @Override public void onCompleted(Exception ex) { socket.setClosedCallback(null); socket.close(); } }); socket.setWriteableCallback(null); // should not get any data after this point... // if so, eat it and disconnect. socket.setDataCallback(new DataCallback.NullDataCallback() { @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { super.onDataAvailable(emitter, bb); bb.recycle(); socket.setClosedCallback(null); socket.close(); } }); } private void nextConnection(AsyncHttpRequest request) { Uri uri = request.getUri(); final int port = getSchemePort(uri); String key = computeLookup(uri, port, request.getProxyHost(), request.getProxyPort()); synchronized (AsyncSocketMiddleware.this) { ConnectionInfo info = connectionInfo.get(key); if (info == null) return; --info.openCount; while (info.openCount < maxConnectionCount && info.queue.size() > 0) { GetSocketData gsd = info.queue.remove(); SimpleCancellable socketCancellable = (SimpleCancellable)gsd.socketCancellable; if (socketCancellable.isCancelled()) continue; Cancellable connect = getSocket(gsd); socketCancellable.setParent(connect); } maybeCleanupConnectionInfo(key); } } protected boolean isKeepAlive(OnResponseCompleteData data) { return HttpUtil.isKeepAlive(data.response.protocol(), data.response.headers()) && HttpUtil.isKeepAlive(Protocol.HTTP_1_1, data.request.getHeaders()); } @Override public void onResponseComplete(final OnResponseCompleteData data) { if (data.state.get("socket-owner") != this) return; try { idleSocket(data.socket); if (data.exception != null || !data.socket.isOpen()) { data.request.logv("closing out socket (exception)"); data.socket.setClosedCallback(null); data.socket.close(); return; } if (!isKeepAlive(data)) { data.request.logv("closing out socket (not keep alive)"); data.socket.setClosedCallback(null); data.socket.close(); return; } data.request.logd("Recycling keep-alive socket"); recycleSocket(data.socket, data.request); } finally { nextConnection(data.request); } } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/BasicNameValuePair.java ================================================ /* * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpcore/trunk/module-main/src/main/java/org/apache/http/message/BasicNameValuePair.java $ * $Revision: 604625 $ * $Date: 2007-12-16 06:11:11 -0800 (Sun, 16 Dec 2007) $ * * ==================================================================== * 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. * ==================================================================== * * This software consists of voluntary contributions made by many * individuals on behalf of the Apache Software Foundation. For more * information on the Apache Software Foundation, please see * . * */ package com.koushikdutta.async.http; import android.text.TextUtils; /** * A simple class encapsulating an attribute/value pair. *

* This class comforms to the generic grammar and formatting rules outlined in the * Section 2.2 * and * Section 3.6 * of RFC 2616 *

* 2.2 Basic Rules *

* The following rules are used throughout this specification to describe basic parsing constructs. * The US-ASCII coded character set is defined by ANSI X3.4-1986. *

*
 *     OCTET          = 
 *     CHAR           = 
 *     UPALPHA        = 
 *     LOALPHA        = 
 *     ALPHA          = UPALPHA | LOALPHA
 *     DIGIT          = 
 *     CTL            = 
 *     CR             = 
 *     LF             = 
 *     SP             = 
 *     HT             = 
 *     <">            = 
 * 
*

* Many HTTP/1.1 header field values consist of words separated by LWS or special * characters. These special characters MUST be in a quoted string to be used within * a parameter value (as defined in section 3.6). *

*

 * token          = 1*
 * separators     = "(" | ")" | "<" | ">" | "@"
 *                | "," | ";" | ":" | "\" | <">
 *                | "/" | "[" | "]" | "?" | "="
 *                | "{" | "}" | SP | HT
 * 
*

* A string of text is parsed as a single word if it is quoted using double-quote marks. *

*
 * quoted-string  = ( <"> *(qdtext | quoted-pair ) <"> )
 * qdtext         = >
 * 
*

* The backslash character ("\") MAY be used as a single-character quoting mechanism only * within quoted-string and comment constructs. *

*
 * quoted-pair    = "\" CHAR
 * 
* 3.6 Transfer Codings *

* Parameters are in the form of attribute/value pairs. *

*
 * parameter               = attribute "=" value
 * attribute               = token
 * value                   = token | quoted-string
 * 
* * @author Oleg Kalnichevski * */ public class BasicNameValuePair implements NameValuePair, Cloneable { private final String name; private final String value; /** * Default Constructor taking a name and a value. The value may be null. * * @param name The name. * @param value The value. */ public BasicNameValuePair(final String name, final String value) { super(); if (name == null) { throw new IllegalArgumentException("Name may not be null"); } this.name = name; this.value = value; } /** * Returns the name. * * @return String name The name */ public String getName() { return this.name; } /** * Returns the value. * * @return String value The current value. */ public String getValue() { return this.value; } /** * Get a string representation of this pair. * * @return A string representation. */ public String toString() { return name + "=" + value; } public boolean equals(final Object object) { if (object == null) return false; if (this == object) return true; if (object instanceof NameValuePair) { BasicNameValuePair that = (BasicNameValuePair) object; return this.name.equals(that.name) && TextUtils.equals(this.value, that.value); } else { return false; } } public int hashCode() { return name.hashCode() ^ value.hashCode(); } public Object clone() throws CloneNotSupportedException { return super.clone(); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/BodyDecoderException.java ================================================ package com.koushikdutta.async.http; public class BodyDecoderException extends Exception { public BodyDecoderException(String message) { super(message); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/ConnectionClosedException.java ================================================ package com.koushikdutta.async.http; public class ConnectionClosedException extends Exception { public ConnectionClosedException(String message) { super(message); } public ConnectionClosedException(String detailMessage, Throwable throwable) { super(detailMessage, throwable); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/ConnectionFailedException.java ================================================ package com.koushikdutta.async.http; public class ConnectionFailedException extends Exception { public ConnectionFailedException(String message) { super(message); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/Headers.java ================================================ package com.koushikdutta.async.http; import android.text.TextUtils; import com.koushikdutta.async.util.TaggedList; import java.util.Collection; import java.util.List; import java.util.Locale; import java.util.Map; /** * Created by koush on 7/21/14. */ public class Headers { public Headers() { } public Headers(Map> mm) { for (String key: mm.keySet()) { addAll(key, mm.get(key)); } } final Multimap map = new Multimap() { @Override protected List newList() { return new TaggedList(); } }; public Multimap getMultiMap() { return map; } public List getAll(String header) { return map.get(header.toLowerCase(Locale.US)); } public String get(String header) { return map.getString(header.toLowerCase(Locale.US)); } public Headers set(String header, String value) { if (value != null && (value.contains("\n") || value.contains("\r"))) throw new IllegalArgumentException("value must not contain a new line or line feed"); String lc = header.toLowerCase(Locale.US); map.put(lc, value); TaggedList list = (TaggedList)map.get(lc); list.tagNull(header); return this; } public Headers add(String header, String value) { String lc = header.toLowerCase(Locale.US); map.add(lc, value); TaggedList list = (TaggedList)map.get(lc); list.tagNull(header); return this; } public Headers addLine(String line) { if (line != null) { line = line.trim(); String[] parts = line.split(":", 2); if (parts.length == 2) add(parts[0].trim(), parts[1].trim()); else add(parts[0].trim(), ""); } return this; } public Headers addAll(String header, List values) { for (String v: values) { add(header, v); } return this; } public Headers addAll(Map> m) { for (String key: m.keySet()) { for (String value: m.get(key)) { add(key, value); } } return this; } public Headers addAllMap(Map m) { for (String key: m.keySet()) { add(key, m.get(key)); } return this; } public Headers addAll(Headers headers) { // safe to addall since this is another Headers object map.putAll(headers.map); return this; } public List removeAll(String header) { return map.remove(header.toLowerCase(Locale.US)); } public String remove(String header) { List r = removeAll(header.toLowerCase(Locale.US)); if (r == null || r.size() == 0) return null; return r.get(0); } public Headers removeAll(Collection headers) { for (String header: headers) { remove(header); } return this; } public StringBuilder toStringBuilder() { StringBuilder result = new StringBuilder(256); for (String key: map.keySet()) { TaggedList list = (TaggedList)map.get(key); for (String v: list) { result.append((String)list.tag()) .append(": ") .append(v) .append("\r\n"); } } result.append("\r\n"); return result; } @Override public String toString() { return toStringBuilder().toString(); } public String toPrefixString(String prefix) { return toStringBuilder() .insert(0, prefix + "\r\n") .toString(); } public static Headers parse(String payload) { String[] lines = payload.split("\n"); Headers headers = new Headers(); for (String line: lines) { line = line.trim(); if (TextUtils.isEmpty(line)) continue; headers.addLine(line); } return headers; } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/HttpDate.java ================================================ /* * 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; 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 STANDARD_DATE_FORMAT = new ThreadLocal() { @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) { if (value == null) return null; 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); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/HttpTransportMiddleware.java ================================================ package com.koushikdutta.async.http; import android.text.TextUtils; import com.koushikdutta.async.AsyncSocket; import com.koushikdutta.async.BufferedDataSink; import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.DataSink; import com.koushikdutta.async.LineEmitter; import com.koushikdutta.async.Util; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.http.body.AsyncHttpRequestBody; import com.koushikdutta.async.http.filter.ChunkedOutputFilter; import java.io.IOException; /** * Created by koush on 7/24/14. */ public class HttpTransportMiddleware extends SimpleMiddleware { @Override public boolean exchangeHeaders(final OnExchangeHeaderData data) { Protocol p = Protocol.get(data.protocol); if (p != null && p != Protocol.HTTP_1_0 && p != Protocol.HTTP_1_1) return super.exchangeHeaders(data); AsyncHttpRequest request = data.request; AsyncHttpRequestBody requestBody = data.request.getBody(); if (requestBody != null) { if (requestBody.length() >= 0) { request.getHeaders().set("Content-Length", String.valueOf(requestBody.length())); data.response.sink(data.socket); } else if ("close".equals(request.getHeaders().get("Connection"))) { data.response.sink(data.socket); } else { request.getHeaders().set("Transfer-Encoding", "Chunked"); data.response.sink(new ChunkedOutputFilter(data.socket)); } } String rl = request.getRequestLine().toString(); String rs = request.getHeaders().toPrefixString(rl); byte[] rsBytes = rs.getBytes(); // try to get the request body in the same packet as the request headers... if it will fit // in the max MTU (1540 or whatever). final boolean waitForBody = requestBody != null && requestBody.length() >= 0 && requestBody.length() + rsBytes.length < 1024; final BufferedDataSink bsink; final DataSink headerSink; if (waitForBody) { // force buffering of headers bsink = new BufferedDataSink(data.response.sink()); bsink.forceBuffering(true); data.response.sink(bsink); headerSink = bsink; } else { bsink = null; headerSink = data.socket; } request.logv("\n" + rs); final CompletedCallback sentCallback = data.sendHeadersCallback; Util.writeAll(headerSink, rsBytes, new CompletedCallback() { @Override public void onCompleted(Exception ex) { Util.end(sentCallback, ex); // flush headers and any request body that was written by the callback if (bsink != null) { bsink.forceBuffering(false); bsink.setMaxBuffer(0); } } }); LineEmitter.StringCallback headerCallback = new LineEmitter.StringCallback() { Headers mRawHeaders = new Headers(); String statusLine; @Override public void onStringAvailable(String s) { try { s = s.trim(); if (statusLine == null) { statusLine = s; } else if (!TextUtils.isEmpty(s)) { mRawHeaders.addLine(s); } else { String[] parts = statusLine.split(" ", 3); if (parts.length < 2) throw new Exception(new IOException("Not HTTP")); data.response.headers(mRawHeaders); String protocol = parts[0]; data.response.protocol(protocol); data.response.code(Integer.parseInt(parts[1])); data.response.message(parts.length == 3 ? parts[2] : ""); data.receiveHeadersCallback.onCompleted(null); // socket may get detached after headers (websocket) AsyncSocket socket = data.response.socket(); if (socket == null) return; DataEmitter emitter; // HEAD requests must not return any data. They still may // return content length, etc, which will confuse the body decoder if (!data.request.hasBody()) { emitter = HttpUtil.EndEmitter.create(socket.getServer(), null); } else if (responseIsEmpty(data.response.code())) { emitter = HttpUtil.EndEmitter.create(socket.getServer(), null); } else { emitter = HttpUtil.getBodyDecoder(socket, Protocol.get(protocol), mRawHeaders, false); } data.response.emitter(emitter); } } catch (Exception ex) { data.receiveHeadersCallback.onCompleted(ex); } } }; LineEmitter liner = new LineEmitter(); data.socket.setDataCallback(liner); liner.setLineCallback(headerCallback); return true; } static boolean responseIsEmpty(int code) { return (code >= 100 && code <= 199) || code == 204 || code == 304; } @Override public void onRequestSent(OnRequestSentData data) { Protocol p = Protocol.get(data.protocol); if (p != null && p != Protocol.HTTP_1_0 && p != Protocol.HTTP_1_1) return; if (data.response.sink() instanceof ChunkedOutputFilter) data.response.sink().end(); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/HttpUtil.java ================================================ package com.koushikdutta.async.http; import com.koushikdutta.async.AsyncServer; import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.FilteredDataEmitter; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.http.body.AsyncHttpRequestBody; import com.koushikdutta.async.http.body.JSONObjectBody; import com.koushikdutta.async.http.body.MultipartFormDataBody; import com.koushikdutta.async.http.body.StringBody; import com.koushikdutta.async.http.body.UrlEncodedFormBody; import com.koushikdutta.async.http.filter.ChunkedInputFilter; import com.koushikdutta.async.http.filter.ContentLengthFilter; import com.koushikdutta.async.http.filter.GZIPInputFilter; import com.koushikdutta.async.http.filter.InflaterInputFilter; public class HttpUtil { public static AsyncHttpRequestBody getBody(DataEmitter emitter, CompletedCallback reporter, Headers headers) { String contentType = headers.get("Content-Type"); if (contentType != null) { String[] values = contentType.split(";"); for (int i = 0; i < values.length; i++) { values[i] = values[i].trim(); } for (String ct: values) { if (UrlEncodedFormBody.CONTENT_TYPE.equals(ct)) { return new UrlEncodedFormBody(); } if (JSONObjectBody.CONTENT_TYPE.equals(ct)) { return new JSONObjectBody(); } if (StringBody.CONTENT_TYPE.equals(ct)) { return new StringBody(); } if (ct != null && ct.startsWith(MultipartFormDataBody.PRIMARY_TYPE)) { return new MultipartFormDataBody(contentType); } } } return null; } static class EndEmitter extends FilteredDataEmitter { private EndEmitter() { } public static EndEmitter create(AsyncServer server, final Exception e) { final EndEmitter ret = new EndEmitter(); // don't need to worry about any race conditions with post and this return value // since we are in the server thread. server.post(new Runnable() { @Override public void run() { ret.report(e); } }); return ret; } } public static DataEmitter getBodyDecoder(DataEmitter emitter, Protocol protocol, Headers headers, boolean server) { long _contentLength = -1; try { String header = headers.get("Content-Length"); if (header != null) _contentLength = Long.parseLong(header); } catch (NumberFormatException ex) { } final long contentLength = _contentLength; if (-1 != contentLength) { if (contentLength < 0) { EndEmitter ender = EndEmitter.create(emitter.getServer(), new BodyDecoderException("not using chunked encoding, and no content-length found.")); ender.setDataEmitter(emitter); emitter = ender; return emitter; } if (contentLength == 0) { EndEmitter ender = EndEmitter.create(emitter.getServer(), null); ender.setDataEmitter(emitter); emitter = ender; return emitter; } ContentLengthFilter contentLengthWatcher = new ContentLengthFilter(contentLength); contentLengthWatcher.setDataEmitter(emitter); emitter = contentLengthWatcher; } else if ("chunked".equalsIgnoreCase(headers.get("Transfer-Encoding"))) { ChunkedInputFilter chunker = new ChunkedInputFilter(); chunker.setDataEmitter(emitter); emitter = chunker; } else if (server) { // if this is the server, and the client has not indicated a request body, the client is done EndEmitter ender = EndEmitter.create(emitter.getServer(), null); ender.setDataEmitter(emitter); emitter = ender; return emitter; } if ("gzip".equals(headers.get("Content-Encoding"))) { GZIPInputFilter gunzipper = new GZIPInputFilter(); gunzipper.setDataEmitter(emitter); emitter = gunzipper; } else if ("deflate".equals(headers.get("Content-Encoding"))) { InflaterInputFilter inflater = new InflaterInputFilter(); inflater.setDataEmitter(emitter); emitter = inflater; } // conversely, if this is the client (http 1.0), and the server has not indicated a request body, we do not report // the close/end event until the server actually closes the connection. return emitter; } public static boolean isKeepAlive(Protocol protocol, Headers headers) { // connection is always keep alive as this is an http/1.1 client String connection = headers.get("Connection"); if (connection == null) return protocol == Protocol.HTTP_1_1; return "keep-alive".equalsIgnoreCase(connection); } public static boolean isKeepAlive(String protocol, Headers headers) { // connection is always keep alive as this is an http/1.1 client String connection = headers.get("Connection"); if (connection == null) return Protocol.get(protocol) == Protocol.HTTP_1_1; return "keep-alive".equalsIgnoreCase(connection); } public static long contentLength(Headers headers) { String cl = headers.get("Content-Length"); if (cl == null) return -1; try { return Long.parseLong(cl); } catch (NumberFormatException e) { return -1; } } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/HybiParser.java ================================================ // // HybiParser.java: draft-ietf-hybi-thewebsocketprotocol-13 parser // // Based on code from the faye project. // https://github.com/faye/faye-websocket-node // Copyright (c) 2009-2012 James Coglan // // Ported from Javascript to Java by Eric Butler // // (The MIT License) // // Permission is hereby granted, free of charge, to any person obtaining // a copy of this software and associated documentation files (the // "Software"), to deal in the Software without restriction, including // without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to // the following conditions: // // The above copyright notice and this permission notice shall be // included in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package com.koushikdutta.async.http; import android.util.Log; import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.DataEmitterReader; import com.koushikdutta.async.callback.DataCallback; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.util.Arrays; import java.util.List; import java.util.zip.DataFormatException; import java.util.zip.Inflater; abstract class HybiParser { private static final String TAG = "HybiParser"; private boolean mMasking = true; private boolean mDeflate = false; private int mStage; private boolean mFinal; private boolean mMasked; private boolean mDeflated; private int mOpcode; private int mLengthSize; private int mLength; private int mMode; private byte[] mMask = new byte[0]; private byte[] mPayload = new byte[0]; private boolean mClosed = false; private ByteArrayOutputStream mBuffer = new ByteArrayOutputStream(); private Inflater mInflater = new Inflater(true); private byte[] mInflateBuffer = new byte[4096]; private static final int BYTE = 255; private static final int FIN = 128; private static final int MASK = 128; private static final int RSV1 = 64; private static final int RSV2 = 32; private static final int RSV3 = 16; private static final int OPCODE = 15; private static final int LENGTH = 127; private static final int MODE_TEXT = 1; private static final int MODE_BINARY = 2; private static final int OP_CONTINUATION = 0; private static final int OP_TEXT = 1; private static final int OP_BINARY = 2; private static final int OP_CLOSE = 8; private static final int OP_PING = 9; private static final int OP_PONG = 10; private static final List OPCODES = Arrays.asList( OP_CONTINUATION, OP_TEXT, OP_BINARY, OP_CLOSE, OP_PING, OP_PONG ); private static final List FRAGMENTED_OPCODES = Arrays.asList( OP_CONTINUATION, OP_TEXT, OP_BINARY ); // // public HybiParser(WebSocketClient client) { // mClient = client; // } private static byte[] mask(byte[] payload, byte[] mask, int offset) { if (mask.length == 0) return payload; for (int i = 0; i < payload.length - offset; i++) { payload[offset + i] = (byte) (payload[offset + i] ^ mask[i % 4]); } return payload; } private byte[] inflate(byte[] payload) throws DataFormatException { ByteArrayOutputStream inflated = new ByteArrayOutputStream(); mInflater.setInput(payload); while (!mInflater.needsInput()) { int chunkSize = mInflater.inflate(mInflateBuffer); inflated.write(mInflateBuffer, 0, chunkSize); } mInflater.setInput(new byte[] { 0, 0, -1, -1 }); while (!mInflater.needsInput()) { int chunkSize = mInflater.inflate(mInflateBuffer); inflated.write(mInflateBuffer, 0, chunkSize); } return inflated.toByteArray(); } public void setMasking(boolean masking) { mMasking = masking; } public void setDeflate(boolean deflate) { mDeflate = deflate; } DataCallback mStage0 = new DataCallback() { @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { try { parseOpcode(bb.get()); } catch (ProtocolError e) { report(e); e.printStackTrace(); } parse(); } }; DataCallback mStage1 = new DataCallback() { @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { parseLength(bb.get()); parse(); } }; DataCallback mStage2 = new DataCallback() { @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { byte[] bytes = new byte[mLengthSize]; bb.get(bytes); try { parseExtendedLength(bytes); } catch (ProtocolError e) { report(e); e.printStackTrace(); } parse(); } }; DataCallback mStage3 = new DataCallback() { @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { mMask = new byte[4]; bb.get(mMask); mStage = 4; parse(); } }; DataCallback mStage4 = new DataCallback() { @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { mPayload = new byte[mLength]; bb.get(mPayload); try { emitFrame(); } catch (IOException e) { report(e); e.printStackTrace(); } mStage = 0; parse(); } }; void parse() { switch (mStage) { case 0: mReader.read(1, mStage0); break; case 1: mReader.read(1, mStage1); break; case 2: mReader.read(mLengthSize, mStage2); break; case 3: mReader.read(4, mStage3); break; case 4: mReader.read(mLength, mStage4); break; } } private DataEmitterReader mReader = new DataEmitterReader(); private static final long BASE = 2; private static final long _2_TO_8_ = BASE << 7; private static final long _2_TO_16_ = BASE << 15; private static final long _2_TO_24 = BASE << 23; private static final long _2_TO_32_ = BASE << 31; private static final long _2_TO_40_ = BASE << 39; private static final long _2_TO_48_ = BASE << 47; private static final long _2_TO_56_ = BASE << 55; public HybiParser(DataEmitter socket) { socket.setDataCallback(mReader); parse(); } private void parseOpcode(byte data) throws ProtocolError { boolean rsv1 = (data & RSV1) == RSV1; boolean rsv2 = (data & RSV2) == RSV2; boolean rsv3 = (data & RSV3) == RSV3; if ((!mDeflate && rsv1) || rsv2 || rsv3) { throw new ProtocolError("RSV not zero"); } mFinal = (data & FIN) == FIN; mOpcode = (data & OPCODE); mDeflated = rsv1; mMask = new byte[0]; mPayload = new byte[0]; if (!OPCODES.contains(mOpcode)) { throw new ProtocolError("Bad opcode"); } if (!FRAGMENTED_OPCODES.contains(mOpcode) && !mFinal) { throw new ProtocolError("Expected non-final packet"); } mStage = 1; } private void parseLength(byte data) { mMasked = (data & MASK) == MASK; mLength = (data & LENGTH); if (mLength >= 0 && mLength <= 125) { mStage = mMasked ? 3 : 4; } else { mLengthSize = (mLength == 126) ? 2 : 8; mStage = 2; } } private void parseExtendedLength(byte[] buffer) throws ProtocolError { mLength = getInteger(buffer); mStage = mMasked ? 3 : 4; } public byte[] frame(String data) { return frame(OP_TEXT, data, -1); } public byte[] frame(byte[] data) { return frame(OP_BINARY, data, -1); } public byte[] frame(byte[] data, int offset, int length) { return frame(OP_BINARY, data, -1, offset, length); } public byte[] pingFrame(String data) { return frame(OP_PING, data, -1); } public byte[] pongFrame(String data) { return frame(OP_PONG, data, -1); } /** * Flip the opcode so to avoid the name collision with the public method * * @param opcode * @param data * @param errorCode * @return */ private byte[] frame(int opcode, byte[] data, int errorCode) { return frame(opcode, data, errorCode, 0, data.length); } /** * Don't actually need the flipped method signature, trying to keep it in line with the byte[] version * * @param opcode * @param data * @param errorCode * @return */ private byte[] frame(int opcode, String data, int errorCode) { return frame(opcode, decode(data), errorCode); } private byte[] frame(int opcode, byte [] data, int errorCode, int dataOffset, int dataLength) { if (mClosed) return null; // Log.d(TAG, "Creating frame for: " + data + " op: " + opcode + " err: " + errorCode); byte[] buffer = data; int insert = (errorCode > 0) ? 2 : 0; int length = dataLength + insert - dataOffset; int header = (length <= 125) ? 2 : (length <= 65535 ? 4 : 10); int offset = header + (mMasking ? 4 : 0); int masked = mMasking ? MASK : 0; byte[] frame = new byte[length + offset]; frame[0] = (byte) ((byte)FIN | (byte)opcode); if (length <= 125) { frame[1] = (byte) (masked | length); } else if (length <= 65535) { frame[1] = (byte) (masked | 126); frame[2] = (byte) (length / 256); frame[3] = (byte) (length & BYTE); } else { frame[1] = (byte) (masked | 127); frame[2] = (byte) (( length / _2_TO_56_) & BYTE); frame[3] = (byte) (( length / _2_TO_48_) & BYTE); frame[4] = (byte) (( length / _2_TO_40_) & BYTE); frame[5] = (byte) (( length / _2_TO_32_) & BYTE); frame[6] = (byte) (( length / _2_TO_24) & BYTE); frame[7] = (byte) (( length / _2_TO_16_) & BYTE); frame[8] = (byte) (( length / _2_TO_8_) & BYTE); frame[9] = (byte) (length & BYTE); } if (errorCode > 0) { frame[offset] = (byte) ((errorCode / 256) & BYTE); frame[offset+1] = (byte) (errorCode & BYTE); } System.arraycopy(buffer, dataOffset, frame, offset + insert, dataLength - dataOffset); if (mMasking) { byte[] mask = { (byte) Math.floor(Math.random() * 256), (byte) Math.floor(Math.random() * 256), (byte) Math.floor(Math.random() * 256), (byte) Math.floor(Math.random() * 256) }; System.arraycopy(mask, 0, frame, header, mask.length); mask(frame, mask, offset); } return frame; } public void close(int code, String reason) { if (mClosed) return; sendFrame(frame(OP_CLOSE, reason, code)); mClosed = true; } private void emitFrame() throws IOException { byte[] payload = mask(mPayload, mMask, 0); if (mDeflated) { try { payload = inflate(payload); } catch (DataFormatException e) { throw new IOException("Invalid deflated data"); } } int opcode = mOpcode; if (opcode == OP_CONTINUATION) { if (mMode == 0) { throw new ProtocolError("Mode was not set."); } mBuffer.write(payload); if (mFinal) { byte[] message = mBuffer.toByteArray(); if (mMode == MODE_TEXT) { onMessage(encode(message)); } else { onMessage(message); } reset(); } } else if (opcode == OP_TEXT) { if (mFinal) { String messageText = encode(payload); onMessage(messageText); } else { mMode = MODE_TEXT; mBuffer.write(payload); } } else if (opcode == OP_BINARY) { if (mFinal) { onMessage(payload); } else { mMode = MODE_BINARY; mBuffer.write(payload); } } else if (opcode == OP_CLOSE) { int code = (payload.length >= 2) ? 256 * (payload[0] & 0xFF) + (payload[1] & 0xFF) : 0; String reason = (payload.length > 2) ? encode(slice(payload, 2)) : null; // Log.d(TAG, "Got close op! " + code + " " + reason); onDisconnect(code, reason); } else if (opcode == OP_PING) { if (payload.length > 125) { throw new ProtocolError("Ping payload too large"); } // Log.d(TAG, "Sending pong!!"); String message = encode(payload); sendFrame(frame(OP_PONG, payload, -1)); onPing(message); } else if (opcode == OP_PONG) { String message = encode(payload); onPong(message); // Log.d(TAG, "Got pong! " + message); } } protected abstract void onMessage(byte[] payload); protected abstract void onMessage(String payload); protected abstract void onPong(String payload); protected abstract void onPing(String payload); protected abstract void onDisconnect(int code, String reason); protected abstract void report(Exception ex); protected abstract void sendFrame(byte[] frame); private void reset() { mMode = 0; mBuffer.reset(); } private String encode(byte[] buffer) { try { return new String(buffer, "UTF-8"); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } private byte[] decode(String string) { try { return (string).getBytes("UTF-8"); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } private int getInteger(byte[] bytes) throws ProtocolError { long i = byteArrayToLong(bytes, 0, bytes.length); if (i < 0 || i > Integer.MAX_VALUE) { throw new ProtocolError("Bad integer: " + i); } return (int) i; } private byte[] slice(byte[] array, int start) { byte[] copy = new byte[array.length - start]; System.arraycopy(array, start, copy, 0, array.length - start); return copy; } @Override protected void finalize() throws Throwable { Inflater inflater = mInflater; if (inflater != null) { try { inflater.end(); } catch (Exception e) { Log.e(TAG, "inflater.end failed", e); } } super.finalize(); } public static class ProtocolError extends IOException { public ProtocolError(String detailMessage) { super(detailMessage); } } private static long byteArrayToLong(byte[] b, int offset, int length) { if (b.length < length) throw new IllegalArgumentException("length must be less than or equal to b.length"); long value = 0; for (int i = 0; i < length; i++) { int shift = (length - 1 - i) * 8; value += (b[i + offset] & 0x000000FF) << shift; } return value; } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/Multimap.java ================================================ package com.koushikdutta.async.http; import android.net.Uri; import android.text.TextUtils; import com.koushikdutta.async.util.TaggedList; import java.net.URLDecoder; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** * Created by koush on 5/27/13. */ public class Multimap extends LinkedHashMap> implements Iterable { public Multimap() { } protected List newList() { return new ArrayList(); } public String getString(String name) { List ret = get(name); if (ret == null || ret.size() == 0) return null; return ret.get(0); } public String getAllString(String name, String delimiter) { List ret = get(name); if (ret == null || ret.size() == 0) return null; StringBuilder builder = new StringBuilder(); boolean first = true; for (String value: ret) { if (!first) builder.append(delimiter); builder.append(value); first = false; } return builder.toString(); } public List ensure(String name) { List ret = get(name); if (ret == null) { ret = newList(); put(name, ret); } return ret; } public void add(String name, String value) { ensure(name).add(value); } public void put(String name, String value) { List ret = newList(); ret.add(value); put(name, ret); } public Multimap(List pairs) { for (NameValuePair pair: pairs) add(pair.getName(), pair.getValue()); } public Multimap(Multimap m) { putAll(m); } public interface StringDecoder { public String decode(String s); } public static Multimap parse(String value, String delimiter, boolean unquote, StringDecoder decoder) { return parse(value, delimiter, "=", unquote, decoder); } public static Multimap parse(String value, String delimiter, String assigner, boolean unquote, StringDecoder decoder) { Multimap map = new Multimap(); if (value == null) return map; String[] parts = value.split(delimiter); for (String part: parts) { String[] pair = part.split(assigner, 2); String key = pair[0].trim(); // watch for empty string or trailing delimiter if (TextUtils.isEmpty(key)) continue; String v = null; if (pair.length > 1) v = pair[1]; if (v != null && unquote && v.endsWith("\"") && v.startsWith("\"")) v = v.substring(1, v.length() - 1); if (v != null && decoder != null) { key = decoder.decode(key); v = decoder.decode(v); } map.add(key, v); } return map; } public static Multimap parseSemicolonDelimited(String header) { return parse(header, ";", true, null); } public static Multimap parseCommaDelimited(String header) { return parse(header, ",", true, null); } public static final StringDecoder QUERY_DECODER = new StringDecoder() { @Override public String decode(String s) { return Uri.decode(s); } }; public static Multimap parseQuery(String query) { return parse(query, "&", false, QUERY_DECODER); } public static final StringDecoder URL_DECODER = new StringDecoder() { @Override public String decode(String s) { return URLDecoder.decode(s); } }; public static Multimap parseUrlEncoded(String query) { return parse(query, "&", false, URL_DECODER); } @Override public Iterator iterator() { ArrayList ret = new ArrayList(); for (String name: keySet()) { List values = get(name); for (String value: values) { ret.add(new BasicNameValuePair(name, value)); } } return ret.iterator(); } public Map toSingleMap() { HashMap ret = new HashMap<>(); for (String key: keySet()) { ret.put(key, getString(key)); } return ret; } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/NameValuePair.java ================================================ /* * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpcore/trunk/module-main/src/main/java/org/apache/http/NameValuePair.java $ * $Revision: 496070 $ * $Date: 2007-01-14 04:18:34 -0800 (Sun, 14 Jan 2007) $ * * ==================================================================== * 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. * ==================================================================== * * This software consists of voluntary contributions made by many * individuals on behalf of the Apache Software Foundation. For more * information on the Apache Software Foundation, please see * . * */ package com.koushikdutta.async.http; /** * A simple class encapsulating an attribute/value pair. *

* This class comforms to the generic grammar and formatting rules outlined in the * Section 2.2 * and * Section 3.6 * of RFC 2616 *

* 2.2 Basic Rules *

* The following rules are used throughout this specification to describe basic parsing constructs. * The US-ASCII coded character set is defined by ANSI X3.4-1986. *

*
 *     OCTET          = 
 *     CHAR           = 
 *     UPALPHA        = 
 *     LOALPHA        = 
 *     ALPHA          = UPALPHA | LOALPHA
 *     DIGIT          = 
 *     CTL            = 
 *     CR             = 
 *     LF             = 
 *     SP             = 
 *     HT             = 
 *     <">            = 
 * 
*

* Many HTTP/1.1 header field values consist of words separated by LWS or special * characters. These special characters MUST be in a quoted string to be used within * a parameter value (as defined in section 3.6). *

*

 * token          = 1*
 * separators     = "(" | ")" | "<" | ">" | "@"
 *                | "," | ";" | ":" | "\" | <">
 *                | "/" | "[" | "]" | "?" | "="
 *                | "{" | "}" | SP | HT
 * 
*

* A string of text is parsed as a single word if it is quoted using double-quote marks. *

*
 * quoted-string  = ( <"> *(qdtext | quoted-pair ) <"> )
 * qdtext         = >
 * 
*

* The backslash character ("\") MAY be used as a single-character quoting mechanism only * within quoted-string and comment constructs. *

*
 * quoted-pair    = "\" CHAR
 * 
* 3.6 Transfer Codings *

* Parameters are in the form of attribute/value pairs. *

*
 * parameter               = attribute "=" value
 * attribute               = token
 * value                   = token | quoted-string
 * 
* * @author Oleg Kalnichevski * */ public interface NameValuePair { String getName(); String getValue(); } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/Protocol.java ================================================ package com.koushikdutta.async.http; import java.util.Hashtable; import java.util.Locale; /** * Protocols that OkHttp implements for NPN and * ALPN * selection. *

*

Protocol vs Scheme

* Despite its name, {@link java.net.URL#getProtocol()} returns the * {@linkplain java.net.URI#getScheme() scheme} (http, https, etc.) of the URL, not * the protocol (http/1.1, spdy/3.1, etc.). OkHttp uses the word protocol * to identify how HTTP messages are framed. */ public enum Protocol { /** * An obsolete plaintext framing that does not use persistent sockets by * default. */ HTTP_1_0("http/1.0"), /** * A plaintext framing that includes persistent connections. *

*

This version of OkHttp implements RFC 2616, and tracks * revisions to that spec. */ HTTP_1_1("http/1.1"), /** * Chromium's binary-framed protocol that includes header compression, * multiplexing multiple requests on the same socket, and server-push. * HTTP/1.1 semantics are layered on SPDY/3. *

*

This version of OkHttp implements SPDY 3 draft * 3.1. Future releases of OkHttp may use this identifier for a newer draft * of the SPDY spec. */ SPDY_3("spdy/3.1") { @Override public boolean needsSpdyConnection() { return true; } }, /** * The IETF's binary-framed protocol that includes header compression, * multiplexing multiple requests on the same socket, and server-push. * HTTP/1.1 semantics are layered on HTTP/2. *

*

This version of OkHttp implements HTTP/2 draft 12 * with HPACK draft * 6. Future releases of OkHttp may use this identifier for a newer draft * of these specs. */ HTTP_2("h2-13") { @Override public boolean needsSpdyConnection() { return true; } }; private final String protocol; private static final Hashtable protocols = new Hashtable(); static { protocols.put(HTTP_1_0.toString(), HTTP_1_0); protocols.put(HTTP_1_1.toString(), HTTP_1_1); protocols.put(SPDY_3.toString(), SPDY_3); protocols.put(HTTP_2.toString(), HTTP_2); } Protocol(String protocol) { this.protocol = protocol; } /** * Returns the protocol identified by {@code protocol}. */ public static Protocol get(String protocol) { if (protocol == null) return null; return protocols.get(protocol.toLowerCase(Locale.US)); } /** * Returns the string used to identify this protocol for ALPN and NPN, like * "http/1.1", "spdy/3.1" or "h2-13". */ @Override public String toString() { return protocol; } public boolean needsSpdyConnection() { return false; } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/ProtocolVersion.java ================================================ /* * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpcore/trunk/module-main/src/main/java/org/apache/http/ProtocolVersion.java $ * $Revision: 609106 $ * $Date: 2008-01-05 01:15:42 -0800 (Sat, 05 Jan 2008) $ * * ==================================================================== * 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. * ==================================================================== * * This software consists of voluntary contributions made by many * individuals on behalf of the Apache Software Foundation. For more * information on the Apache Software Foundation, please see * . * */ package com.koushikdutta.async.http; import java.io.Serializable; /** * Represents a protocol version, as specified in RFC 2616. * RFC 2616 specifies only HTTP versions, like "HTTP/1.1" and "HTTP/1.0". * RFC 3261 specifies a message format that is identical to HTTP except * for the protocol name. It defines a protocol version "SIP/2.0". * There are some nitty-gritty differences between the interpretation * of versions in HTTP and SIP. In those cases, HTTP takes precedence. *

* This class defines a protocol version as a combination of * protocol name, major version number, and minor version number. * Note that {@link #equals} and {@link #hashCode} are defined as * final here, they cannot be overridden in derived classes. * * @author Oleg Kalnichevski * @author Roland Weber * * @version $Revision: 609106 $ */ public class ProtocolVersion implements Serializable, Cloneable { private static final long serialVersionUID = 8950662842175091068L; /** Name of the protocol. */ protected final String protocol; /** Major version number of the protocol */ protected final int major; /** Minor version number of the protocol */ protected final int minor; /** * Create a protocol version designator. * * @param protocol the name of the protocol, for example "HTTP" * @param major the major version number of the protocol * @param minor the minor version number of the protocol */ public ProtocolVersion(String protocol, int major, int minor) { if (protocol == null) { throw new IllegalArgumentException ("Protocol name must not be null."); } if (major < 0) { throw new IllegalArgumentException ("Protocol major version number must not be negative."); } if (minor < 0) { throw new IllegalArgumentException ("Protocol minor version number may not be negative"); } this.protocol = protocol; this.major = major; this.minor = minor; } /** * Returns the name of the protocol. * * @return the protocol name */ public final String getProtocol() { return protocol; } /** * Returns the major version number of the protocol. * * @return the major version number. */ public final int getMajor() { return major; } /** * Returns the minor version number of the HTTP protocol. * * @return the minor version number. */ public final int getMinor() { return minor; } /** * Obtains a specific version of this protocol. * This can be used by derived classes to instantiate themselves instead * of the base class, and to define constants for commonly used versions. *
* The default implementation in this class returns this * if the version matches, and creates a new {@link ProtocolVersion} * otherwise. * * @param major the major version * @param minor the minor version * * @return a protocol version with the same protocol name * and the argument version */ public ProtocolVersion forVersion(int major, int minor) { if ((major == this.major) && (minor == this.minor)) { return this; } // argument checking is done in the constructor return new ProtocolVersion(this.protocol, major, minor); } /** * Obtains a hash code consistent with {@link #equals}. * * @return the hashcode of this protocol version */ public final int hashCode() { return this.protocol.hashCode() ^ (this.major * 100000) ^ this.minor; } /** * Checks equality of this protocol version with an object. * The object is equal if it is a protocl version with the same * protocol name, major version number, and minor version number. * The specific class of the object is not relevant, * instances of derived classes with identical attributes are * equal to instances of the base class and vice versa. * * @param obj the object to compare with * * @return true if the argument is the same protocol version, * false otherwise */ public final boolean equals(Object obj) { if (this == obj) { return true; } if (!(obj instanceof ProtocolVersion)) { return false; } ProtocolVersion that = (ProtocolVersion) obj; return ((this.protocol.equals(that.protocol)) && (this.major == that.major) && (this.minor == that.minor)); } /** * Checks whether this protocol can be compared to another one. * Only protocol versions with the same protocol name can be * {@link #compareToVersion compared}. * * @param that the protocol version to consider * * @return true if {@link #compareToVersion compareToVersion} * can be called with the argument, false otherwise */ public boolean isComparable(ProtocolVersion that) { return (that != null) && this.protocol.equals(that.protocol); } /** * Compares this protocol version with another one. * Only protocol versions with the same protocol name can be compared. * This method does not define a total ordering, as it would be * required for {@link java.lang.Comparable}. * * @param that the protocl version to compare with * * @return a negative integer, zero, or a positive integer * as this version is less than, equal to, or greater than * the argument version. * * @throws IllegalArgumentException * if the argument has a different protocol name than this object, * or if the argument is null */ public int compareToVersion(ProtocolVersion that) { if (that == null) { throw new IllegalArgumentException ("Protocol version must not be null."); } if (!this.protocol.equals(that.protocol)) { throw new IllegalArgumentException ("Versions for different protocols cannot be compared. " + this + " " + that); } int delta = getMajor() - that.getMajor(); if (delta == 0) { delta = getMinor() - that.getMinor(); } return delta; } /** * Tests if this protocol version is greater or equal to the given one. * * @param version the version against which to check this version * * @return true if this protocol version is * {@link #isComparable comparable} to the argument * and {@link #compareToVersion compares} as greater or equal, * false otherwise */ public final boolean greaterEquals(ProtocolVersion version) { return isComparable(version) && (compareToVersion(version) >= 0); } /** * Tests if this protocol version is less or equal to the given one. * * @param version the version against which to check this version * * @return true if this protocol version is * {@link #isComparable comparable} to the argument * and {@link #compareToVersion compares} as less or equal, * false otherwise */ public final boolean lessEquals(ProtocolVersion version) { return isComparable(version) && (compareToVersion(version) <= 0); } /** * Converts this protocol version to a string. * * @return a protocol version string, like "HTTP/1.1" */ public String toString() { StringBuilder buffer = new StringBuilder(); buffer.append(this.protocol); buffer.append('/'); buffer.append(Integer.toString(this.major)); buffer.append('.'); buffer.append(Integer.toString(this.minor)); return buffer.toString(); } public Object clone() throws CloneNotSupportedException { return super.clone(); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/RedirectLimitExceededException.java ================================================ package com.koushikdutta.async.http; public class RedirectLimitExceededException extends Exception { public RedirectLimitExceededException(String message) { super(message); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/RequestLine.java ================================================ /* * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpcore/trunk/module-main/src/main/java/org/apache/http/RequestLine.java $ * $Revision: 573864 $ * $Date: 2007-09-08 08:53:25 -0700 (Sat, 08 Sep 2007) $ * * ==================================================================== * 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. * ==================================================================== * * This software consists of voluntary contributions made by many * individuals on behalf of the Apache Software Foundation. For more * information on the Apache Software Foundation, please see * . * */ package com.koushikdutta.async.http; /** * The first line of an {@link HttpRequest HttpRequest}. * It contains the method, URI, and HTTP version of the request. * For details, see RFC 2616. * * @author Oleg Kalnichevski * * @version $Revision: 573864 $ * * @since 4.0 */ public interface RequestLine { String getMethod(); ProtocolVersion getProtocolVersion(); String getUri(); } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/SSLEngineSNIConfigurator.java ================================================ package com.koushikdutta.async.http; import android.os.Build; import java.lang.reflect.Field; import java.util.Hashtable; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; /** * Created by koush on 12/8/14. */ public class SSLEngineSNIConfigurator implements AsyncSSLEngineConfigurator { private static class EngineHolder implements AsyncSSLEngineConfigurator { Field peerHost; Field peerPort; Field sslParameters; Field useSni; boolean skipReflection; @Override public SSLEngine createEngine(SSLContext sslContext, String peerHost, int peerPort) { return null; } public EngineHolder(Class engineClass) { try { peerHost = engineClass.getSuperclass().getDeclaredField("peerHost"); peerHost.setAccessible(true); peerPort = engineClass.getSuperclass().getDeclaredField("peerPort"); peerPort.setAccessible(true); sslParameters = engineClass.getDeclaredField("sslParameters"); sslParameters.setAccessible(true); useSni = sslParameters.getType().getDeclaredField("useSni"); useSni.setAccessible(true); } catch (NoSuchFieldException e) { } } @Override public void configureEngine(SSLEngine engine, AsyncHttpClientMiddleware.GetSocketData data, String host, int port) { if (useSni == null || skipReflection) return; try { peerHost.set(engine, host); peerPort.set(engine, port); Object sslp = sslParameters.get(engine); useSni.set(sslp, true); } catch (IllegalAccessException e) { } } } Hashtable holders = new Hashtable(); @Override public SSLEngine createEngine(SSLContext sslContext, String peerHost, int peerPort) { // pre M, must use reflection to enable SNI, otherwise createSSLEngine(peerHost, peerPort) works. SSLEngine engine; boolean skipReflection = "GmsCore_OpenSSL".equals(sslContext.getProvider().getName()) || Build.VERSION.SDK_INT >= Build.VERSION_CODES.M; if (skipReflection) engine = sslContext.createSSLEngine(peerHost, peerPort); else engine = sslContext.createSSLEngine(); // ensureHolder(engine).skipReflection = skipReflection; return engine; } EngineHolder ensureHolder(SSLEngine engine) { String name = engine.getClass().getCanonicalName(); EngineHolder holder = holders.get(name); if (holder == null) { holder = new EngineHolder(engine.getClass()); holders.put(name, holder); } return holder; } @Override public void configureEngine(SSLEngine engine, AsyncHttpClientMiddleware.GetSocketData data, String host, int port) { EngineHolder holder = ensureHolder(engine); holder.configureEngine(engine, data, host, port); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/SimpleMiddleware.java ================================================ package com.koushikdutta.async.http; import com.koushikdutta.async.future.Cancellable; public class SimpleMiddleware implements AsyncHttpClientMiddleware { @Override public void onRequest(OnRequestData data) { } @Override public Cancellable getSocket(GetSocketData data) { return null; } @Override public boolean exchangeHeaders(OnExchangeHeaderData data) { return false; } @Override public void onRequestSent(OnRequestSentData data) { } @Override public void onHeadersReceived(OnHeadersReceivedData data) { } @Override public void onBodyDecoder(OnBodyDecoderData data) { } @Override public AsyncHttpRequest onResponseReady(OnResponseReadyData data) { return null; } @Override public void onResponseComplete(OnResponseCompleteData data) { } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/WebSocket.java ================================================ package com.koushikdutta.async.http; import com.koushikdutta.async.AsyncSocket; public interface WebSocket extends AsyncSocket { interface StringCallback { void onStringAvailable(String s); } interface PingCallback { void onPingReceived(String s); } interface PongCallback { void onPongReceived(String s); } void send(byte[] bytes); void send(String string); void send(byte [] bytes, int offset, int len); void ping(String message); void pong(String message); void setStringCallback(StringCallback callback); StringCallback getStringCallback(); void setPingCallback(PingCallback callback); void setPongCallback(PongCallback callback); PongCallback getPongCallback(); boolean isBuffering(); String getProtocol(); AsyncSocket getSocket(); } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/WebSocketHandshakeException.java ================================================ package com.koushikdutta.async.http; public class WebSocketHandshakeException extends Exception { public WebSocketHandshakeException(String message) { super(message); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/WebSocketImpl.java ================================================ package com.koushikdutta.async.http; import android.text.TextUtils; import android.util.Base64; import com.koushikdutta.async.AsyncServer; import com.koushikdutta.async.AsyncSocket; import com.koushikdutta.async.BufferedDataSink; import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.Util; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.DataCallback; import com.koushikdutta.async.callback.WritableCallback; import com.koushikdutta.async.http.server.AsyncHttpServerRequest; import com.koushikdutta.async.http.server.AsyncHttpServerResponse; import java.nio.ByteBuffer; import java.nio.LongBuffer; import java.security.MessageDigest; import java.util.LinkedList; import java.util.UUID; public class WebSocketImpl implements WebSocket { @Override public void end() { mSocket.end(); } private static byte[] toByteArray(UUID uuid) { byte[] byteArray = new byte[(Long.SIZE / Byte.SIZE) * 2]; ByteBuffer buffer = ByteBuffer.wrap(byteArray); LongBuffer longBuffer = buffer.asLongBuffer(); longBuffer.put(new long[] { uuid.getMostSignificantBits(),uuid.getLeastSignificantBits() }); return byteArray; } private static String SHA1(String text) { try { MessageDigest md = MessageDigest.getInstance("SHA-1"); md.update(text.getBytes("iso-8859-1"), 0, text.length()); byte[] sha1hash = md.digest(); return Base64.encodeToString(sha1hash, Base64.NO_WRAP); } catch (Exception ex) { return null; } } final static String MAGIC = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; private LinkedList pending; private void addAndEmit(ByteBufferList bb) { if (pending == null) { Util.emitAllData(this, bb); if (bb.remaining() > 0) { pending = new LinkedList(); pending.add(bb); } return; } while (!isPaused()) { bb = pending.remove(); Util.emitAllData(this, bb); if (bb.remaining() > 0) pending.add(0, bb); } if (pending.size() == 0) pending = null; } private void setupParser(boolean masking, boolean deflate) { mParser = new HybiParser(mSocket) { @Override protected void report(Exception ex) { if (WebSocketImpl.this.mExceptionCallback != null) WebSocketImpl.this.mExceptionCallback.onCompleted(ex); } @Override protected void onMessage(byte[] payload) { addAndEmit(new ByteBufferList(payload)); } @Override protected void onMessage(String payload) { if (WebSocketImpl.this.mStringCallback != null) WebSocketImpl.this.mStringCallback.onStringAvailable(payload); } @Override protected void onDisconnect(int code, String reason) { mSocket.close(); // if (WebSocketImpl.this.mClosedCallback != null) // WebSocketImpl.this.mClosedCallback.onCompleted(null); } @Override protected void sendFrame(byte[] frame) { mSink.write(new ByteBufferList(frame)); } @Override protected void onPing(String payload) { if (WebSocketImpl.this.mPingCallback != null) WebSocketImpl.this.mPingCallback.onPingReceived(payload); } @Override protected void onPong(String payload) { if (WebSocketImpl.this.mPongCallback != null) WebSocketImpl.this.mPongCallback.onPongReceived(payload); } }; mParser.setMasking(masking); mParser.setDeflate(deflate); if (mSocket.isPaused()) mSocket.resume(); } private AsyncSocket mSocket; BufferedDataSink mSink; public WebSocketImpl(AsyncHttpServerRequest request, AsyncHttpServerResponse response) { this(request.getSocket()); String key = request.getHeaders().get("Sec-WebSocket-Key"); String concat = key + MAGIC; String sha1 = SHA1(concat); String origin = request.getHeaders().get("Origin"); response.code(101); response.getHeaders().set("Upgrade", "WebSocket"); response.getHeaders().set("Connection", "Upgrade"); response.getHeaders().set("Sec-WebSocket-Accept", sha1); String protocol = request.getHeaders().get("Sec-WebSocket-Protocol"); // match the protocol (sanity checking and enforcement is done in the caller) if (!TextUtils.isEmpty(protocol)) response.getHeaders().set("Sec-WebSocket-Protocol", protocol); // if (origin != null) // response.getHeaders().getHeaders().set("Access-Control-Allow-Origin", "http://" + origin); response.writeHead(); setupParser(false, false); } String protocol; @Override public String getProtocol() { return protocol; } public static void addWebSocketUpgradeHeaders(AsyncHttpRequest req, String... protocols) { Headers headers = req.getHeaders(); final String key = Base64.encodeToString(toByteArray(UUID.randomUUID()),Base64.NO_WRAP); headers.set("Sec-WebSocket-Version", "13"); headers.set("Sec-WebSocket-Key", key); headers.set("Sec-WebSocket-Extensions", "x-webkit-deflate-frame"); headers.set("Connection", "Upgrade"); headers.set("Upgrade", "websocket"); if (protocols != null) { for (String protocol: protocols) { headers.add("Sec-WebSocket-Protocol", protocol); } } headers.set("Pragma", "no-cache"); headers.set("Cache-Control", "no-cache"); if (TextUtils.isEmpty(req.getHeaders().get("User-Agent"))) req.getHeaders().set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.15 Safari/537.36"); } public WebSocketImpl(AsyncSocket socket) { mSocket = socket; mSink = new BufferedDataSink(mSocket); } public static WebSocket finishHandshake(Headers requestHeaders, AsyncHttpResponse response) { if (response == null) return null; if (response.code() != 101) return null; if (!"websocket".equalsIgnoreCase(response.headers().get("Upgrade"))) return null; String sha1 = response.headers().get("Sec-WebSocket-Accept"); if (sha1 == null) return null; String key = requestHeaders.get("Sec-WebSocket-Key"); if (key == null) return null; String concat = key + MAGIC; String expected = SHA1(concat).trim(); if (!sha1.equalsIgnoreCase(expected)) return null; String extensions = requestHeaders.get("Sec-WebSocket-Extensions"); boolean deflate = false; if (extensions != null) { if (extensions.equals("x-webkit-deflate-frame")) deflate = true; // is this right? do we want to crap out here? Commenting out // as I suspect this caused a regression. // else // return null; } WebSocketImpl ret = new WebSocketImpl(response.detachSocket()); ret.protocol = response.headers().get("Sec-WebSocket-Protocol"); ret.setupParser(true, deflate); return ret; } HybiParser mParser; @Override public void close() { mSocket.close(); } @Override public void setClosedCallback(CompletedCallback handler) { mSocket.setClosedCallback(handler); } @Override public CompletedCallback getClosedCallback() { return mSocket.getClosedCallback(); } CompletedCallback mExceptionCallback; @Override public void setEndCallback(CompletedCallback callback) { mExceptionCallback = callback; } @Override public CompletedCallback getEndCallback() { return mExceptionCallback; } @Override public void send(byte[] bytes) { getServer().post(() -> mSink.write(new ByteBufferList((mParser.frame(bytes))))); } @Override public void send(byte[] bytes, int offset, int len) { getServer().post(() -> mSink.write(new ByteBufferList(mParser.frame(bytes, offset, len)))); } @Override public void send(String string) { getServer().post(() -> mSink.write(new ByteBufferList((mParser.frame(string))))); } @Override public void ping(String string) { getServer().post(() -> mSink.write(new ByteBufferList(ByteBuffer.wrap(mParser.pingFrame(string))))); } @Override public void pong(String string) { getServer().post(() -> mSink.write(new ByteBufferList(ByteBuffer.wrap(mParser.pongFrame(string))))); } private StringCallback mStringCallback; @Override public void setStringCallback(StringCallback callback) { mStringCallback = callback; } private DataCallback mDataCallback; @Override public void setDataCallback(DataCallback callback) { mDataCallback = callback; } @Override public StringCallback getStringCallback() { return mStringCallback; } private PingCallback mPingCallback; @Override public void setPingCallback(PingCallback callback) { mPingCallback = callback; } private PongCallback mPongCallback; @Override public void setPongCallback(PongCallback callback) { mPongCallback = callback; } @Override public PongCallback getPongCallback() { return mPongCallback; } @Override public DataCallback getDataCallback() { return mDataCallback; } @Override public boolean isOpen() { return mSocket.isOpen(); } @Override public boolean isBuffering() { return mSink.remaining() > 0; } @Override public void write(ByteBufferList bb) { byte[] buf = bb.getAllByteArray(); send(buf); } @Override public void setWriteableCallback(WritableCallback handler) { mSink.setWriteableCallback(handler); } @Override public WritableCallback getWriteableCallback() { return mSink.getWriteableCallback(); } @Override public AsyncSocket getSocket() { return mSocket; } @Override public AsyncServer getServer() { return mSocket.getServer(); } @Override public boolean isChunked() { return false; } @Override public void pause() { mSocket.pause(); } @Override public void resume() { mSocket.resume(); } @Override public boolean isPaused() { return mSocket.isPaused(); } @Override public String charset() { return null; } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/body/AsyncHttpRequestBody.java ================================================ package com.koushikdutta.async.http.body; import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.DataSink; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.http.AsyncHttpRequest; public interface AsyncHttpRequestBody { public void write(AsyncHttpRequest request, DataSink sink, CompletedCallback completed); public void parse(DataEmitter emitter, CompletedCallback completed); public String getContentType(); public boolean readFullyOnRequest(); public int length(); public T get(); } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/body/ByteBufferListRequestBody.java ================================================ package com.koushikdutta.async.http.body; import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.DataSink; import com.koushikdutta.async.Util; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.http.AsyncHttpRequest; import com.koushikdutta.async.parser.ByteBufferListParser; public class ByteBufferListRequestBody implements AsyncHttpRequestBody { public ByteBufferListRequestBody() { } ByteBufferList bb; public ByteBufferListRequestBody(ByteBufferList bb) { this.bb = bb; } @Override public void write(AsyncHttpRequest request, DataSink sink, CompletedCallback completed) { Util.writeAll(sink, bb, completed); } @Override public void parse(DataEmitter emitter, CompletedCallback completed) { new ByteBufferListParser().parse(emitter).setCallback((e, result) -> { bb = result; completed.onCompleted(e); }); } public static String CONTENT_TYPE = "application/binary"; @Override public String getContentType() { return CONTENT_TYPE; } @Override public boolean readFullyOnRequest() { return true; } @Override public int length() { return bb.remaining(); } @Override public ByteBufferList get() { return bb; } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/body/DocumentBody.java ================================================ package com.koushikdutta.async.http.body; import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.DataSink; import com.koushikdutta.async.Util; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.future.FutureCallback; import com.koushikdutta.async.http.AsyncHttpRequest; import com.koushikdutta.async.parser.DocumentParser; import com.koushikdutta.async.util.Charsets; import org.w3c.dom.Document; import java.io.ByteArrayOutputStream; import java.io.OutputStreamWriter; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; /** * Created by koush on 8/30/13. */ public class DocumentBody implements AsyncHttpRequestBody { public DocumentBody() { this(null); } public DocumentBody(Document document) { this.document = document; } ByteArrayOutputStream bout; private void prepare() { if (bout != null) return; try { DOMSource source = new DOMSource(document); TransformerFactory tf = TransformerFactory.newInstance(); Transformer transformer = tf.newTransformer(); bout = new ByteArrayOutputStream(); OutputStreamWriter writer = new OutputStreamWriter(bout, Charsets.UTF_8); StreamResult result = new StreamResult(writer); transformer.transform(source, result); writer.flush(); } catch (Exception e) { } } @Override public void write(AsyncHttpRequest request, DataSink sink, CompletedCallback completed) { prepare(); byte[] bytes = bout.toByteArray(); Util.writeAll(sink, bytes, completed); } @Override public void parse(DataEmitter emitter, final CompletedCallback completed) { new DocumentParser().parse(emitter).setCallback(new FutureCallback() { @Override public void onCompleted(Exception e, Document result) { document = result; completed.onCompleted(e); } }); } public static final String CONTENT_TYPE = "application/xml"; @Override public String getContentType() { return CONTENT_TYPE; } @Override public boolean readFullyOnRequest() { return true; } @Override public int length() { prepare(); return bout.size(); } Document document; @Override public Document get() { return document; } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/body/FileBody.java ================================================ package com.koushikdutta.async.http.body; import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.DataSink; import com.koushikdutta.async.Util; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.http.AsyncHttpRequest; import java.io.File; /** * Created by koush on 10/14/13. */ public class FileBody implements AsyncHttpRequestBody { public static final String CONTENT_TYPE = "application/binary"; File file; String contentType = CONTENT_TYPE; public FileBody(File file) { this.file = file; } public FileBody(File file, String contentType) { this.file = file; this.contentType = contentType; } @Override public void write(AsyncHttpRequest request, DataSink sink, CompletedCallback completed) { Util.pump(file, sink, completed); } @Override public void parse(DataEmitter emitter, CompletedCallback completed) { throw new AssertionError("not implemented"); } @Override public String getContentType() { return contentType; } public void setContentType(String contentType) { this.contentType = contentType; } @Override public boolean readFullyOnRequest() { throw new AssertionError("not implemented"); } @Override public int length() { return (int)file.length(); } @Override public File get() { return file; } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/body/FilePart.java ================================================ package com.koushikdutta.async.http.body; import com.koushikdutta.async.http.BasicNameValuePair; import com.koushikdutta.async.http.NameValuePair; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; public class FilePart extends StreamPart { File file; public FilePart(String name, final File file) { super(name, (int)file.length(), new ArrayList() { { add(new BasicNameValuePair("filename", file.getName())); } }); // getRawHeaders().set("Content-Type", "application/xml"); this.file = file; } @Override protected InputStream getInputStream() throws IOException { return new FileInputStream(file); } @Override public String toString() { return getName(); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/body/JSONArrayBody.java ================================================ package com.koushikdutta.async.http.body; import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.DataSink; import com.koushikdutta.async.Util; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.future.FutureCallback; import com.koushikdutta.async.http.AsyncHttpRequest; import com.koushikdutta.async.parser.JSONArrayParser; import org.json.JSONArray; public class JSONArrayBody implements AsyncHttpRequestBody { public JSONArrayBody() { } byte[] mBodyBytes; JSONArray json; public JSONArrayBody(JSONArray json) { this(); this.json = json; } @Override public void parse(DataEmitter emitter, final CompletedCallback completed) { new JSONArrayParser().parse(emitter).setCallback(new FutureCallback() { @Override public void onCompleted(Exception e, JSONArray result) { json = result; completed.onCompleted(e); } }); } @Override public void write(AsyncHttpRequest request, DataSink sink, final CompletedCallback completed) { Util.writeAll(sink, mBodyBytes, completed); } @Override public String getContentType() { return "application/json"; } @Override public boolean readFullyOnRequest() { return true; } @Override public int length() { mBodyBytes = json.toString().getBytes(); return mBodyBytes.length; } public static final String CONTENT_TYPE = "application/json"; @Override public JSONArray get() { return json; } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/body/JSONObjectBody.java ================================================ package com.koushikdutta.async.http.body; import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.DataSink; import com.koushikdutta.async.Util; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.future.FutureCallback; import com.koushikdutta.async.http.AsyncHttpRequest; import com.koushikdutta.async.parser.JSONObjectParser; import org.json.JSONObject; public class JSONObjectBody implements AsyncHttpRequestBody { public JSONObjectBody() { } byte[] mBodyBytes; JSONObject json; public JSONObjectBody(JSONObject json) { this(); this.json = json; } @Override public void parse(DataEmitter emitter, final CompletedCallback completed) { new JSONObjectParser().parse(emitter).setCallback(new FutureCallback() { @Override public void onCompleted(Exception e, JSONObject result) { json = result; completed.onCompleted(e); } }); } @Override public void write(AsyncHttpRequest request, DataSink sink, final CompletedCallback completed) { Util.writeAll(sink, mBodyBytes, completed); } @Override public String getContentType() { return CONTENT_TYPE; } @Override public boolean readFullyOnRequest() { return true; } @Override public int length() { mBodyBytes = json.toString().getBytes(); return mBodyBytes.length; } public static final String CONTENT_TYPE = "application/json"; @Override public JSONObject get() { return json; } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/body/MultipartFormDataBody.java ================================================ package com.koushikdutta.async.http.body; import android.text.TextUtils; import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.DataSink; import com.koushikdutta.async.LineEmitter; import com.koushikdutta.async.LineEmitter.StringCallback; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.ContinuationCallback; import com.koushikdutta.async.callback.DataCallback; import com.koushikdutta.async.future.Continuation; import com.koushikdutta.async.http.AsyncHttpRequest; import com.koushikdutta.async.http.Headers; import com.koushikdutta.async.http.Multimap; import com.koushikdutta.async.http.server.BoundaryEmitter; import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.UUID; public class MultipartFormDataBody extends BoundaryEmitter implements AsyncHttpRequestBody { LineEmitter liner; Headers formData; ByteBufferList lastData; Part lastPart; public interface MultipartCallback { public void onPart(Part part); } @Override public void parse(DataEmitter emitter, final CompletedCallback completed) { setDataEmitter(emitter); setEndCallback(completed); } void handleLast() { if (lastData == null) return; if (formData == null) formData = new Headers(); String value = lastData.peekString(); String name = TextUtils.isEmpty(lastPart.getName()) ? "unnamed" : lastPart.getName(); StringPart part = new StringPart(name, value); part.mHeaders = lastPart.mHeaders; addPart(part); formData.add(name, value); lastPart = null; lastData = null; } public String getField(String name) { if (formData == null) return null; return formData.get(name); } @Override protected void onBoundaryEnd() { super.onBoundaryEnd(); handleLast(); } @Override protected void onBoundaryStart() { final Headers headers = new Headers(); liner = new LineEmitter(); liner.setLineCallback(new StringCallback() { @Override public void onStringAvailable(String s) { if (!"\r".equals(s)){ headers.addLine(s); } else { handleLast(); liner = null; setDataCallback(null); Part part = new Part(headers); if (mCallback != null) mCallback.onPart(part); if (getDataCallback() == null) { // if (part.isFile()) { // setDataCallback(new NullDataCallback()); // return; // } lastPart = part; lastData = new ByteBufferList(); setDataCallback(new DataCallback() { @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { bb.get(lastData); } }); } } } }); setDataCallback(liner); } public static final String PRIMARY_TYPE = "multipart/"; public static final String CONTENT_TYPE = PRIMARY_TYPE + "form-data"; String contentType = CONTENT_TYPE; public MultipartFormDataBody(String contentType) { Multimap map = Multimap.parseSemicolonDelimited(contentType); String boundary = map.getString("boundary"); if (boundary == null) report(new Exception ("No boundary found for multipart/form-data")); else setBoundary(boundary); } MultipartCallback mCallback; public void setMultipartCallback(MultipartCallback callback) { mCallback = callback; } public MultipartCallback getMultipartCallback() { return mCallback; } int written; @Override public void write(AsyncHttpRequest request, final DataSink sink, final CompletedCallback completed) { if (mParts == null) return; Continuation c = new Continuation(new CompletedCallback() { @Override public void onCompleted(Exception ex) { completed.onCompleted(ex); // if (ex == null) // sink.end(); // else // sink.close(); } }); for (final Part part: mParts) { c.add(new ContinuationCallback() { @Override public void onContinue(Continuation continuation, CompletedCallback next) throws Exception { byte[] bytes = part.getRawHeaders().toPrefixString(getBoundaryStart()).getBytes(); com.koushikdutta.async.Util.writeAll(sink, bytes, next); written += bytes.length; } }) .add(new ContinuationCallback() { @Override public void onContinue(Continuation continuation, CompletedCallback next) throws Exception { long partLength = part.length(); if (partLength >= 0) written += partLength; part.write(sink, next); } }) .add(new ContinuationCallback() { @Override public void onContinue(Continuation continuation, CompletedCallback next) throws Exception { byte[] bytes = "\r\n".getBytes(); com.koushikdutta.async.Util.writeAll(sink, bytes, next); written += bytes.length; } }); } c.add(new ContinuationCallback() { @Override public void onContinue(Continuation continuation, CompletedCallback next) throws Exception { byte[] bytes = (getBoundaryEnd()).getBytes(); com.koushikdutta.async.Util.writeAll(sink, bytes, next); written += bytes.length; } }); c.start(); } @Override public String getContentType() { if (getBoundary() == null) { setBoundary("----------------------------" + UUID.randomUUID().toString().replace("-", "")); } return contentType + "; boundary=" + getBoundary(); } @Override public boolean readFullyOnRequest() { return false; } int totalToWrite; @Override public int length() { if (getBoundary() == null) { setBoundary("----------------------------" + UUID.randomUUID().toString().replace("-", "")); } int length = 0; for (final Part part: mParts) { String partHeader = part.getRawHeaders().toPrefixString(getBoundaryStart()); if (part.length() == -1) return -1; length += part.length() + partHeader.getBytes().length + "\r\n".length(); } length += (getBoundaryEnd()).getBytes().length; return totalToWrite = length; } public MultipartFormDataBody() { } public void setContentType(String contentType) { this.contentType = contentType; } public List getParts() { if (mParts == null) return null; return new ArrayList<>(mParts); } public void addFilePart(String name, File file) { addPart(new FilePart(name, file)); } public void addStringPart(String name, String value) { addPart(new StringPart(name, value)); } private ArrayList mParts; public void addPart(Part part) { if (mParts == null) mParts = new ArrayList(); mParts.add(part); } @Override public Multimap get() { return new Multimap(formData.getMultiMap()); } @Override public String toString() { for (Part part: getParts()) { return part.toString(); } return "multipart content is empty"; } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/body/Part.java ================================================ package com.koushikdutta.async.http.body; import com.koushikdutta.async.DataSink; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.http.Headers; import com.koushikdutta.async.http.Multimap; import com.koushikdutta.async.http.NameValuePair; import java.io.File; import java.util.List; import java.util.Locale; public class Part { public static final String CONTENT_DISPOSITION = "Content-Disposition"; Headers mHeaders; Multimap mContentDisposition; public Part(Headers headers) { mHeaders = headers; mContentDisposition = Multimap.parseSemicolonDelimited(mHeaders.get(CONTENT_DISPOSITION)); } public String getName() { return mContentDisposition.getString("name"); } private long length = -1; public Part(String name, long length, List contentDisposition) { this.length = length; mHeaders = new Headers(); StringBuilder builder = new StringBuilder(String.format(Locale.ENGLISH, "form-data; name=\"%s\"", name)); if (contentDisposition != null) { for (NameValuePair pair: contentDisposition) { builder.append(String.format(Locale.ENGLISH, "; %s=\"%s\"", pair.getName(), pair.getValue())); } } mHeaders.set(CONTENT_DISPOSITION, builder.toString()); mContentDisposition = Multimap.parseSemicolonDelimited(mHeaders.get(CONTENT_DISPOSITION)); } public Headers getRawHeaders() { return mHeaders; } public String getContentType() { return mHeaders.get("Content-Type"); } public void setContentType(String contentType) { mHeaders.set("Content-Type", contentType); } public String getFilename() { String file = mContentDisposition.getString("filename"); if (file == null) return null; return new File(file).getName(); } public boolean isFile() { return mContentDisposition.containsKey("filename"); } public long length() { return length; } public void write(DataSink sink, CompletedCallback callback) { } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/body/StreamBody.java ================================================ package com.koushikdutta.async.http.body; import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.DataSink; import com.koushikdutta.async.Util; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.http.AsyncHttpRequest; import java.io.InputStream; public class StreamBody implements AsyncHttpRequestBody { InputStream stream; int length; String contentType = CONTENT_TYPE; /** * Construct an http body from a stream * @param stream * @param length Length of stream to read, or value < 0 to read to end */ public StreamBody(InputStream stream, int length) { this.stream = stream; this.length = length; } @Override public void write(AsyncHttpRequest request, DataSink sink, CompletedCallback completed) { Util.pump(stream, length < 0 ? Integer.MAX_VALUE : length, sink, completed); } @Override public void parse(DataEmitter emitter, CompletedCallback completed) { throw new AssertionError("not implemented"); } public static final String CONTENT_TYPE = "application/binary"; @Override public String getContentType() { return contentType; } public StreamBody setContentType(String contentType) { this.contentType = contentType; return this; } @Override public boolean readFullyOnRequest() { throw new AssertionError("not implemented"); } @Override public int length() { return length; } @Override public InputStream get() { return stream; } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/body/StreamPart.java ================================================ package com.koushikdutta.async.http.body; import java.io.IOException; import java.io.InputStream; import java.util.List; import com.koushikdutta.async.DataSink; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.http.NameValuePair; public abstract class StreamPart extends Part { public StreamPart(String name, long length, List contentDisposition) { super(name, length, contentDisposition); } @Override public void write(DataSink sink, CompletedCallback callback) { try { InputStream is = getInputStream(); com.koushikdutta.async.Util.pump(is, sink, callback); } catch (Exception e) { callback.onCompleted(e); } } protected abstract InputStream getInputStream() throws IOException; } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/body/StringBody.java ================================================ package com.koushikdutta.async.http.body; import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.DataSink; import com.koushikdutta.async.Util; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.future.FutureCallback; import com.koushikdutta.async.http.AsyncHttpRequest; import com.koushikdutta.async.parser.StringParser; public class StringBody implements AsyncHttpRequestBody { public StringBody() { } byte[] mBodyBytes; String string; public StringBody(String string) { this(); this.string = string; } @Override public void parse(DataEmitter emitter, final CompletedCallback completed) { new StringParser().parse(emitter).setCallback(new FutureCallback() { @Override public void onCompleted(Exception e, String result) { string = result; completed.onCompleted(e); } }); } public static final String CONTENT_TYPE = "text/plain"; @Override public void write(AsyncHttpRequest request, DataSink sink, final CompletedCallback completed) { if (mBodyBytes == null) mBodyBytes = string.getBytes(); Util.writeAll(sink, mBodyBytes, completed); } @Override public String getContentType() { return "text/plain"; } @Override public boolean readFullyOnRequest() { return true; } @Override public int length() { if (mBodyBytes == null) mBodyBytes = string.getBytes(); return mBodyBytes.length; } @Override public String toString() { return string; } @Override public String get() { return toString(); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/body/StringPart.java ================================================ package com.koushikdutta.async.http.body; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; public class StringPart extends StreamPart { String value; public StringPart(String name, String value) { super(name, value.getBytes().length, null); this.value = value; } @Override protected InputStream getInputStream() throws IOException { return new ByteArrayInputStream(value.getBytes()); } public String getValue() { return value; } @Override public String toString() { return value; } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/body/UrlEncodedFormBody.java ================================================ package com.koushikdutta.async.http.body; import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.DataSink; import com.koushikdutta.async.Util; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.DataCallback; import com.koushikdutta.async.http.AsyncHttpRequest; import com.koushikdutta.async.http.Multimap; import com.koushikdutta.async.http.NameValuePair; import com.koushikdutta.async.util.Charsets; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.List; public class UrlEncodedFormBody implements AsyncHttpRequestBody { private Multimap mParameters; private byte[] mBodyBytes; public UrlEncodedFormBody(Multimap parameters) { mParameters = parameters; } public UrlEncodedFormBody(List parameters) { mParameters = new Multimap(parameters); } private void buildData() { boolean first = true; StringBuilder b = new StringBuilder(); try { for (NameValuePair pair: mParameters) { if (pair.getValue() == null) continue; if (!first) b.append('&'); first = false; b.append(URLEncoder.encode(pair.getName(), "UTF-8")); b.append('='); b.append(URLEncoder.encode(pair.getValue(), "UTF-8")); } mBodyBytes = b.toString().getBytes("UTF-8"); } catch (UnsupportedEncodingException e) { throw new AssertionError(e); } } @Override public void write(AsyncHttpRequest request, final DataSink response, final CompletedCallback completed) { if (mBodyBytes == null) buildData(); Util.writeAll(response, mBodyBytes, completed); } public static final String CONTENT_TYPE = "application/x-www-form-urlencoded"; @Override public String getContentType() { return CONTENT_TYPE + "; charset=utf-8"; } @Override public void parse(DataEmitter emitter, final CompletedCallback completed) { final ByteBufferList data = new ByteBufferList(); emitter.setDataCallback(new DataCallback() { @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { bb.get(data); } }); emitter.setEndCallback(new CompletedCallback() { @Override public void onCompleted(Exception ex) { try { if (ex != null) throw ex; mParameters = Multimap.parseUrlEncoded(data.readString()); } catch (Exception e) { completed.onCompleted(e); return; } completed.onCompleted(null); } }); } public UrlEncodedFormBody() { } @Override public boolean readFullyOnRequest() { return true; } @Override public int length() { if (mBodyBytes == null) buildData(); return mBodyBytes.length; } @Override public Multimap get() { return mParameters; } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/cache/HeaderParser.java ================================================ /* * 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.cache; 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) { if (value == null) return; 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; } } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/cache/Objects.java ================================================ /* * 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.cache; 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(); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/cache/RawHeaders.java ================================================ package com.koushikdutta.async.http.cache; /* * 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 android.text.TextUtils; 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. * *

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. * *

This class trims whitespace from values. It never returns values with * leading or trailing whitespace. */ final class RawHeaders { private static final Comparator FIELD_NAME_COMPARATOR = new Comparator() { @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 namesAndValues = new ArrayList(20); private String statusLine; private int httpMinorVersion = 1; private int responseCode = -1; private String responseMessage; public RawHeaders() {} public RawHeaders(RawHeaders copyFrom) { copy(copyFrom); } public void copy(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 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 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> toMultimap() { Map> result = new TreeMap>(FIELD_NAME_COMPARATOR); for (int i = 0; i < namesAndValues.size(); i += 2) { String fieldName = namesAndValues.get(i); String value = namesAndValues.get(i + 1); List allValues = new ArrayList(); List 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> map) { RawHeaders result = new RawHeaders(); for (Entry> entry : map.entrySet()) { String fieldName = entry.getKey(); List values = entry.getValue(); if (fieldName != null) { result.addAll(fieldName, values); } else if (!values.isEmpty()) { result.setStatusLine(values.get(values.size() - 1)); } } return result; } public static RawHeaders parse(String payload) { String[] lines = payload.split("\n"); RawHeaders headers = new RawHeaders(); for (String line: lines) { line = line.trim(); if (TextUtils.isEmpty(line)) continue; if (headers.getStatusLine() == null) headers.setStatusLine(line); else headers.addLine(line); } return headers; } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/cache/RequestHeaders.java ================================================ /* * 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.cache; import android.net.Uri; import com.koushikdutta.async.http.HttpDate; import java.util.Date; import java.util.List; import java.util.Map; /** * Parsed HTTP request headers. */ 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"); } if (contentLength != -1) { 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> allCookieHeaders) { for (Map.Entry> entry : allCookieHeaders.entrySet()) { String key = entry.getKey(); if ("Cookie".equalsIgnoreCase(key) || "Cookie2".equalsIgnoreCase(key)) { headers.addAll(key, entry.getValue()); } } } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/cache/ResponseCacheMiddleware.java ================================================ package com.koushikdutta.async.http.cache; import android.net.Uri; import android.util.Base64; import com.koushikdutta.async.AsyncSSLSocket; import com.koushikdutta.async.AsyncServer; import com.koushikdutta.async.AsyncSocket; import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.FilteredDataEmitter; import com.koushikdutta.async.Util; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.WritableCallback; import com.koushikdutta.async.future.Cancellable; import com.koushikdutta.async.future.SimpleCancellable; import com.koushikdutta.async.http.AsyncHttpClient; import com.koushikdutta.async.http.AsyncHttpClientMiddleware; import com.koushikdutta.async.http.AsyncHttpGet; import com.koushikdutta.async.http.AsyncHttpRequest; import com.koushikdutta.async.http.Headers; import com.koushikdutta.async.http.SimpleMiddleware; import com.koushikdutta.async.util.Allocator; import com.koushikdutta.async.util.Charsets; import com.koushikdutta.async.util.FileCache; import com.koushikdutta.async.util.StreamUtility; import java.io.BufferedWriter; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Writer; import java.net.CacheResponse; import java.nio.ByteBuffer; import java.security.cert.Certificate; import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.List; import java.util.Locale; import java.util.Map; import javax.net.ssl.SSLEngine; public class ResponseCacheMiddleware extends SimpleMiddleware { public static final int ENTRY_METADATA = 0; public static final int ENTRY_BODY = 1; public static final int ENTRY_COUNT = 2; public static final String SERVED_FROM = "X-Served-From"; public static final String CONDITIONAL_CACHE = "conditional-cache"; public static final String CACHE = "cache"; private static final String LOGTAG = "AsyncHttpCache"; private boolean caching = true; private int writeSuccessCount; private int writeAbortCount; private FileCache cache; private AsyncServer server; private int conditionalCacheHitCount; private int cacheHitCount; private int networkCount; private int cacheStoreCount; private ResponseCacheMiddleware() { } public static ResponseCacheMiddleware addCache(AsyncHttpClient client, File cacheDir, long size) throws IOException { for (AsyncHttpClientMiddleware middleware: client.getMiddleware()) { if (middleware instanceof ResponseCacheMiddleware) throw new IOException("Response cache already added to http client"); } ResponseCacheMiddleware ret = new ResponseCacheMiddleware(); ret.server = client.getServer(); ret.cache = new FileCache(cacheDir, size, false); client.insertMiddleware(ret); return ret; } public FileCache getFileCache() { return cache; } public boolean getCaching() { return caching; } public void setCaching(boolean caching) { this.caching = caching; } public void removeFromCache(Uri uri) { String key = FileCache.toKeyString(uri); getFileCache().remove(key); } // step 1) see if we can serve request from the cache directly. // also see if this can be turned into a conditional cache request. @Override public Cancellable getSocket(final GetSocketData data) { RequestHeaders requestHeaders = new RequestHeaders(data.request.getUri(), RawHeaders.fromMultimap(data.request.getHeaders().getMultiMap())); data.state.put("request-headers", requestHeaders); if (cache == null || !caching || requestHeaders.isNoCache()) { networkCount++; return null; } String key = FileCache.toKeyString(data.request.getUri()); FileInputStream[] snapshot = null; long contentLength; Entry entry; try { snapshot = cache.get(key, ENTRY_COUNT); if (snapshot == null) { networkCount++; return null; } contentLength = snapshot[ENTRY_BODY].available(); entry = new Entry(snapshot[ENTRY_METADATA]); } catch (IOException e) { // Give up because the cache cannot be read. networkCount++; StreamUtility.closeQuietly(snapshot); return null; } // verify the entry matches if (!entry.matches(data.request.getUri(), data.request.getMethod(), data.request.getHeaders().getMultiMap())) { networkCount++; StreamUtility.closeQuietly(snapshot); return null; } EntryCacheResponse candidate = new EntryCacheResponse(entry, snapshot[ENTRY_BODY]); Map> responseHeadersMap; FileInputStream cachedResponseBody; try { responseHeadersMap = candidate.getHeaders(); cachedResponseBody = candidate.getBody(); } catch (Exception e) { networkCount++; StreamUtility.closeQuietly(snapshot); return null; } if (responseHeadersMap == null || cachedResponseBody == null) { networkCount++; StreamUtility.closeQuietly(snapshot); return null; } RawHeaders rawResponseHeaders = RawHeaders.fromMultimap(responseHeadersMap); ResponseHeaders cachedResponseHeaders = new ResponseHeaders(data.request.getUri(), rawResponseHeaders); rawResponseHeaders.set("Content-Length", String.valueOf(contentLength)); rawResponseHeaders.removeAll("Content-Encoding"); rawResponseHeaders.removeAll("Transfer-Encoding"); cachedResponseHeaders.setLocalTimestamps(System.currentTimeMillis(), System.currentTimeMillis()); long now = System.currentTimeMillis(); ResponseSource responseSource = cachedResponseHeaders.chooseResponseSource(now, requestHeaders); if (responseSource == ResponseSource.CACHE) { data.request.logi("Response retrieved from cache"); final CachedSocket socket = entry.isHttps() ? new CachedSSLSocket(candidate, contentLength) : new CachedSocket(candidate, contentLength); socket.pending.add(ByteBuffer.wrap(rawResponseHeaders.toHeaderString().getBytes())); server.post(new Runnable() { @Override public void run() { data.connectCallback.onConnectCompleted(null, socket); socket.sendCachedDataOnNetworkThread(); } }); cacheHitCount++; data.state.put("socket-owner", this); SimpleCancellable ret = new SimpleCancellable(); ret.setComplete(); return ret; } else if (responseSource == ResponseSource.CONDITIONAL_CACHE) { data.request.logi("Response may be served from conditional cache"); CacheData cacheData = new CacheData(); cacheData.snapshot = snapshot; cacheData.contentLength = contentLength; cacheData.cachedResponseHeaders = cachedResponseHeaders; cacheData.candidate = candidate; data.state.put("cache-data", cacheData); return null; } else { data.request.logd("Response can not be served from cache"); // NETWORK or other networkCount++; StreamUtility.closeQuietly(snapshot); return null; } } public int getConditionalCacheHitCount() { return conditionalCacheHitCount; } public int getCacheHitCount() { return cacheHitCount; } public int getNetworkCount() { return networkCount; } public int getCacheStoreCount() { return cacheStoreCount; } // step 2) if this is a conditional cache request, serve it from the cache if necessary // otherwise, see if it is cacheable @Override public void onBodyDecoder(OnBodyDecoderData data) { CachedSocket cached = com.koushikdutta.async.Util.getWrappedSocket(data.socket, CachedSocket.class); if (cached != null) { data.response.headers().set(SERVED_FROM, CACHE); return; } CacheData cacheData = data.state.get("cache-data"); RawHeaders rh = RawHeaders.fromMultimap(data.response.headers().getMultiMap()); rh.removeAll("Content-Length"); rh.setStatusLine(String.format(Locale.ENGLISH, "%s %s %s", data.response.protocol(), data.response.code(), data.response.message())); ResponseHeaders networkResponse = new ResponseHeaders(data.request.getUri(), rh); data.state.put("response-headers", networkResponse); if (cacheData != null) { if (cacheData.cachedResponseHeaders.validate(networkResponse)) { data.request.logi("Serving response from conditional cache"); ResponseHeaders combined = cacheData.cachedResponseHeaders.combine(networkResponse); data.response.headers(new Headers(combined.getHeaders().toMultimap())); data.response.code(combined.getHeaders().getResponseCode()); data.response.message(combined.getHeaders().getResponseMessage()); data.response.headers().set(SERVED_FROM, CONDITIONAL_CACHE); conditionalCacheHitCount++; CachedBodyEmitter bodySpewer = new CachedBodyEmitter(cacheData.candidate, cacheData.contentLength); bodySpewer.setDataEmitter(data.bodyEmitter); data.bodyEmitter = bodySpewer; bodySpewer.sendCachedData(); return; } // did not validate, so fall through and cache the response data.state.remove("cache-data"); StreamUtility.closeQuietly(cacheData.snapshot); } if (!caching) return; RequestHeaders requestHeaders = data.state.get("request-headers"); if (requestHeaders == null || !networkResponse.isCacheable(requestHeaders) || !data.request.getMethod().equals(AsyncHttpGet.METHOD)) { /* * Don't cache non-GET responses. We're technically allowed to cache * HEAD requests and some POST requests, but the complexity of doing * so is high and the benefit is low. */ networkCount++; data.request.logd("Response is not cacheable"); return; } String key = FileCache.toKeyString(data.request.getUri()); RawHeaders varyHeaders = requestHeaders.getHeaders().getAll(networkResponse.getVaryFields()); Entry entry = new Entry(data.request.getUri(), varyHeaders, data.request, networkResponse.getHeaders()); BodyCacher cacher = new BodyCacher(); EntryEditor editor = new EntryEditor(key); try { entry.writeTo(editor); // create the file editor.newOutputStream(ENTRY_BODY); } catch (Exception e) { // Log.e(LOGTAG, "error", e); editor.abort(); networkCount++; return; } cacher.editor = editor; cacher.setDataEmitter(data.bodyEmitter); data.bodyEmitter = cacher; data.state.put("body-cacher", cacher); data.request.logd("Caching response"); cacheStoreCount++; } // step 3: close up shop @Override public void onResponseComplete(OnResponseCompleteData data) { CacheData cacheData = data.state.get("cache-data"); if (cacheData != null && cacheData.snapshot != null) StreamUtility.closeQuietly(cacheData.snapshot); CachedSocket cachedSocket = Util.getWrappedSocket(data.socket, CachedSocket.class); if (cachedSocket != null) StreamUtility.closeQuietly((cachedSocket.cacheResponse).getBody()); BodyCacher cacher = data.state.get("body-cacher"); if (cacher != null) { if (data.exception != null) cacher.abort(); else cacher.commit(); } } public void clear() { if (cache != null) { cache.clear(); } } public static class CacheData { FileInputStream[] snapshot; EntryCacheResponse candidate; long contentLength; ResponseHeaders cachedResponseHeaders; } private static class BodyCacher extends FilteredDataEmitter { EntryEditor editor; ByteBufferList cached; @Override protected void report(Exception e) { super.report(e); if (e != null) abort(); } @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { if (cached != null) { super.onDataAvailable(emitter, cached); // couldn't emit it all, so just wait for another day... if (cached.remaining() > 0) return; cached = null; } // write to cache... any data not consumed needs to be retained for the next callback ByteBufferList copy = new ByteBufferList(); try { if (editor != null) { OutputStream outputStream = editor.newOutputStream(ENTRY_BODY); if (outputStream != null) { while (!bb.isEmpty()) { ByteBuffer b = bb.remove(); try { ByteBufferList.writeOutputStream(outputStream, b); } finally { copy.add(b); } } } else { abort(); } } } catch (Exception e) { abort(); } finally { bb.get(copy); copy.get(bb); } super.onDataAvailable(emitter, bb); if (editor != null && bb.remaining() > 0) { cached = new ByteBufferList(); bb.get(cached); } } @Override public void close() { abort(); super.close(); } public void abort() { if (editor != null) { editor.abort(); editor = null; } } public void commit() { if (editor != null) { editor.commit(); editor = null; } } } private static class CachedBodyEmitter extends FilteredDataEmitter { EntryCacheResponse cacheResponse; ByteBufferList pending = new ByteBufferList(); private boolean paused; private Allocator allocator = new Allocator(); boolean allowEnd; public CachedBodyEmitter(EntryCacheResponse cacheResponse, long contentLength) { this.cacheResponse = cacheResponse; allocator.setCurrentAlloc((int)contentLength); } Runnable sendCachedDataRunnable = new Runnable() { @Override public void run() { sendCachedDataOnNetworkThread(); } }; void sendCachedDataOnNetworkThread() { if (pending.remaining() > 0) { super.onDataAvailable(CachedBodyEmitter.this, pending); if (pending.remaining() > 0) return; } // fill pending try { ByteBuffer buffer = allocator.allocate(); FileInputStream din = cacheResponse.getBody(); int read = din.read(buffer.array(), buffer.arrayOffset(), buffer.capacity()); if (read == -1) { ByteBufferList.reclaim(buffer); allowEnd = true; report(null); return; } allocator.track(read); buffer.limit(read); pending.add(buffer); } catch (IOException e) { allowEnd = true; report(e); return; } super.onDataAvailable(this, pending); if (pending.remaining() > 0) return; // this limits max throughput to 256k (aka max alloc) * 100 per second... // roughly 25MB/s getServer().postDelayed(sendCachedDataRunnable, 10); } void sendCachedData() { getServer().post(sendCachedDataRunnable); } @Override public void resume() { paused = false; sendCachedData(); } @Override public boolean isPaused() { return paused; } @Override public void close() { if (getServer().getAffinity() != Thread.currentThread()) { getServer().post(new Runnable() { @Override public void run() { close(); } }); return; } pending.recycle(); StreamUtility.closeQuietly(cacheResponse.getBody()); super.close(); } @Override protected void report(Exception e) { // a 304 response will immediate call report/end since there is no body. // prevent this from happening by waiting for the actual body to be spit out. if (!allowEnd) return; StreamUtility.closeQuietly(cacheResponse.getBody()); super.report(e); } } private static final class Entry { private final String uri; private final RawHeaders varyHeaders; private final String requestMethod; private final RawHeaders responseHeaders; private final String cipherSuite; private final Certificate[] peerCertificates; private final Certificate[] localCertificates; /* * Reads an entry from an input stream. A typical entry looks like this: * http://google.com/foo * GET * 2 * Accept-Language: fr-CA * Accept-Charset: UTF-8 * HTTP/1.1 200 OK * 3 * Content-Type: image/png * Content-Length: 100 * Cache-Control: max-age=600 * * A typical HTTPS file looks like this: * https://google.com/foo * GET * 2 * Accept-Language: fr-CA * Accept-Charset: UTF-8 * HTTP/1.1 200 OK * 3 * Content-Type: image/png * Content-Length: 100 * Cache-Control: max-age=600 * * AES_256_WITH_MD5 * 2 * base64-encoded peerCertificate[0] * base64-encoded peerCertificate[1] * -1 * * The file is newline separated. The first two lines are the URL and * the request method. Next is the number of HTTP Vary request header * lines, followed by those lines. * * Next is the response status line, followed by the number of HTTP * response header lines, followed by those lines. * * HTTPS responses also contain SSL session information. This begins * with a blank line, and then a line containing the cipher suite. Next * is the length of the peer certificate chain. These certificates are * base64-encoded and appear each on their own line. The next line * contains the length of the local certificate chain. These * certificates are also base64-encoded and appear each on their own * line. A length of -1 is used to encode a null array. */ public Entry(InputStream in) throws IOException { StrictLineReader reader = null; try { reader = new StrictLineReader(in, Charsets.US_ASCII); uri = reader.readLine(); requestMethod = reader.readLine(); varyHeaders = new RawHeaders(); int varyRequestHeaderLineCount = reader.readInt(); for (int i = 0; i < varyRequestHeaderLineCount; i++) { varyHeaders.addLine(reader.readLine()); } responseHeaders = new RawHeaders(); responseHeaders.setStatusLine(reader.readLine()); int responseHeaderLineCount = reader.readInt(); for (int i = 0; i < responseHeaderLineCount; i++) { responseHeaders.addLine(reader.readLine()); } // if (isHttps()) { // String blank = reader.readLine(); // if (blank.length() != 0) { // throw new IOException("expected \"\" but was \"" + blank + "\""); // } // cipherSuite = reader.readLine(); // peerCertificates = readCertArray(reader); // localCertificates = readCertArray(reader); // } else { cipherSuite = null; peerCertificates = null; localCertificates = null; // } } finally { StreamUtility.closeQuietly(reader, in); } } public Entry(Uri uri, RawHeaders varyHeaders, AsyncHttpRequest request, RawHeaders responseHeaders) { this.uri = uri.toString(); this.varyHeaders = varyHeaders; this.requestMethod = request.getMethod(); this.responseHeaders = responseHeaders; // if (isHttps()) { // HttpsURLConnection httpsConnection = (HttpsURLConnection) httpConnection; // cipherSuite = httpsConnection.getCipherSuite(); // Certificate[] peerCertificatesNonFinal = null; // try { // peerCertificatesNonFinal = httpsConnection.getServerCertificates(); // } catch (SSLPeerUnverifiedException ignored) { // } // peerCertificates = peerCertificatesNonFinal; // localCertificates = httpsConnection.getLocalCertificates(); // } else { cipherSuite = null; peerCertificates = null; localCertificates = null; // } } public void writeTo(EntryEditor editor) throws IOException { OutputStream out = editor.newOutputStream(ENTRY_METADATA); Writer writer = new BufferedWriter(new OutputStreamWriter(out, Charsets.UTF_8)); writer.write(uri + '\n'); writer.write(requestMethod + '\n'); writer.write(Integer.toString(varyHeaders.length()) + '\n'); for (int i = 0; i < varyHeaders.length(); i++) { writer.write(varyHeaders.getFieldName(i) + ": " + varyHeaders.getValue(i) + '\n'); } writer.write(responseHeaders.getStatusLine() + '\n'); writer.write(Integer.toString(responseHeaders.length()) + '\n'); for (int i = 0; i < responseHeaders.length(); i++) { writer.write(responseHeaders.getFieldName(i) + ": " + responseHeaders.getValue(i) + '\n'); } if (isHttps()) { writer.write('\n'); writer.write(cipherSuite + '\n'); writeCertArray(writer, peerCertificates); writeCertArray(writer, localCertificates); } writer.close(); } private boolean isHttps() { return uri.startsWith("https://"); } private Certificate[] readCertArray(StrictLineReader reader) throws IOException { int length = reader.readInt(); if (length == -1) { return null; } try { CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); Certificate[] result = new Certificate[length]; for (int i = 0; i < result.length; i++) { String line = reader.readLine(); byte[] bytes = Base64.decode(line, Base64.DEFAULT); result[i] = certificateFactory.generateCertificate( new ByteArrayInputStream(bytes)); } return result; } catch (CertificateException e) { throw new IOException(e.getMessage()); } } private void writeCertArray(Writer writer, Certificate[] certificates) throws IOException { if (certificates == null) { writer.write("-1\n"); return; } try { writer.write(Integer.toString(certificates.length) + '\n'); for (Certificate certificate : certificates) { byte[] bytes = certificate.getEncoded(); String line = Base64.encodeToString(bytes, Base64.DEFAULT); writer.write(line + '\n'); } } catch (CertificateEncodingException e) { throw new IOException(e.getMessage()); } } public boolean matches(Uri uri, String requestMethod, Map> requestHeaders) { return this.uri.equals(uri.toString()) && this.requestMethod.equals(requestMethod) && new ResponseHeaders(uri, responseHeaders) .varyMatches(varyHeaders.toMultimap(), requestHeaders); } } static class EntryCacheResponse extends CacheResponse { private final Entry entry; private final FileInputStream snapshot; public EntryCacheResponse(Entry entry, FileInputStream snapshot) { this.entry = entry; this.snapshot = snapshot; } @Override public Map> getHeaders() { return entry.responseHeaders.toMultimap(); } @Override public FileInputStream getBody() { return snapshot; } } private class CachedSSLSocket extends CachedSocket implements AsyncSSLSocket { public CachedSSLSocket(EntryCacheResponse cacheResponse, long contentLength) { super(cacheResponse, contentLength); } @Override public SSLEngine getSSLEngine() { return null; } @Override public X509Certificate[] getPeerCertificates() { return null; } } private class CachedSocket extends CachedBodyEmitter implements AsyncSocket { boolean closed; boolean open; CompletedCallback closedCallback; public CachedSocket(EntryCacheResponse cacheResponse, long contentLength) { super(cacheResponse, contentLength); allowEnd = true; } @Override public void end() { } @Override protected void report(Exception e) { super.report(e); if (closed) return; closed = true; if (closedCallback != null) closedCallback.onCompleted(e); } @Override public void write(ByteBufferList bb) { // it's gonna write headers and stuff... whatever bb.recycle(); } @Override public WritableCallback getWriteableCallback() { return null; } @Override public void setWriteableCallback(WritableCallback handler) { } @Override public boolean isOpen() { return open; } @Override public void close() { open = false; } @Override public CompletedCallback getClosedCallback() { return closedCallback; } @Override public void setClosedCallback(CompletedCallback handler) { closedCallback = handler; } @Override public AsyncServer getServer() { return server; } } class EntryEditor { String key; File[] temps; FileOutputStream[] outs; boolean done; public EntryEditor(String key) { this.key = key; temps = cache.getTempFiles(ENTRY_COUNT); outs = new FileOutputStream[ENTRY_COUNT]; } void commit() { StreamUtility.closeQuietly(outs); if (done) return; cache.commitTempFiles(key, temps); writeSuccessCount++; done = true; } FileOutputStream newOutputStream(int index) throws IOException { if (outs[index] == null) outs[index] = new FileOutputStream(temps[index]); return outs[index]; } void abort() { StreamUtility.closeQuietly(outs); FileCache.removeFiles(temps); if (done) return; writeAbortCount++; done = true; } } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/cache/ResponseHeaders.java ================================================ /* * 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.cache; import android.net.Uri; import com.koushikdutta.async.http.HttpDate; import java.net.HttpURLConnection; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.TimeUnit; /** * Parsed HTTP response headers. */ 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 varyFields = Collections.emptySet(); private String contentEncoding; private String transferEncoding; private long 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.CASE_INSENSITIVE_ORDER); } for (String varyField : value.split(",")) { varyFields.add(varyField.trim().toLowerCase(Locale.US)); } } else if ("Content-Encoding".equalsIgnoreCase(fieldName)) { contentEncoding = value; } else if ("Transfer-Encoding".equalsIgnoreCase(fieldName)) { transferEncoding = value; } else if ("Content-Length".equalsIgnoreCase(fieldName)) { try { contentLength = Long.parseLong(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 getVaryFields() { return varyFields; } public String getContentEncoding() { return contentEncoding; } public long 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.getEncodedQuery() == 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> cachedRequest, Map> 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 (etag != null) { request.setIfNoneMatch(etag); } else if (lastModified != null) { request.setIfModifiedSince(lastModified); } else if (servedDate != null) { request.setIfModifiedSince(servedDate); } 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"); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/cache/ResponseSource.java ================================================ /* * 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.cache; 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; } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/cache/StrictLineReader.java ================================================ /* * Copyright (C) 2012 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.cache; import com.koushikdutta.async.util.Charsets; import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.EOFException; import java.io.InputStream; import java.io.IOException; import java.nio.charset.Charset; /** * Buffers input from an {@link InputStream} for reading lines. * * This class is used for buffered reading of lines. For purposes of this class, a line ends with * "\n" or "\r\n". End of input is reported by throwing {@code EOFException}. Unterminated line at * end of input is invalid and will be ignored, the caller may use {@code hasUnterminatedLine()} * to detect it after catching the {@code EOFException}. * * This class is intended for reading input that strictly consists of lines, such as line-based * cache entries or cache journal. Unlike the {@link BufferedReader} which in conjunction with * {@link InputStreamReader} provides similar functionality, this class uses different * end-of-input reporting and a more restrictive definition of a line. * * This class supports only charsets that encode '\r' and '\n' as a single byte with value 13 * and 10, respectively, and the representation of no other character contains these values. * We currently check in constructor that the charset is one of US-ASCII, UTF-8 and ISO-8859-1. * The default charset is US_ASCII. */ class StrictLineReader implements Closeable { private static final byte CR = (byte)'\r'; private static final byte LF = (byte)'\n'; private final InputStream in; /* * Buffered data is stored in {@code buf}. As long as no exception occurs, 0 <= pos <= end * and the data in the range [pos, end) is buffered for reading. At end of input, if there is * an unterminated line, we set end == -1, otherwise end == pos. If the underlying * {@code InputStream} throws an {@code IOException}, end may remain as either pos or -1. */ private byte[] buf; private int pos; private int end; /** * Constructs a new {@code StrictLineReader} with the default capacity and charset. * * @param in the {@code InputStream} to read data from. * @throws NullPointerException if {@code in} is null. */ public StrictLineReader(InputStream in) { this(in, 8192); } /** * Constructs a new {@code LineReader} with the specified capacity and the default charset. * * @param in the {@code InputStream} to read data from. * @param capacity the capacity of the buffer. * @throws NullPointerException if {@code in} is null. * @throws IllegalArgumentException for negative or zero {@code capacity}. */ public StrictLineReader(InputStream in, int capacity) { this(in, capacity, Charsets.US_ASCII); } /** * Constructs a new {@code LineReader} with the specified charset and the default capacity. * * @param in the {@code InputStream} to read data from. * @param charset the charset used to decode data. * Only US-ASCII, UTF-8 and ISO-8859-1 is supported. * @throws NullPointerException if {@code in} or {@code charset} is null. * @throws IllegalArgumentException if the specified charset is not supported. */ public StrictLineReader(InputStream in, Charset charset) { this(in, 8192, charset); } /** * Constructs a new {@code LineReader} with the specified capacity and charset. * * @param in the {@code InputStream} to read data from. * @param capacity the capacity of the buffer. * @param charset the charset used to decode data. * Only US-ASCII, UTF-8 and ISO-8859-1 is supported. * @throws NullPointerException if {@code in} or {@code charset} is null. * @throws IllegalArgumentException if {@code capacity} is negative or zero * or the specified charset is not supported. */ public StrictLineReader(InputStream in, int capacity, Charset charset) { if (in == null) { throw new NullPointerException("in == null"); } else if (charset == null) { throw new NullPointerException("charset == null"); } if (capacity < 0) { throw new IllegalArgumentException("capacity <= 0"); } if (!(charset.equals(Charsets.US_ASCII) || charset.equals(Charsets.UTF_8))) { throw new IllegalArgumentException("Unsupported encoding"); } this.in = in; buf = new byte[capacity]; } /** * Closes the reader by closing the underlying {@code InputStream} and * marking this reader as closed. * * @throws IOException for errors when closing the underlying {@code InputStream}. */ @Override public void close() throws IOException { synchronized (in) { if (buf != null) { buf = null; in.close(); } } } /** * Reads the next line. A line ends with {@code "\n"} or {@code "\r\n"}, * this end of line marker is not included in the result. * * @return the next line from the input. * @throws IOException for underlying {@code InputStream} errors. * @throws EOFException for the end of source stream. */ public String readLine() throws IOException { synchronized (in) { if (buf == null) { throw new IOException("LineReader is closed"); } // Read more data if we are at the end of the buffered data. // Though it's an error to read after an exception, we will let {@code fillBuf()} // throw again if that happens; thus we need to handle end == -1 as well as end == pos. if (pos >= end) { fillBuf(); } // Try to find LF in the buffered data and return the line if successful. for (int i = pos; i != end; ++i) { if (buf[i] == LF) { int lineEnd = (i != pos && buf[i - 1] == CR) ? i - 1 : i; String res = new String(buf, pos, lineEnd - pos); pos = i + 1; return res; } } // Let's anticipate up to 80 characters on top of those already read. ByteArrayOutputStream out = new ByteArrayOutputStream(end - pos + 80) { @Override public String toString() { int length = (count > 0 && buf[count - 1] == CR) ? count - 1 : count; return new String(buf, 0, length); } }; while (true) { out.write(buf, pos, end - pos); // Mark unterminated line in case fillBuf throws EOFException or IOException. end = -1; fillBuf(); // Try to find LF in the buffered data and return the line if successful. for (int i = pos; i != end; ++i) { if (buf[i] == LF) { if (i != pos) { out.write(buf, pos, i - pos); } pos = i + 1; return out.toString(); } } } } } /** * Read an {@code int} from a line containing its decimal representation. * * @return the value of the {@code int} from the next line. * @throws IOException for underlying {@code InputStream} errors or conversion error. * @throws EOFException for the end of source stream. */ public int readInt() throws IOException { String intString = readLine(); try { return Integer.parseInt(intString); } catch (NumberFormatException e) { throw new IOException("expected an int but was \"" + intString + "\""); } } /** * Check whether there was an unterminated line at end of input after the line reader reported * end-of-input with EOFException. The value is meaningless in any other situation. * * @return true if there was an unterminated line at end of input. */ public boolean hasUnterminatedLine() { return end == -1; } /** * Reads new input data into the buffer. Call only with pos == end or end == -1, * depending on the desired outcome if the function throws. * * @throws IOException for underlying {@code InputStream} errors. * @throws EOFException for the end of source stream. */ private void fillBuf() throws IOException { int result = in.read(buf, 0, buf.length); if (result == -1) { throw new EOFException(); } pos = 0; end = result; } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/callback/HttpConnectCallback.java ================================================ package com.koushikdutta.async.http.callback; import com.koushikdutta.async.http.AsyncHttpResponse; public interface HttpConnectCallback { public void onConnectCompleted(Exception ex, AsyncHttpResponse response); } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/callback/RequestCallback.java ================================================ package com.koushikdutta.async.http.callback; import com.koushikdutta.async.callback.ResultCallback; import com.koushikdutta.async.http.AsyncHttpResponse; public interface RequestCallback extends ResultCallback { public void onConnect(AsyncHttpResponse response); public void onProgress(AsyncHttpResponse response, long downloaded, long total); } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/filter/ChunkedDataException.java ================================================ package com.koushikdutta.async.http.filter; public class ChunkedDataException extends Exception { public ChunkedDataException(String message) { super(message); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/filter/ChunkedInputFilter.java ================================================ package com.koushikdutta.async.http.filter; import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.FilteredDataEmitter; import com.koushikdutta.async.Util; public class ChunkedInputFilter extends FilteredDataEmitter { private int mChunkLength = 0; private int mChunkLengthRemaining = 0; private State mState = State.CHUNK_LEN; private enum State { CHUNK_LEN, CHUNK_LEN_CR, CHUNK_LEN_CRLF, CHUNK, CHUNK_CR, CHUNK_CRLF, COMPLETE, ERROR, } private boolean checkByte(char b, char value) { if (b != value) { mState = State.ERROR; report(new ChunkedDataException(value + " was expected, 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 protected void report(Exception e) { if (e == null && mState != State.COMPLETE) e = new ChunkedDataException("chunked input ended before final chunk"); super.report(e); } ByteBufferList pending = new ByteBufferList(); @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { if (mState == State.ERROR) { bb.recycle(); return; } 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 { report(new ChunkedDataException("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; bb.get(pending, reading); Util.emitAllData(this, pending); 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; report(null); } mChunkLength = 0; break; case COMPLETE: // Exception fail = new Exception("Continued receiving data after chunk complete"); // report(fail); return; } } } catch (Exception ex) { report(ex); } } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/filter/ChunkedOutputFilter.java ================================================ package com.koushikdutta.async.http.filter; import java.nio.ByteBuffer; import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.DataSink; import com.koushikdutta.async.FilteredDataSink; public class ChunkedOutputFilter extends FilteredDataSink { public ChunkedOutputFilter(DataSink sink) { super(sink); } @Override public ByteBufferList filter(ByteBufferList bb) { String chunkLen = Integer.toString(bb.remaining(), 16) + "\r\n"; bb.addFirst(ByteBuffer.wrap(chunkLen.getBytes())); bb.add(ByteBuffer.wrap("\r\n".getBytes())); return bb; } @Override public void end() { setMaxBuffer(Integer.MAX_VALUE); ByteBufferList fin = new ByteBufferList(); write(fin); setMaxBuffer(0); // do NOT call through to super.end, as chunking is a framing protocol. // we don't want to close the underlying transport. } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/filter/ContentLengthFilter.java ================================================ package com.koushikdutta.async.http.filter; import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.FilteredDataEmitter; public class ContentLengthFilter extends FilteredDataEmitter { public ContentLengthFilter(long contentLength) { this.contentLength = contentLength; } @Override protected void report(Exception e) { if (e == null && totalRead != contentLength) e = new PrematureDataEndException("End of data reached before content length was read: " + totalRead + "/" + contentLength + " Paused: " + isPaused()); super.report(e); } long contentLength; long totalRead; ByteBufferList transformed = new ByteBufferList(); @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { int remaining = bb.remaining(); long toRead = Math.min(contentLength - totalRead, remaining); bb.get(transformed, (int)toRead); int beforeRead = transformed.remaining(); super.onDataAvailable(emitter, transformed); totalRead += (beforeRead - transformed.remaining()); transformed.get(bb); if (totalRead == contentLength) report(null); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/filter/DataRemainingException.java ================================================ package com.koushikdutta.async.http.filter; public class DataRemainingException extends Exception { public DataRemainingException(String message, Exception cause) { super(message, cause); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/filter/GZIPInputFilter.java ================================================ package com.koushikdutta.async.http.filter; import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.PushParser; import com.koushikdutta.async.PushParser.ParseCallback; import com.koushikdutta.async.callback.DataCallback; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Locale; import java.util.zip.CRC32; import java.util.zip.GZIPInputStream; import java.util.zip.Inflater; public class GZIPInputFilter extends InflaterInputFilter { 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)); } } 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 GZIPInputFilter() { super(new Inflater(true)); } boolean mNeedsHeader = true; protected CRC32 crc = new CRC32(); public static int unsignedToBytes(byte b) { return b & 0xFF; } @Override @SuppressWarnings("unused") public void onDataAvailable(final DataEmitter emitter, ByteBufferList bb) { if (mNeedsHeader) { final PushParser parser = new PushParser(emitter); parser.readByteArray(10, new ParseCallback() { int flags; boolean hcrc; public void parsed(byte[] header) { short magic = peekShort(header, 0, ByteOrder.LITTLE_ENDIAN); if (magic != (short) GZIPInputStream.GZIP_MAGIC) { report(new IOException(String.format(Locale.ENGLISH, "unknown format (magic number %x)", magic))); emitter.setDataCallback(new NullDataCallback()); return; } flags = header[3]; hcrc = (flags & FHCRC) != 0; if (hcrc) { crc.update(header, 0, header.length); } if ((flags & FEXTRA) != 0) { parser.readByteArray(2, new ParseCallback() { public void parsed(byte[] header) { if (hcrc) { crc.update(header, 0, 2); } int length = peekShort(header, 0, ByteOrder.LITTLE_ENDIAN) & 0xffff; parser.readByteArray(length, new ParseCallback() { public void parsed(byte[] buf) { if (hcrc) { crc.update(buf, 0, buf.length); } next(); } }); } }); } else { next(); } } private 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()); ByteBufferList.reclaim(b); } } bb.recycle(); done(); } }; if ((flags & FNAME) != 0) { parser.until((byte) 0, summer); return; } if ((flags & FCOMMENT) != 0) { parser.until((byte) 0, summer); return; } done(); } private void done() { if (hcrc) { parser.readByteArray(2, new ParseCallback() { public void parsed(byte[] header) { short crc16 = peekShort(header, 0, ByteOrder.LITTLE_ENDIAN); if ((short) crc.getValue() != crc16) { report(new IOException("CRC mismatch")); return; } crc.reset(); mNeedsHeader = false; setDataEmitter(emitter); // emitter.setDataCallback(GZIPInputFilter.this); } }); } else { mNeedsHeader = false; setDataEmitter(emitter); } } }); } else { super.onDataAvailable(emitter, bb); } } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/filter/InflaterInputFilter.java ================================================ package com.koushikdutta.async.http.filter; import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.FilteredDataEmitter; import com.koushikdutta.async.Util; import java.io.IOException; import java.nio.ByteBuffer; import java.util.zip.Inflater; public class InflaterInputFilter extends FilteredDataEmitter { private Inflater mInflater; @Override protected void report(Exception e) { mInflater.end(); if (e != null && mInflater.getRemaining() > 0) { e = new DataRemainingException("data still remaining in inflater", e); } super.report(e); } ByteBufferList transformed = new ByteBufferList(); @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { try { ByteBuffer output = ByteBufferList.obtain(bb.remaining() * 2); 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()); output.position(output.position() + inflated); if (!output.hasRemaining()) { output.flip(); transformed.add(output); int newSize = output.capacity() * 2; output = ByteBufferList.obtain(newSize); } } while (!mInflater.needsInput() && !mInflater.finished()); } ByteBufferList.reclaim(b); } output.flip(); transformed.add(output); Util.emitAllData(this, transformed); } catch (Exception ex) { report(ex); } } public InflaterInputFilter() { this(new Inflater()); } public InflaterInputFilter(Inflater inflater) { mInflater = inflater; } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/filter/PrematureDataEndException.java ================================================ package com.koushikdutta.async.http.filter; public class PrematureDataEndException extends Exception { public PrematureDataEndException(String message) { super(message); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpRequestBodyProvider.java ================================================ package com.koushikdutta.async.http.server; import com.koushikdutta.async.http.Headers; import com.koushikdutta.async.http.body.AsyncHttpRequestBody; public interface AsyncHttpRequestBodyProvider { AsyncHttpRequestBody getBody(Headers headers); } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServer.java ================================================ package com.koushikdutta.async.http.server; import android.annotation.TargetApi; import android.os.Build; import android.util.Log; import com.koushikdutta.async.AsyncSSLSocket; import com.koushikdutta.async.AsyncSSLSocketWrapper; import com.koushikdutta.async.AsyncServer; import com.koushikdutta.async.AsyncServerSocket; import com.koushikdutta.async.AsyncSocket; import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.Util; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.ListenCallback; import com.koushikdutta.async.callback.ValueCallback; import com.koushikdutta.async.http.Headers; import com.koushikdutta.async.http.HttpUtil; import com.koushikdutta.async.http.Multimap; import com.koushikdutta.async.http.WebSocket; import com.koushikdutta.async.http.body.AsyncHttpRequestBody; import java.net.URLDecoder; import java.util.ArrayList; import java.util.Hashtable; import javax.net.ssl.SSLContext; @TargetApi(Build.VERSION_CODES.ECLAIR) public class AsyncHttpServer extends AsyncHttpServerRouter { ArrayList mListeners = new ArrayList(); public void stop() { if (mListeners != null) { for (AsyncServerSocket listener: mListeners) { listener.stop(); } } } protected boolean onRequest(AsyncHttpServerRequest request, AsyncHttpServerResponse response) { return false; } protected void onResponseCompleted(AsyncHttpServerRequest request, AsyncHttpServerResponse response) { } protected void onRequest(HttpServerRequestCallback callback, AsyncHttpServerRequest request, AsyncHttpServerResponse response) { if (callback != null) { try { callback.onRequest(request, response); } catch (Exception e) { Log.e("AsyncHttpServer", "request callback raised uncaught exception. Catching versus crashing process", e); response.code(500); response.end(); } } } protected boolean isKeepAlive(AsyncHttpServerRequest request, AsyncHttpServerResponse response) { return HttpUtil.isKeepAlive(response.getHttpVersion(), request.getHeaders()); } protected AsyncHttpRequestBody onUnknownBody(Headers headers) { return new UnknownRequestBody(headers.get("Content-Type")); } protected boolean isSwitchingProtocols(AsyncHttpServerResponse res) { return res.code() == 101; } ListenCallback mListenCallback = new ListenCallback() { @Override public void onAccepted(final AsyncSocket socket) { final AsyncHttpServerRequestImpl req = new AsyncHttpServerRequestImpl() { AsyncHttpServerRequestImpl self = this; HttpServerRequestCallback requestCallback; String fullPath; String path; boolean responseComplete; boolean requestComplete; AsyncHttpServerResponseImpl res; boolean hasContinued; boolean handled; final Runnable onFinally = new Runnable() { @Override public void run() { Log.i("HTTP", "Done"); } }; final ValueCallback onException = new ValueCallback() { @Override public void onResult(Exception value) { Log.e("HTTP", "exception", value); } }; void onRequest() { AsyncHttpServer.this.onRequest(requestCallback, this, res); } @Override protected AsyncHttpRequestBody onBody(Headers headers) { String statusLine = getStatusLine(); String[] parts = statusLine.split(" "); fullPath = parts[1]; path = URLDecoder.decode(fullPath.split("\\?")[0]); method = parts[0]; RouteMatch route = route(method, path); if (route == null) return null; matcher = route.matcher; requestCallback = route.callback; if (route.bodyCallback == null) return null; return route.bodyCallback.getBody(headers); } @Override protected AsyncHttpRequestBody onUnknownBody(Headers headers) { return AsyncHttpServer.this.onUnknownBody(headers); } @Override protected void onHeadersReceived() { Headers headers = getHeaders(); // should the negotiation of 100 continue be here, or in the request impl? // probably here, so AsyncResponse can negotiate a 100 continue. if (!hasContinued && "100-continue".equals(headers.get("Expect"))) { pause(); // System.out.println("continuing..."); Util.writeAll(mSocket, "HTTP/1.1 100 Continue\r\n\r\n".getBytes(), new CompletedCallback() { @Override public void onCompleted(Exception ex) { resume(); if (ex != null) { report(ex); return; } hasContinued = true; onHeadersReceived(); } }); return; } // System.out.println(headers.toHeaderString()); res = new AsyncHttpServerResponseImpl(socket, this) { @Override protected void report(Exception e) { super.report(e); if (e != null) { socket.setDataCallback(new NullDataCallback()); socket.setEndCallback(new NullCompletedCallback()); socket.close(); } } @Override protected void onEnd() { responseComplete = true; super.onEnd(); mSocket.setEndCallback(null); onResponseCompleted(getRequest(), res); // reuse the socket for a subsequent request. handleOnCompleted(); } }; handled = AsyncHttpServer.this.onRequest(this, res); if (handled) return; if (requestCallback == null) { res.code(404); res.end(); return; } if (!getBody().readFullyOnRequest() || requestComplete) onRequest(); } @Override public void onCompleted(Exception e) { if (isSwitchingProtocols(res)) return; requestComplete = true; super.onCompleted(e); // no http pipelining, gc trashing if the socket dies // while the request is being sent and is paused or something mSocket.setDataCallback(new NullDataCallback() { @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { super.onDataAvailable(emitter, bb); mSocket.close(); } }); if (e != null) { mSocket.close(); return; } handleOnCompleted(); if (getBody().readFullyOnRequest() && !handled) { onRequest(); } } private void handleOnCompleted() { // response may complete before request. the request may have a body, and // the response may be sent before it is fully sent. // if the protocol was switched off http, abandon the socket, // otherwise attempt to recycle it. if (requestComplete && responseComplete && !isSwitchingProtocols(res)) { if (isKeepAlive(self, res)) { onAccepted(socket); } else { socket.close(); } } } @Override public String getPath() { return path; } @Override public Multimap getQuery() { String[] parts = fullPath.split("\\?", 2); if (parts.length < 2) return new Multimap(); return Multimap.parseQuery(parts[1]); } @Override public String getUrl() { return fullPath; } }; req.setSocket(socket); socket.resume(); } @Override public void onCompleted(Exception error) { report(error); } @Override public void onListening(AsyncServerSocket socket) { mListeners.add(socket); } }; public AsyncServerSocket listen(AsyncServer server, int port) { return server.listen(null, port, mListenCallback); } private void report(Exception ex) { if (mCompletedCallback != null) mCompletedCallback.onCompleted(ex); } public AsyncServerSocket listen(int port) { return listen(AsyncServer.getDefault(), port); } public void listenSecure(final int port, final SSLContext sslContext) { AsyncServer.getDefault().listen(null, port, new ListenCallback() { @Override public void onAccepted(AsyncSocket socket) { AsyncSSLSocketWrapper.handshake(socket, null, port, sslContext.createSSLEngine(), null, null, false, new AsyncSSLSocketWrapper.HandshakeCallback() { @Override public void onHandshakeCompleted(Exception e, AsyncSSLSocket socket) { if (socket != null) mListenCallback.onAccepted(socket); } }); } @Override public void onListening(AsyncServerSocket socket) { mListenCallback.onListening(socket); } @Override public void onCompleted(Exception ex) { mListenCallback.onCompleted(ex); } }); } public ListenCallback getListenCallback() { return mListenCallback; } CompletedCallback mCompletedCallback; public void setErrorCallback(CompletedCallback callback) { mCompletedCallback = callback; } public CompletedCallback getErrorCallback() { return mCompletedCallback; } private static Hashtable mCodes = new Hashtable(); static { mCodes.put(200, "OK"); mCodes.put(202, "Accepted"); mCodes.put(206, "Partial Content"); mCodes.put(101, "Switching Protocols"); mCodes.put(301, "Moved Permanently"); mCodes.put(302, "Found"); mCodes.put(304, "Not Modified"); mCodes.put(400, "Bad Request"); mCodes.put(401, "Unauthorized"); mCodes.put(404, "Not Found"); mCodes.put(500, "Internal Server Error"); } public static String getResponseCodeDescription(int code) { String d = mCodes.get(code); if (d == null) return "Unknown"; return d; } public static void addResponseCodeDescription( int code, String description ) { mCodes.put(code, description); } public static interface WebSocketRequestCallback { void onConnected(WebSocket webSocket, AsyncHttpServerRequest request); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerRequest.java ================================================ package com.koushikdutta.async.http.server; import com.koushikdutta.async.AsyncSocket; import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.http.Headers; import com.koushikdutta.async.http.Multimap; import com.koushikdutta.async.http.body.AsyncHttpRequestBody; import java.util.Map; import java.util.regex.Matcher; public interface AsyncHttpServerRequest extends DataEmitter { Headers getHeaders(); Matcher getMatcher(); void setMatcher(Matcher matcher); T getBody(); AsyncSocket getSocket(); String getPath(); Multimap getQuery(); String getMethod(); String getUrl(); String get(String name); Map getState(); } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerRequestImpl.java ================================================ package com.koushikdutta.async.http.server; import com.koushikdutta.async.AsyncSocket; import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.FilteredDataEmitter; import com.koushikdutta.async.LineEmitter; import com.koushikdutta.async.LineEmitter.StringCallback; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.DataCallback; import com.koushikdutta.async.http.Headers; import com.koushikdutta.async.http.HttpUtil; import com.koushikdutta.async.http.Multimap; import com.koushikdutta.async.http.Protocol; import com.koushikdutta.async.http.body.AsyncHttpRequestBody; import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.regex.Matcher; public abstract class AsyncHttpServerRequestImpl extends FilteredDataEmitter implements AsyncHttpServerRequest, CompletedCallback { private String statusLine; private Headers mRawHeaders = new Headers(); AsyncSocket mSocket; private HashMap state = new HashMap<>(); @Override public HashMap getState() { return state; } public String getStatusLine() { return statusLine; } private CompletedCallback mReporter = new CompletedCallback() { @Override public void onCompleted(Exception error) { AsyncHttpServerRequestImpl.this.onCompleted(error); } }; @Override public void onCompleted(Exception e) { // if (mBody != null) // mBody.onCompleted(e); report(e); } abstract protected void onHeadersReceived(); protected void onNotHttp() { System.out.println("not http!"); } protected AsyncHttpRequestBody onUnknownBody(Headers headers) { return null; } protected AsyncHttpRequestBody onBody(Headers headers) { return null; } StringCallback mHeaderCallback = new StringCallback() { @Override public void onStringAvailable(String s) { if (statusLine == null) { statusLine = s; if (!statusLine.contains("HTTP/")) { onNotHttp(); mSocket.setDataCallback(new NullDataCallback()); report(new IOException("data/header received was not not http")); } return; } if (!"\r".equals(s)){ mRawHeaders.addLine(s); return; } DataEmitter emitter = HttpUtil.getBodyDecoder(mSocket, Protocol.HTTP_1_1, mRawHeaders, true); mBody = onBody(mRawHeaders); if (mBody == null) { mBody = HttpUtil.getBody(emitter, mReporter, mRawHeaders); if (mBody == null) { mBody = onUnknownBody(mRawHeaders); if (mBody == null) mBody = new UnknownRequestBody(mRawHeaders.get("Content-Type")); } } mBody.parse(emitter, mReporter); onHeadersReceived(); } }; String method; @Override public String getMethod() { return method; } void setSocket(AsyncSocket socket) { mSocket = socket; LineEmitter liner = new LineEmitter(); mSocket.setDataCallback(liner); liner.setLineCallback(mHeaderCallback); mSocket.setEndCallback(new NullCompletedCallback()); } @Override public AsyncSocket getSocket() { return mSocket; } @Override public Headers getHeaders() { return mRawHeaders; } @Override public void setDataCallback(DataCallback callback) { mSocket.setDataCallback(callback); } @Override public DataCallback getDataCallback() { return mSocket.getDataCallback(); } @Override public boolean isChunked() { return mSocket.isChunked(); } AsyncHttpRequestBody mBody; @Override public AsyncHttpRequestBody getBody() { return mBody; } @Override public void pause() { mSocket.pause(); } @Override public void resume() { mSocket.resume(); } @Override public boolean isPaused() { return mSocket.isPaused(); } @Override public String toString() { if (mRawHeaders == null) return super.toString(); return mRawHeaders.toPrefixString(statusLine); } @Override public String get(String name) { Multimap query = getQuery(); String ret = query.getString(name); if (ret != null) return ret; AsyncHttpRequestBody body = getBody(); Object bodyObject = body.get(); if (bodyObject instanceof Multimap) { Multimap map = (Multimap)bodyObject; return map.getString(name); } return null; } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerResponse.java ================================================ package com.koushikdutta.async.http.server; import com.koushikdutta.async.AsyncSocket; import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.DataSink; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.http.AsyncHttpResponse; import com.koushikdutta.async.http.Headers; import com.koushikdutta.async.http.body.AsyncHttpRequestBody; import com.koushikdutta.async.parser.AsyncParser; import org.json.JSONArray; import org.json.JSONObject; import java.io.File; import java.io.InputStream; import java.nio.ByteBuffer; public interface AsyncHttpServerResponse extends DataSink, CompletedCallback { void end(); void send(String contentType, byte[] bytes); void send(String contentType, ByteBufferList bb); void send(String contentType, ByteBuffer bb); void send(String contentType, String string); void send(String string); void send(JSONObject json); void send(JSONArray jsonArray); void sendFile(File file); void sendStream(InputStream inputStream, long totalLength); void sendBody(AsyncParser body, T value); AsyncHttpServerResponse code(int code); int code(); Headers getHeaders(); void writeHead(); void setContentType(String contentType); void redirect(String location); AsyncHttpServerRequest getRequest(); String getHttpVersion(); void setHttpVersion(String httpVersion); // NOT FINAL void proxy(AsyncHttpResponse response); /** * Alias for end. Used with CompletedEmitters */ void onCompleted(Exception ex); AsyncSocket getSocket(); void setSocket(AsyncSocket socket); } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerResponseImpl.java ================================================ package com.koushikdutta.async.http.server; import android.text.TextUtils; import com.koushikdutta.async.AsyncServer; import com.koushikdutta.async.AsyncSocket; import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.DataSink; import com.koushikdutta.async.Util; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.DataCallback; import com.koushikdutta.async.callback.WritableCallback; import com.koushikdutta.async.http.AsyncHttpHead; import com.koushikdutta.async.http.AsyncHttpResponse; import com.koushikdutta.async.http.Headers; import com.koushikdutta.async.http.HttpUtil; import com.koushikdutta.async.http.Protocol; import com.koushikdutta.async.http.filter.ChunkedOutputFilter; import com.koushikdutta.async.parser.AsyncParser; import com.koushikdutta.async.util.StreamUtility; import org.json.JSONArray; import org.json.JSONObject; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; import java.util.Locale; public class AsyncHttpServerResponseImpl implements AsyncHttpServerResponse { private Headers mRawHeaders = new Headers(); private long mContentLength = -1; @Override public Headers getHeaders() { return mRawHeaders; } public AsyncSocket getSocket() { return mSocket; } @Override public void setSocket(AsyncSocket socket) { mSocket = socket; } AsyncSocket mSocket; AsyncHttpServerRequestImpl mRequest; AsyncHttpServerResponseImpl(AsyncSocket socket, AsyncHttpServerRequestImpl req) { mSocket = socket; mRequest = req; if (HttpUtil.isKeepAlive(Protocol.HTTP_1_1, req.getHeaders())) mRawHeaders.set("Connection", "Keep-Alive"); } @Override public AsyncHttpServerRequest getRequest() { return mRequest; } @Override public void write(ByteBufferList bb) { // order is important here... // do the header write... this will call onWritable, which may be reentrant if (!headWritten) initFirstWrite(); // now check to see if the list is empty. reentrancy may cause it to empty itself. if (bb.remaining() == 0) return; // null sink means that the header has not finished writing if (mSink == null) return; // can successfully write! mSink.write(bb); } boolean headWritten = false; DataSink mSink; void initFirstWrite() { if (headWritten) return; headWritten = true; final boolean isChunked; String currentEncoding = mRawHeaders.get("Transfer-Encoding"); if ("".equals(currentEncoding)) mRawHeaders.removeAll("Transfer-Encoding"); boolean canUseChunked = ("Chunked".equalsIgnoreCase(currentEncoding) || currentEncoding == null) && !"close".equalsIgnoreCase(mRawHeaders.get("Connection")); if (mContentLength < 0) { String contentLength = mRawHeaders.get("Content-Length"); if (!TextUtils.isEmpty(contentLength)) mContentLength = Long.valueOf(contentLength); } if (mContentLength < 0 && canUseChunked) { mRawHeaders.set("Transfer-Encoding", "Chunked"); isChunked = true; } else { isChunked = false; } String statusLine = String.format(Locale.ENGLISH, "%s %s %s", httpVersion, code, AsyncHttpServer.getResponseCodeDescription(code)); String rh = mRawHeaders.toPrefixString(statusLine); Util.writeAll(mSocket, rh.getBytes(), ex -> { if (ex != null) { report(ex); return; } if (isChunked) { ChunkedOutputFilter chunked = new ChunkedOutputFilter(mSocket); chunked.setMaxBuffer(0); mSink = chunked; } else { mSink = mSocket; } mSink.setClosedCallback(closedCallback); closedCallback = null; mSink.setWriteableCallback(writable); writable = null; if (ended) { // the response ended while headers were written end(); return; } getServer().post(() -> { WritableCallback wb = getWriteableCallback(); if (wb != null) wb.onWriteable(); }); }); } WritableCallback writable; @Override public void setWriteableCallback(WritableCallback handler) { if (mSink != null) mSink.setWriteableCallback(handler); else writable = handler; } @Override public WritableCallback getWriteableCallback() { if (mSink != null) return mSink.getWriteableCallback(); return writable; } boolean ended; @Override public void end() { if (ended) return; ended = true; if (headWritten && mSink == null) { // header is in the process of being written... bail out. // end will be called again after finished. return; } if (!headWritten) { // end was called, and no head or body was yet written, // so strip the transfer encoding as that is superfluous. mRawHeaders.remove("Transfer-Encoding"); } if (mSink instanceof ChunkedOutputFilter) { // this filter won't close the socket underneath. mSink.end(); } else if (!headWritten) { if (!mRequest.getMethod().equalsIgnoreCase(AsyncHttpHead.METHOD)) send("text/html", ""); else { writeHead(); onEnd(); } } else { onEnd(); } } @Override public void writeHead() { initFirstWrite(); } @Override public void setContentType(String contentType) { mRawHeaders.set("Content-Type", contentType); } @Override public void send(final String contentType, final byte[] bytes) { send(contentType, new ByteBufferList(bytes)); } @Override public void sendBody(AsyncParser body, T value) { mRawHeaders.set("Content-Type", body.getMime()); body.write(this, value, ex -> end()); } @Override public void send(String contentType, ByteBuffer bb) { send(contentType, new ByteBufferList(bb)); } @Override public void send(String contentType, ByteBufferList bb) { getServer().post(() -> { mContentLength = bb.remaining(); mRawHeaders.set("Content-Length", Long.toString(mContentLength)); if (contentType != null) mRawHeaders.set("Content-Type", contentType); Util.writeAll(AsyncHttpServerResponseImpl.this, bb, ex -> onEnd()); }); } @Override public void send(String contentType, final String string) { try { send(contentType, string.getBytes("UTF-8")); } catch (UnsupportedEncodingException e) { throw new AssertionError(e); } } boolean mEnded; protected void onEnd() { mEnded = true; } protected void report(Exception e) { } @Override public void send(String string) { String contentType = mRawHeaders.get("Content-Type"); if (contentType == null) contentType = "text/html; charset=utf-8"; send(contentType, string); } @Override public void send(JSONObject json) { send("application/json; charset=utf-8", json.toString()); } @Override public void send(JSONArray jsonArray) { send("application/json; charset=utf-8", jsonArray.toString()); } @Override public void sendStream(final InputStream inputStream, long totalLength) { long start = 0; long end = totalLength - 1; String range = mRequest.getHeaders().get("Range"); if (range != null) { String[] parts = range.split("="); if (parts.length != 2 || !"bytes".equals(parts[0])) { // Requested range not satisfiable code(416); end(); return; } parts = parts[1].split("-"); try { if (parts.length > 2) throw new MalformedRangeException(); if (!TextUtils.isEmpty(parts[0])) start = Long.parseLong(parts[0]); if (parts.length == 2 && !TextUtils.isEmpty(parts[1])) end = Long.parseLong(parts[1]); else end = totalLength - 1; code(206); getHeaders().set("Content-Range", String.format(Locale.ENGLISH, "bytes %d-%d/%d", start, end, totalLength)); } catch (Exception e) { code(416); end(); return; } } try { if (start != inputStream.skip(start)) throw new StreamSkipException("skip failed to skip requested amount"); mContentLength = end - start + 1; mRawHeaders.set("Content-Length", String.valueOf(mContentLength)); mRawHeaders.set("Accept-Ranges", "bytes"); if (mRequest.getMethod().equals(AsyncHttpHead.METHOD)) { writeHead(); onEnd(); return; } if (mContentLength == 0) { writeHead(); StreamUtility.closeQuietly(inputStream); onEnd(); return; } getServer().post(() -> Util.pump(inputStream, mContentLength, AsyncHttpServerResponseImpl.this, ex -> { StreamUtility.closeQuietly(inputStream); onEnd(); })); } catch (Exception e) { code(500); end(); } } @Override public void sendFile(File file) { try { if (mRawHeaders.get("Content-Type") == null) mRawHeaders.set("Content-Type", AsyncHttpServer.getContentType(file.getAbsolutePath())); FileInputStream fin = new FileInputStream(file); sendStream(new BufferedInputStream(fin, 64000), file.length()); } catch (FileNotFoundException e) { code(404); end(); } } @Override public void proxy(final AsyncHttpResponse remoteResponse) { code(remoteResponse.code()); remoteResponse.headers().removeAll("Transfer-Encoding"); remoteResponse.headers().removeAll("Content-Encoding"); remoteResponse.headers().removeAll("Connection"); getHeaders().addAll(remoteResponse.headers()); // TODO: remove? remoteResponse.headers().set("Connection", "close"); Util.pump(remoteResponse, this, ex -> { remoteResponse.setEndCallback(new NullCompletedCallback()); remoteResponse.setDataCallback(new DataCallback.NullDataCallback()); end(); }); } int code = 200; @Override public AsyncHttpServerResponse code(int code) { this.code = code; return this; } @Override public int code() { return code; } @Override public void redirect(String location) { code(302); mRawHeaders.set("Location", location); end(); } String httpVersion = "HTTP/1.1"; @Override public String getHttpVersion() { return httpVersion; } @Override public void setHttpVersion(String httpVersion) { this.httpVersion = httpVersion; } @Override public void onCompleted(Exception ex) { end(); } @Override public boolean isOpen() { if (mSink != null) return mSink.isOpen(); return mSocket.isOpen(); } CompletedCallback closedCallback; @Override public void setClosedCallback(CompletedCallback handler) { if (mSink != null) mSink.setClosedCallback(handler); else closedCallback = handler; } @Override public CompletedCallback getClosedCallback() { if (mSink != null) return mSink.getClosedCallback(); return closedCallback; } @Override public AsyncServer getServer() { return mSocket.getServer(); } @Override public String toString() { if (mRawHeaders == null) return super.toString(); String statusLine = String.format(Locale.ENGLISH, "%s %s %s", httpVersion, code, AsyncHttpServer.getResponseCodeDescription(code)); return mRawHeaders.toPrefixString(statusLine); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerRouter.java ================================================ package com.koushikdutta.async.http.server; import android.content.Context; import android.content.res.AssetManager; import android.text.TextUtils; import android.util.Log; import com.koushikdutta.async.Util; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.future.Future; import com.koushikdutta.async.future.SimpleFuture; import com.koushikdutta.async.http.AsyncHttpGet; import com.koushikdutta.async.http.AsyncHttpHead; import com.koushikdutta.async.http.AsyncHttpPost; import com.koushikdutta.async.http.WebSocket; import com.koushikdutta.async.http.WebSocketImpl; import com.koushikdutta.async.util.StreamUtility; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Hashtable; import java.util.jar.Manifest; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; public class AsyncHttpServerRouter implements RouteMatcher { private static class RouteInfo { String method; Pattern regex; HttpServerRequestCallback callback; AsyncHttpRequestBodyProvider bodyCallback; } final ArrayList routes = new ArrayList<>(); public void removeAction(String action, String regex) { for (int i = 0; i < routes.size(); i++) { RouteInfo p = routes.get(i); if (TextUtils.equals(p.method, action) && regex.equals(p.regex.toString())) { routes.remove(i); return; } } } public void addAction(String action, String regex, HttpServerRequestCallback callback, AsyncHttpRequestBodyProvider bodyCallback) { RouteInfo p = new RouteInfo(); p.regex = Pattern.compile("^" + regex); p.callback = callback; p.method = action; p.bodyCallback = bodyCallback; synchronized (routes) { routes.add(p); } } public void addAction(String action, String regex, HttpServerRequestCallback callback) { addAction(action, regex, callback, null); } public void websocket(String regex, final AsyncHttpServer.WebSocketRequestCallback callback) { websocket(regex, null, callback); } static public WebSocket checkWebSocketUpgrade(final String protocol, AsyncHttpServerRequest request, final AsyncHttpServerResponse response) { boolean hasUpgrade = false; String connection = request.getHeaders().get("Connection"); if (connection != null) { String[] connections = connection.split(","); for (String c: connections) { if ("Upgrade".equalsIgnoreCase(c.trim())) { hasUpgrade = true; break; } } } if (!"websocket".equalsIgnoreCase(request.getHeaders().get("Upgrade")) || !hasUpgrade) { return null; } String peerProtocol = request.getHeaders().get("Sec-WebSocket-Protocol"); if (!TextUtils.equals(protocol, peerProtocol)) { return null; } return new WebSocketImpl(request, response); } public void websocket(String regex, final String protocol, final AsyncHttpServer.WebSocketRequestCallback callback) { get(regex, (request, response) -> { WebSocket webSocket = checkWebSocketUpgrade(protocol, request, response); if (webSocket == null) { response.code(404); response.end(); return; } callback.onConnected(webSocket, request); }); } public void get(String regex, HttpServerRequestCallback callback) { addAction(AsyncHttpGet.METHOD, regex, callback); } public void post(String regex, HttpServerRequestCallback callback) { addAction(AsyncHttpPost.METHOD, regex, callback); } public static class Asset { public Asset(int available, InputStream inputStream, String path) { this.available = available; this.inputStream = inputStream; this.path = path; } public int available; public InputStream inputStream; public String path; } public static Asset getAssetStream(final Context context, String asset) { return getAssetStream(context.getAssets(), asset); } public static Asset getAssetStream(AssetManager am, String asset) { try { InputStream is = am.open(asset); return new Asset(is.available(), is, asset); } catch (IOException e) { final String[] extensions = new String[] { "/index.htm", "/index.html", "index.htm", "index.html", ".htm", ".html" }; for (String ext: extensions) { try { InputStream is = am.open(asset + ext); return new Asset(is.available(), is, asset + ext); } catch (IOException ex) { } } return null; } } static Hashtable mContentTypes = new Hashtable<>(); static { mContentTypes.put("js", "application/javascript"); mContentTypes.put("json", "application/json"); mContentTypes.put("png", "image/png"); mContentTypes.put("jpg", "image/jpeg"); mContentTypes.put("jpeg", "image/jpeg"); mContentTypes.put("html", "text/html"); mContentTypes.put("css", "text/css"); mContentTypes.put("mp4", "video/mp4"); mContentTypes.put("mov", "video/quicktime"); mContentTypes.put("wmv", "video/x-ms-wmv"); mContentTypes.put("txt", "text/plain"); } public static String getContentType(String path) { int index = path.lastIndexOf("."); if (index != -1) { String e = path.substring(index + 1); String ct = mContentTypes.get(e); if (ct != null) return ct; } return null; } static Hashtable> AppManifests = new Hashtable<>(); static synchronized Manifest ensureManifest(Context context) { Future future = AppManifests.get(context.getPackageName()); if (future != null) return future.tryGet(); ZipFile zip = null; SimpleFuture result = new SimpleFuture<>(); try { zip = new ZipFile(context.getPackageResourcePath()); ZipEntry entry = zip.getEntry("META-INF/MANIFEST.MF"); Manifest manifest = new Manifest(zip.getInputStream(entry)); result.setComplete(manifest); return manifest; } catch (Exception e) { result.setComplete(e); return null; } finally { StreamUtility.closeQuietly(zip); AppManifests.put(context.getPackageName(), result); } } static boolean isClientCached(Context context, AsyncHttpServerRequest request, AsyncHttpServerResponse response, String assetFileName) { Manifest manifest = ensureManifest(context); if (manifest == null) return false; try { String digest = manifest.getEntries().get("assets/" + assetFileName).getValue("SHA-256-Digest"); if (TextUtils.isEmpty(digest)) return false; String etag = String.format("\"%s\"", digest); response.getHeaders().set("ETag", etag); String ifNoneMatch = request.getHeaders().get("If-None-Match"); return TextUtils.equals(ifNoneMatch, etag); } catch (Exception e) { Log.w(AsyncHttpServerRouter.class.getSimpleName(), "Error getting ETag for apk asset", e); return false; } } public void directory(Context context, String regex, final String assetPath) { AssetManager am = context.getAssets(); addAction(AsyncHttpGet.METHOD, regex, (request, response) -> { String path = request.getMatcher().replaceAll(""); Asset pair = getAssetStream(am, assetPath + path); if (pair == null || pair.inputStream == null) { response.code(404); response.end(); return; } if (isClientCached(context, request, response, pair.path)) { StreamUtility.closeQuietly(pair.inputStream); response.code(304); response.end(); return; } response.getHeaders().set("Content-Length", String.valueOf(pair.available)); response.getHeaders().add("Content-Type", getContentType(pair.path)); response.code(200); Util.pump(pair.inputStream, pair.available, response, ex -> { response.end(); StreamUtility.closeQuietly(pair.inputStream); }); }); addAction(AsyncHttpHead.METHOD, regex, (request, response) -> { String path = request.getMatcher().replaceAll(""); Asset pair = getAssetStream(am, assetPath + path); if (pair == null || pair.inputStream == null) { response.code(404); response.end(); return; } StreamUtility.closeQuietly(pair.inputStream); if (isClientCached(context, request, response, pair.path)) { response.code(304); } else { response.getHeaders().set("Content-Length", String.valueOf(pair.available)); response.getHeaders().add("Content-Type", getContentType(pair.path)); response.code(200); } response.end(); }); } public void directory(String regex, final File directory) { directory(regex, directory, false); } public void directory(String regex, final File directory, final boolean list) { addAction(AsyncHttpGet.METHOD, regex, new HttpServerRequestCallback() { @Override public void onRequest(AsyncHttpServerRequest request, final AsyncHttpServerResponse response) { String path = request.getMatcher().replaceAll(""); File file = new File(directory, path); if (file.isDirectory() && list) { ArrayList dirs = new ArrayList(); ArrayList files = new ArrayList(); for (File f: file.listFiles()) { if (f.isDirectory()) dirs.add(f); else files.add(f); } Comparator c = new Comparator() { @Override public int compare(File lhs, File rhs) { return lhs.getName().compareTo(rhs.getName()); } }; Collections.sort(dirs, c); Collections.sort(files, c); files.addAll(0, dirs); StringBuilder builder = new StringBuilder(); for (File f: files) { String p = new File(request.getPath(), f.getName()).getAbsolutePath(); builder.append(String.format("

", p, f.getName())); } response.send(builder.toString()); return; } if (!file.isFile()) { response.code(404); response.end(); return; } try { FileInputStream is = new FileInputStream(file); response.code(200); Util.pump(is, is.available(), response, new CompletedCallback() { @Override public void onCompleted(Exception ex) { response.end(); } }); } catch (IOException ex) { response.code(404); response.end(); } } }); } public static class RouteMatch { public final String method; public final String path; public final Matcher matcher; public final HttpServerRequestCallback callback; public final AsyncHttpRequestBodyProvider bodyCallback; private RouteMatch(String method, String path, Matcher matcher, HttpServerRequestCallback callback, AsyncHttpRequestBodyProvider bodyCallback) { this.method = method; this.path = path; this.matcher = matcher; this.callback = callback; this.bodyCallback = bodyCallback; } } abstract class AsyncHttpServerRequestImpl extends com.koushikdutta.async.http.server.AsyncHttpServerRequestImpl { Matcher matcher; @Override public Matcher getMatcher() { return matcher; } @Override public void setMatcher(Matcher matcher) { this.matcher = matcher; } } class Callback implements HttpServerRequestCallback, RouteMatcher { @Override public void onRequest(AsyncHttpServerRequest request, AsyncHttpServerResponse response) { RouteMatch match = route(request.getMethod(), request.getPath()); if (match == null) { response.code(404); response.end(); return; } match.callback.onRequest(request, response); } @Override public RouteMatch route(String method, String path) { return AsyncHttpServerRouter.this.route(method, path); } } private Callback callback = new Callback(); public HttpServerRequestCallback getCallback() { return callback; } @Override public RouteMatch route(String method, String path) { synchronized (routes) { for (RouteInfo p: routes) { // a null method is wildcard. used for nesting routers. if (!TextUtils.equals(method, p.method) && p.method != null) continue; Matcher m = p.regex.matcher(path); if (m.matches()) { if (p.callback instanceof RouteMatcher) { String subPath = m.group(1); return ((RouteMatcher)p.callback).route(method, subPath); } return new RouteMatch(method, path, m, p.callback, p.bodyCallback); } } } return null; } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/server/AsyncProxyServer.java ================================================ package com.koushikdutta.async.http.server; import android.net.Uri; import com.koushikdutta.async.AsyncServer; import com.koushikdutta.async.Util; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.http.AsyncHttpClient; import com.koushikdutta.async.http.AsyncHttpRequest; import com.koushikdutta.async.http.AsyncHttpResponse; import com.koushikdutta.async.http.callback.HttpConnectCallback; /** * Created by koush on 7/22/14. */ public class AsyncProxyServer extends AsyncHttpServer { AsyncHttpClient proxyClient; public AsyncProxyServer(AsyncServer server) { proxyClient = new AsyncHttpClient(server); } @Override protected void onRequest(HttpServerRequestCallback callback, AsyncHttpServerRequest request, final AsyncHttpServerResponse response) { super.onRequest(callback, request, response); if (callback != null) return; try { Uri uri; try { uri = Uri.parse(request.getPath()); if (uri.getScheme() == null) throw new Exception("no host or full uri provided"); } catch (Exception e) { String host = request.getHeaders().get("Host"); int port = 80; if (host != null) { String[] splits = host.split(":", 2); if (splits.length == 2) { host = splits[0]; port = Integer.parseInt(splits[1]); } } uri = Uri.parse("http://" + host + ":" + port + request.getPath()); } proxyClient.execute(new AsyncHttpRequest(uri, request.getMethod(), request.getHeaders()), new HttpConnectCallback() { @Override public void onConnectCompleted(Exception ex, AsyncHttpResponse remoteResponse) { if (ex != null) { response.code(500); response.send(ex.getMessage()); return; } response.proxy(remoteResponse); } }); } catch (Exception e) { response.code(500); response.send(e.getMessage()); } } @Override protected boolean onRequest(AsyncHttpServerRequest request, AsyncHttpServerResponse response) { return true; } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/server/BoundaryEmitter.java ================================================ package com.koushikdutta.async.http.server; import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.FilteredDataEmitter; import java.nio.ByteBuffer; public class BoundaryEmitter extends FilteredDataEmitter { private byte[] boundary; public void setBoundary(String boundary) { this.boundary = ("\r\n--" + boundary).getBytes(); } public String getBoundary() { if (boundary == null) return null; return new String(boundary, 4, boundary.length - 4); } public String getBoundaryStart() { return new String(boundary, 2, boundary.length - 2); } public String getBoundaryEnd() { return getBoundaryStart() + "--\r\n"; } protected void onBoundaryStart() { } protected void onBoundaryEnd() { } // >= 0 matching // -1 matching - (start of boundary end) or \r (boundary start) // -2 matching - (end of boundary end) // -3 matching \r after boundary // -4 matching \n after boundary // the state starts out having already matched \r\n /* Mac:work$ curl -F person=anonymous -F secret=@test.kt http://localhost:5555 POST / HTTP/1.1 Host: localhost:5555 User-Agent: curl/7.54.0 Content-Length: 372 Expect: 100-continue Content-Type: multipart/form-data; boundary=------------------------17903558439eb6ff --------------------------17903558439eb6ff <--- note! two dashes before boundary Content-Disposition: form-data; name="person" anonymous --------------------------17903558439eb6ff <--- note! two dashes before boundary Content-Disposition: form-data; name="secret"; filename="test.kt" Content-Type: application/octet-stream fun main(args: Array) { println("Hello JavaScript!") } --------------------------17903558439eb6ff-- <--- note! two dashes before AND after boundary */ int state = 2; @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { // System.out.println(bb.getString()); // System.out.println("chunk: " + bb.remaining()); // System.out.println("state: " + state); // if we were in the middle of a potential match, let's throw that // at the beginning of the buffer and process it too. if (state > 0) { ByteBuffer b = ByteBufferList.obtain(boundary.length); b.put(boundary, 0, state); b.flip(); bb.addFirst(b); state = 0; } int last = 0; byte[] buf = new byte[bb.remaining()]; bb.get(buf); for (int i = 0; i < buf.length; i++) { if (state >= 0) { if (buf[i] == boundary[state]) { state++; if (state == boundary.length) state = -1; } else if (state > 0) { // let's try matching again one byte after the start // of last match occurrence i -= state; state = 0; } } else if (state == -1) { if (buf[i] == '\r') { state = -4; int len = i - last - boundary.length; if (last != 0 || len != 0) { ByteBuffer b = ByteBufferList.obtain(len).put(buf, last, len); b.flip(); ByteBufferList list = new ByteBufferList(); list.add(b); super.onDataAvailable(this, list); } // System.out.println("bstart"); onBoundaryStart(); } else if (buf[i] == '-') { state = -2; } else { report(new MimeEncodingException("Invalid multipart/form-data. Expected \r or -")); return; } } else if (state == -2) { if (buf[i] == '-') { state = -3; } else { report(new MimeEncodingException("Invalid multipart/form-data. Expected -")); return; } } else if (state == -3) { if (buf[i] == '\r') { state = -4; ByteBuffer b = ByteBufferList.obtain(i - last - boundary.length - 2).put(buf, last, i - last - boundary.length - 2); b.flip(); ByteBufferList list = new ByteBufferList(); list.add(b); super.onDataAvailable(this, list); // System.out.println("bend"); onBoundaryEnd(); } else { report(new MimeEncodingException("Invalid multipart/form-data. Expected \r")); return; } } else if (state == -4) { if (buf[i] == '\n') { last = i + 1; state = 0; } else { report(new MimeEncodingException("Invalid multipart/form-data. Expected \n")); } } else { report(new MimeEncodingException("Invalid multipart/form-data. Unknown state?")); } } if (last < buf.length) { // System.out.println("amount left at boundary: " + (buf.length - last)); // System.out.println("State: " + state); // System.out.println(state); int keep = Math.max(state, 0); ByteBuffer b = ByteBufferList.obtain(buf.length - last - keep).put(buf, last, buf.length - last - keep); b.flip(); ByteBufferList list = new ByteBufferList(); list.add(b); super.onDataAvailable(this, list); } } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/server/HttpServerRequestCallback.java ================================================ package com.koushikdutta.async.http.server; public interface HttpServerRequestCallback { public void onRequest(AsyncHttpServerRequest request, AsyncHttpServerResponse response); } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/server/MalformedRangeException.java ================================================ package com.koushikdutta.async.http.server; public class MalformedRangeException extends Exception { } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/server/MimeEncodingException.java ================================================ package com.koushikdutta.async.http.server; public class MimeEncodingException extends Exception { public MimeEncodingException(String message) { super(message); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/server/RouteMatcher.java ================================================ package com.koushikdutta.async.http.server; public interface RouteMatcher { AsyncHttpServerRouter.RouteMatch route(String method, String path); } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/server/StreamSkipException.java ================================================ package com.koushikdutta.async.http.server; public class StreamSkipException extends Exception { public StreamSkipException(String message) { super(message); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/http/server/UnknownRequestBody.java ================================================ package com.koushikdutta.async.http.server; import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.DataSink; import com.koushikdutta.async.Util; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.DataCallback; import com.koushikdutta.async.http.AsyncHttpRequest; import com.koushikdutta.async.http.body.AsyncHttpRequestBody; public class UnknownRequestBody implements AsyncHttpRequestBody { public UnknownRequestBody(String contentType) { mContentType = contentType; } int length = -1; public UnknownRequestBody(DataEmitter emitter, String contentType, int length) { mContentType = contentType; this.emitter = emitter; this.length = length; } @Override public void write(final AsyncHttpRequest request, DataSink sink, final CompletedCallback completed) { Util.pump(emitter, sink, completed); if (emitter.isPaused()) emitter.resume(); } private String mContentType; @Override public String getContentType() { return mContentType; } @Override public boolean readFullyOnRequest() { return false; } @Override public int length() { return length; } @Override public Void get() { return null; } @Deprecated public void setCallbacks(DataCallback callback, CompletedCallback endCallback) { emitter.setEndCallback(endCallback); emitter.setDataCallback(callback); } public DataEmitter getEmitter() { return emitter; } DataEmitter emitter; @Override public void parse(DataEmitter emitter, CompletedCallback completed) { this.emitter = emitter; emitter.setEndCallback(completed); emitter.setDataCallback(new DataCallback.NullDataCallback()); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/parser/AsyncParser.java ================================================ package com.koushikdutta.async.parser; import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.DataSink; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.future.Future; import java.lang.reflect.Type; /** * Created by koush on 5/27/13. */ public interface AsyncParser { Future parse(DataEmitter emitter); void write(DataSink sink, T value, CompletedCallback completed); Type getType(); String getMime(); } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/parser/ByteBufferListParser.java ================================================ package com.koushikdutta.async.parser; import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.DataSink; import com.koushikdutta.async.Util; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.DataCallback; import com.koushikdutta.async.future.Future; import com.koushikdutta.async.future.SimpleFuture; import java.lang.reflect.Type; /** * Created by koush on 5/27/13. */ public class ByteBufferListParser implements AsyncParser { @Override public Future parse(final DataEmitter emitter) { final ByteBufferList bb = new ByteBufferList(); final SimpleFuture ret = new SimpleFuture() { @Override protected void cancelCleanup() { emitter.close(); } }; emitter.setDataCallback(new DataCallback() { @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList data) { data.get(bb); } }); emitter.setEndCallback(new CompletedCallback() { @Override public void onCompleted(Exception ex) { if (ex != null) { ret.setComplete(ex); return; } try { ret.setComplete(bb); } catch (Exception e) { ret.setComplete(e); } } }); return ret; } @Override public void write(DataSink sink, ByteBufferList value, CompletedCallback completed) { Util.writeAll(sink, value, completed); } @Override public Type getType() { return ByteBufferList.class; } @Override public String getMime() { return null; } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/parser/DocumentParser.java ================================================ package com.koushikdutta.async.parser; import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.DataSink; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.future.Future; import com.koushikdutta.async.http.body.DocumentBody; import com.koushikdutta.async.stream.ByteBufferListInputStream; import org.w3c.dom.Document; import java.lang.reflect.Type; import javax.xml.parsers.DocumentBuilderFactory; /** * Created by koush on 8/3/13. */ public class DocumentParser implements AsyncParser { @Override public Future parse(DataEmitter emitter) { return new ByteBufferListParser().parse(emitter) .thenConvert(from -> DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new ByteBufferListInputStream(from))); } @Override public void write(DataSink sink, Document value, CompletedCallback completed) { new DocumentBody(value).write(null, sink, completed); } @Override public Type getType() { return Document.class; } @Override public String getMime() { return "text/xml"; } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/parser/JSONArrayParser.java ================================================ package com.koushikdutta.async.parser; import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.DataSink; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.future.Future; import org.json.JSONArray; import java.lang.reflect.Type; /** * Created by koush on 5/27/13. */ public class JSONArrayParser implements AsyncParser { @Override public Future parse(DataEmitter emitter) { return new StringParser().parse(emitter) .thenConvert(JSONArray::new); } @Override public void write(DataSink sink, JSONArray value, CompletedCallback completed) { new StringParser().write(sink, value.toString(), completed); } @Override public Type getType() { return JSONArray.class; } @Override public String getMime() { return "application/json"; } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/parser/JSONObjectParser.java ================================================ package com.koushikdutta.async.parser; import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.DataSink; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.future.Future; import org.json.JSONObject; import java.lang.reflect.Type; /** * Created by koush on 5/27/13. */ public class JSONObjectParser implements AsyncParser { @Override public Future parse(DataEmitter emitter) { return new StringParser().parse(emitter).thenConvert(JSONObject::new); } @Override public void write(DataSink sink, JSONObject value, CompletedCallback completed) { new StringParser().write(sink, value.toString(), completed); } @Override public Type getType() { return JSONObject.class; } @Override public String getMime() { return "application/json"; } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/parser/StringParser.java ================================================ package com.koushikdutta.async.parser; import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.DataSink; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.future.Future; import java.lang.reflect.Type; import java.nio.charset.Charset; /** * Created by koush on 5/27/13. */ public class StringParser implements AsyncParser { Charset forcedCharset; public StringParser() { } public StringParser(Charset charset) { this.forcedCharset = charset; } @Override public Future parse(DataEmitter emitter) { final String charset = emitter.charset(); return new ByteBufferListParser().parse(emitter) .thenConvert(from -> { Charset charsetToUse = forcedCharset; if (charsetToUse == null && charset != null) charsetToUse = Charset.forName(charset); return from.readString(charsetToUse); }); } @Override public void write(DataSink sink, String value, CompletedCallback completed) { new ByteBufferListParser().write(sink, new ByteBufferList(value.getBytes()), completed); } @Override public Type getType() { return String.class; } @Override public String getMime() { return null; } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/stream/ByteBufferListInputStream.java ================================================ package com.koushikdutta.async.stream; import com.koushikdutta.async.ByteBufferList; import java.io.IOException; import java.io.InputStream; /** * Created by koush on 6/1/13. */ public class ByteBufferListInputStream extends InputStream { ByteBufferList bb; public ByteBufferListInputStream(ByteBufferList bb) { this.bb = bb; } @Override public int read() throws IOException { if (bb.remaining() <= 0) return -1; return (int)bb.get() & 0x000000ff; } @Override public int read(byte[] buffer) throws IOException { return this.read(buffer, 0, buffer.length); } @Override public int read(byte[] buffer, int offset, int length) throws IOException { if (bb.remaining() <= 0) return -1; int toRead = Math.min(length, bb.remaining()); bb.get(buffer, offset, toRead); return toRead; } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/stream/FileDataSink.java ================================================ package com.koushikdutta.async.stream; import com.koushikdutta.async.AsyncServer; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; /** * Created by koush on 2/2/14. */ public class FileDataSink extends OutputStreamDataSink { File file; public FileDataSink(AsyncServer server, File file) { super(server); this.file = file; } @Override public OutputStream getOutputStream() throws IOException { OutputStream ret = super.getOutputStream(); if (ret == null) { file.getParentFile().mkdirs(); ret = new FileOutputStream(file); setOutputStream(ret); } return ret; } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/stream/InputStreamDataEmitter.java ================================================ package com.koushikdutta.async.stream; import com.koushikdutta.async.AsyncServer; import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.Util; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.DataCallback; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; /** * Created by koush on 5/22/13. */ public class InputStreamDataEmitter implements DataEmitter { AsyncServer server; InputStream inputStream; public InputStreamDataEmitter(AsyncServer server, InputStream inputStream) { this.server = server; this.inputStream = inputStream; doResume(); } DataCallback callback; @Override public void setDataCallback(DataCallback callback) { this.callback = callback; } @Override public DataCallback getDataCallback() { return callback; } @Override public boolean isChunked() { return false; } boolean paused; @Override public void pause() { paused = true; } @Override public void resume() { paused = false; doResume(); } private void report(final Exception e) { getServer().post(new Runnable() { @Override public void run() { Exception ex = e; try { inputStream.close(); } catch (Exception e) { ex = e; } if (endCallback != null) endCallback.onCompleted(ex); } }); } int mToAlloc = 0; ByteBufferList pending = new ByteBufferList(); Runnable pumper = new Runnable() { @Override public void run() { try { if (!pending.isEmpty()) { getServer().run(new Runnable() { @Override public void run() { Util.emitAllData(InputStreamDataEmitter.this, pending); } }); if (!pending.isEmpty()) return; } ByteBuffer b; do { b = ByteBufferList.obtain(Math.min(Math.max(mToAlloc, 2 << 11), 256 * 1024)); int read; if (-1 == (read = inputStream.read(b.array()))) { report(null); return; } mToAlloc = read * 2; b.limit(read); pending.add(b); getServer().run(new Runnable() { @Override public void run() { Util.emitAllData(InputStreamDataEmitter.this, pending); } }); } while (pending.remaining() == 0 && !isPaused()); } catch (Exception e) { report(e); } } }; private void doResume() { new Thread(pumper).start(); } @Override public boolean isPaused() { return paused; } CompletedCallback endCallback; @Override public void setEndCallback(CompletedCallback callback) { endCallback = callback; } @Override public CompletedCallback getEndCallback() { return endCallback; } @Override public AsyncServer getServer() { return server; } @Override public void close() { report(null); try { inputStream.close(); } catch (Exception e) { } } @Override public String charset() { return null; } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/stream/OutputStreamDataCallback.java ================================================ 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.callback.CompletedCallback; import com.koushikdutta.async.callback.DataCallback; public class OutputStreamDataCallback implements DataCallback, CompletedCallback { private OutputStream mOutput; public OutputStreamDataCallback(OutputStream os) { mOutput = os; } public OutputStream getOutputStream() { return mOutput; } @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { try { while (bb.size() > 0) { ByteBuffer b = bb.remove(); mOutput.write(b.array(), b.arrayOffset() + b.position(), b.remaining()); ByteBufferList.reclaim(b); } } catch (Exception ex) { onCompleted(ex); } finally { bb.recycle(); } } public void close() { try { mOutput.close(); } catch (IOException e) { onCompleted(e); } } @Override public void onCompleted(Exception error) { error.printStackTrace(); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/stream/OutputStreamDataSink.java ================================================ package com.koushikdutta.async.stream; import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; import com.koushikdutta.async.AsyncServer; import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.DataSink; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.WritableCallback; public class OutputStreamDataSink implements DataSink { public OutputStreamDataSink(AsyncServer server) { this(server, null); } @Override public void end() { try { if (mStream != null) mStream.close(); reportClose(null); } catch (IOException e) { reportClose(e); } } AsyncServer server; public OutputStreamDataSink(AsyncServer server, OutputStream stream) { this.server = server; setOutputStream(stream); } OutputStream mStream; public void setOutputStream(OutputStream stream) { mStream = stream; } public OutputStream getOutputStream() throws IOException { return mStream; } @Override public void write(final ByteBufferList bb) { try { while (bb.size() > 0) { ByteBuffer b = bb.remove(); getOutputStream().write(b.array(), b.arrayOffset() + b.position(), b.remaining()); ByteBufferList.reclaim(b); } } catch (IOException e) { reportClose(e); } finally { bb.recycle(); } } WritableCallback mWritable; @Override public void setWriteableCallback(WritableCallback handler) { mWritable = handler; } @Override public WritableCallback getWriteableCallback() { return mWritable; } @Override public boolean isOpen() { return closeReported; } boolean closeReported; Exception closeException; public void reportClose(Exception ex) { if (closeReported) return; closeReported = true; closeException = ex; if (mClosedCallback != null) mClosedCallback.onCompleted(closeException); } CompletedCallback mClosedCallback; @Override public void setClosedCallback(CompletedCallback handler) { mClosedCallback = handler; } @Override public CompletedCallback getClosedCallback() { return mClosedCallback; } @Override public AsyncServer getServer() { return server; } WritableCallback outputStreamCallback; public void setOutputStreamWritableCallback(WritableCallback outputStreamCallback) { this.outputStreamCallback = outputStreamCallback; } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/util/Allocator.java ================================================ package com.koushikdutta.async.util; import com.koushikdutta.async.ByteBufferList; import java.nio.ByteBuffer; /** * Created by koush on 6/28/14. */ public class Allocator { final int maxAlloc; int currentAlloc = 0; int minAlloc = 2 << 11; public Allocator(int maxAlloc) { this.maxAlloc = maxAlloc; } public Allocator() { maxAlloc = ByteBufferList.MAX_ITEM_SIZE; } public ByteBuffer allocate() { return allocate(currentAlloc); } public ByteBuffer allocate(int currentAlloc) { return ByteBufferList.obtain(Math.min(Math.max(currentAlloc, minAlloc), maxAlloc)); } public void track(long read) { currentAlloc = (int)read * 2; } public int getMaxAlloc() { return maxAlloc; } public void setCurrentAlloc(int currentAlloc) { this.currentAlloc = currentAlloc; } public int getMinAlloc() { return minAlloc; } public Allocator setMinAlloc(int minAlloc ) { this.minAlloc = Math.max(0, minAlloc); return this; } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/util/ArrayDeque.java ================================================ /* * Written by Josh Bloch of Google Inc. and released to the public domain, * as explained at http://creativecommons.org/publicdomain/zero/1.0/. */ package com.koushikdutta.async.util; // BEGIN android-note // removed link to collections framework docs // END android-note import java.util.AbstractCollection; import java.util.Collection; import java.util.ConcurrentModificationException; import java.util.Iterator; import java.util.NoSuchElementException; /** * Resizable-array implementation of the {@link Deque} interface. Array * deques have no capacity restrictions; they grow as necessary to support * usage. They are not thread-safe; in the absence of external * synchronization, they do not support concurrent access by multiple threads. * Null elements are prohibited. This class is likely to be faster than * {@link Stack} when used as a stack, and faster than {@link LinkedList} * when used as a queue. * *

Most ArrayDeque operations run in amortized constant time. * Exceptions include {@link #remove(Object) remove}, {@link * #removeFirstOccurrence removeFirstOccurrence}, {@link #removeLastOccurrence * removeLastOccurrence}, {@link #contains contains}, {@link #iterator * iterator.remove()}, and the bulk operations, all of which run in linear * time. * *

The iterators returned by this class's iterator method are * fail-fast: If the deque is modified at any time after the iterator * is created, in any way except through the iterator's own remove * method, the iterator will generally throw a {@link * ConcurrentModificationException}. Thus, in the face of concurrent * modification, the iterator fails quickly and cleanly, rather than risking * arbitrary, non-deterministic behavior at an undetermined time in the * future. * *

Note that the fail-fast behavior of an iterator cannot be guaranteed * as it is, generally speaking, impossible to make any hard guarantees in the * presence of unsynchronized concurrent modification. Fail-fast iterators * throw ConcurrentModificationException on a best-effort basis. * Therefore, it would be wrong to write a program that depended on this * exception for its correctness: the fail-fast behavior of iterators * should be used only to detect bugs. * *

This class and its iterator implement all of the * optional methods of the {@link Collection} and {@link * Iterator} interfaces. * * @author Josh Bloch and Doug Lea * @since 1.6 * @param the type of elements held in this collection */ public class ArrayDeque extends AbstractCollection implements Deque, Cloneable, java.io.Serializable { /** * The array in which the elements of the deque are stored. * The capacity of the deque is the length of this array, which is * always a power of two. The array is never allowed to become * full, except transiently within an addX method where it is * resized (see doubleCapacity) immediately upon becoming full, * thus avoiding head and tail wrapping around to equal each * other. We also guarantee that all array cells not holding * deque elements are always null. */ private transient Object[] elements; /** * The index of the element at the head of the deque (which is the * element that would be removed by remove() or pop()); or an * arbitrary number equal to tail if the deque is empty. */ private transient int head; /** * The index at which the next element would be added to the tail * of the deque (via addLast(E), add(E), or push(E)). */ private transient int tail; /** * The minimum capacity that we'll use for a newly created deque. * Must be a power of 2. */ private static final int MIN_INITIAL_CAPACITY = 8; // ****** Array allocation and resizing utilities ****** /** * Allocate empty array to hold the given number of elements. * * @param numElements the number of elements to hold */ private void allocateElements(int numElements) { int initialCapacity = MIN_INITIAL_CAPACITY; // Find the best power of two to hold elements. // Tests "<=" because arrays aren't kept full. if (numElements >= initialCapacity) { initialCapacity = numElements; initialCapacity |= (initialCapacity >>> 1); initialCapacity |= (initialCapacity >>> 2); initialCapacity |= (initialCapacity >>> 4); initialCapacity |= (initialCapacity >>> 8); initialCapacity |= (initialCapacity >>> 16); initialCapacity++; if (initialCapacity < 0) // Too many elements, must back off initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements } elements = new Object[initialCapacity]; } /** * Double the capacity of this deque. Call only when full, i.e., * when head and tail have wrapped around to become equal. */ private void doubleCapacity() { int p = head; int n = elements.length; int r = n - p; // number of elements to the right of p int newCapacity = n << 1; if (newCapacity < 0) throw new IllegalStateException("Sorry, deque too big"); Object[] a = new Object[newCapacity]; System.arraycopy(elements, p, a, 0, r); System.arraycopy(elements, 0, a, r, p); elements = a; head = 0; tail = n; } /** * Copies the elements from our element array into the specified array, * in order (from first to last element in the deque). It is assumed * that the array is large enough to hold all elements in the deque. * * @return its argument */ private T[] copyElements(T[] a) { if (head < tail) { System.arraycopy(elements, head, a, 0, size()); } else if (head > tail) { int headPortionLen = elements.length - head; System.arraycopy(elements, head, a, 0, headPortionLen); System.arraycopy(elements, 0, a, headPortionLen, tail); } return a; } /** * Constructs an empty array deque with an initial capacity * sufficient to hold 16 elements. */ public ArrayDeque() { elements = new Object[16]; } /** * Constructs an empty array deque with an initial capacity * sufficient to hold the specified number of elements. * * @param numElements lower bound on initial capacity of the deque */ public ArrayDeque(int numElements) { allocateElements(numElements); } /** * Constructs a deque containing the elements of the specified * collection, in the order they are returned by the collection's * iterator. (The first element returned by the collection's * iterator becomes the first element, or front of the * deque.) * * @param c the collection whose elements are to be placed into the deque * @throws NullPointerException if the specified collection is null */ public ArrayDeque(Collection c) { allocateElements(c.size()); addAll(c); } // The main insertion and extraction methods are addFirst, // addLast, pollFirst, pollLast. The other methods are defined in // terms of these. /** * Inserts the specified element at the front of this deque. * * @param e the element to add * @throws NullPointerException if the specified element is null */ public void addFirst(E e) { if (e == null) throw new NullPointerException("e == null"); elements[head = (head - 1) & (elements.length - 1)] = e; if (head == tail) doubleCapacity(); } /** * Inserts the specified element at the end of this deque. * *

This method is equivalent to {@link #add}. * * @param e the element to add * @throws NullPointerException if the specified element is null */ public void addLast(E e) { if (e == null) throw new NullPointerException("e == null"); elements[tail] = e; if ( (tail = (tail + 1) & (elements.length - 1)) == head) doubleCapacity(); } /** * Inserts the specified element at the front of this deque. * * @param e the element to add * @return true (as specified by {@link Deque#offerFirst}) * @throws NullPointerException if the specified element is null */ public boolean offerFirst(E e) { addFirst(e); return true; } /** * Inserts the specified element at the end of this deque. * * @param e the element to add * @return true (as specified by {@link Deque#offerLast}) * @throws NullPointerException if the specified element is null */ public boolean offerLast(E e) { addLast(e); return true; } /** * @throws NoSuchElementException {@inheritDoc} */ public E removeFirst() { E x = pollFirst(); if (x == null) throw new NoSuchElementException(); return x; } /** * @throws NoSuchElementException {@inheritDoc} */ public E removeLast() { E x = pollLast(); if (x == null) throw new NoSuchElementException(); return x; } public E pollFirst() { int h = head; @SuppressWarnings("unchecked") E result = (E) elements[h]; // Element is null if deque empty if (result == null) return null; elements[h] = null; // Must null out slot head = (h + 1) & (elements.length - 1); return result; } public E pollLast() { int t = (tail - 1) & (elements.length - 1); @SuppressWarnings("unchecked") E result = (E) elements[t]; if (result == null) return null; elements[t] = null; tail = t; return result; } /** * @throws NoSuchElementException {@inheritDoc} */ public E getFirst() { @SuppressWarnings("unchecked") E result = (E) elements[head]; if (result == null) throw new NoSuchElementException(); return result; } /** * @throws NoSuchElementException {@inheritDoc} */ public E getLast() { @SuppressWarnings("unchecked") E result = (E) elements[(tail - 1) & (elements.length - 1)]; if (result == null) throw new NoSuchElementException(); return result; } public E peekFirst() { @SuppressWarnings("unchecked") E result = (E) elements[head]; // elements[head] is null if deque empty return result; } public E peekLast() { @SuppressWarnings("unchecked") E result = (E) elements[(tail - 1) & (elements.length - 1)]; return result; } /** * Removes the first occurrence of the specified element in this * deque (when traversing the deque from head to tail). * If the deque does not contain the element, it is unchanged. * More formally, removes the first element e such that * o.equals(e) (if such an element exists). * Returns true if this deque contained the specified element * (or equivalently, if this deque changed as a result of the call). * * @param o element to be removed from this deque, if present * @return true if the deque contained the specified element */ public boolean removeFirstOccurrence(Object o) { if (o == null) return false; int mask = elements.length - 1; int i = head; Object x; while ( (x = elements[i]) != null) { if (o.equals(x)) { delete(i); return true; } i = (i + 1) & mask; } return false; } /** * Removes the last occurrence of the specified element in this * deque (when traversing the deque from head to tail). * If the deque does not contain the element, it is unchanged. * More formally, removes the last element e such that * o.equals(e) (if such an element exists). * Returns true if this deque contained the specified element * (or equivalently, if this deque changed as a result of the call). * * @param o element to be removed from this deque, if present * @return true if the deque contained the specified element */ public boolean removeLastOccurrence(Object o) { if (o == null) return false; int mask = elements.length - 1; int i = (tail - 1) & mask; Object x; while ( (x = elements[i]) != null) { if (o.equals(x)) { delete(i); return true; } i = (i - 1) & mask; } return false; } // *** Queue methods *** /** * Inserts the specified element at the end of this deque. * *

This method is equivalent to {@link #addLast}. * * @param e the element to add * @return true (as specified by {@link Collection#add}) * @throws NullPointerException if the specified element is null */ public boolean add(E e) { addLast(e); return true; } /** * Inserts the specified element at the end of this deque. * *

This method is equivalent to {@link #offerLast}. * * @param e the element to add * @return true (as specified by {@link Queue#offer}) * @throws NullPointerException if the specified element is null */ public boolean offer(E e) { return offerLast(e); } /** * Retrieves and removes the head of the queue represented by this deque. * * This method differs from {@link #poll poll} only in that it throws an * exception if this deque is empty. * *

This method is equivalent to {@link #removeFirst}. * * @return the head of the queue represented by this deque * @throws NoSuchElementException {@inheritDoc} */ public E remove() { return removeFirst(); } /** * Retrieves and removes the head of the queue represented by this deque * (in other words, the first element of this deque), or returns * null if this deque is empty. * *

This method is equivalent to {@link #pollFirst}. * * @return the head of the queue represented by this deque, or * null if this deque is empty */ public E poll() { return pollFirst(); } /** * Retrieves, but does not remove, the head of the queue represented by * this deque. This method differs from {@link #peek peek} only in * that it throws an exception if this deque is empty. * *

This method is equivalent to {@link #getFirst}. * * @return the head of the queue represented by this deque * @throws NoSuchElementException {@inheritDoc} */ public E element() { return getFirst(); } /** * Retrieves, but does not remove, the head of the queue represented by * this deque, or returns null if this deque is empty. * *

This method is equivalent to {@link #peekFirst}. * * @return the head of the queue represented by this deque, or * null if this deque is empty */ public E peek() { return peekFirst(); } // *** Stack methods *** /** * Pushes an element onto the stack represented by this deque. In other * words, inserts the element at the front of this deque. * *

This method is equivalent to {@link #addFirst}. * * @param e the element to push * @throws NullPointerException if the specified element is null */ public void push(E e) { addFirst(e); } /** * Pops an element from the stack represented by this deque. In other * words, removes and returns the first element of this deque. * *

This method is equivalent to {@link #removeFirst()}. * * @return the element at the front of this deque (which is the top * of the stack represented by this deque) * @throws NoSuchElementException {@inheritDoc} */ public E pop() { return removeFirst(); } private void checkInvariants() { } /** * Removes the element at the specified position in the elements array, * adjusting head and tail as necessary. This can result in motion of * elements backwards or forwards in the array. * *

This method is called delete rather than remove to emphasize * that its semantics differ from those of {@link List#remove(int)}. * * @return true if elements moved backwards */ private boolean delete(int i) { checkInvariants(); final Object[] elements = this.elements; final int mask = elements.length - 1; final int h = head; final int t = tail; final int front = (i - h) & mask; final int back = (t - i) & mask; // Invariant: head <= i < tail mod circularity if (front >= ((t - h) & mask)) throw new ConcurrentModificationException(); // Optimize for least element motion if (front < back) { if (h <= i) { System.arraycopy(elements, h, elements, h + 1, front); } else { // Wrap around System.arraycopy(elements, 0, elements, 1, i); elements[0] = elements[mask]; System.arraycopy(elements, h, elements, h + 1, mask - h); } elements[h] = null; head = (h + 1) & mask; return false; } else { if (i < t) { // Copy the null tail as well System.arraycopy(elements, i + 1, elements, i, back); tail = t - 1; } else { // Wrap around System.arraycopy(elements, i + 1, elements, i, mask - i); elements[mask] = elements[0]; System.arraycopy(elements, 1, elements, 0, t); tail = (t - 1) & mask; } return true; } } // *** Collection Methods *** /** * Returns the number of elements in this deque. * * @return the number of elements in this deque */ public int size() { return (tail - head) & (elements.length - 1); } /** * Returns true if this deque contains no elements. * * @return true if this deque contains no elements */ public boolean isEmpty() { return head == tail; } /** * Returns an iterator over the elements in this deque. The elements * will be ordered from first (head) to last (tail). This is the same * order that elements would be dequeued (via successive calls to * {@link #remove} or popped (via successive calls to {@link #pop}). * * @return an iterator over the elements in this deque */ public Iterator iterator() { return new DeqIterator(); } public Iterator descendingIterator() { return new DescendingIterator(); } private class DeqIterator implements Iterator { /** * Index of element to be returned by subsequent call to next. */ private int cursor = head; /** * Tail recorded at construction (also in remove), to stop * iterator and also to check for comodification. */ private int fence = tail; /** * Index of element returned by most recent call to next. * Reset to -1 if element is deleted by a call to remove. */ private int lastRet = -1; public boolean hasNext() { return cursor != fence; } public E next() { if (cursor == fence) throw new NoSuchElementException(); @SuppressWarnings("unchecked") E result = (E) elements[cursor]; // This check doesn't catch all possible comodifications, // but does catch the ones that corrupt traversal if (tail != fence || result == null) throw new ConcurrentModificationException(); lastRet = cursor; cursor = (cursor + 1) & (elements.length - 1); return result; } public void remove() { if (lastRet < 0) throw new IllegalStateException(); if (delete(lastRet)) { // if left-shifted, undo increment in next() cursor = (cursor - 1) & (elements.length - 1); fence = tail; } lastRet = -1; } } private class DescendingIterator implements Iterator { /* * This class is nearly a mirror-image of DeqIterator, using * tail instead of head for initial cursor, and head instead of * tail for fence. */ private int cursor = tail; private int fence = head; private int lastRet = -1; public boolean hasNext() { return cursor != fence; } public E next() { if (cursor == fence) throw new NoSuchElementException(); cursor = (cursor - 1) & (elements.length - 1); @SuppressWarnings("unchecked") E result = (E) elements[cursor]; if (head != fence || result == null) throw new ConcurrentModificationException(); lastRet = cursor; return result; } public void remove() { if (lastRet < 0) throw new IllegalStateException(); if (!delete(lastRet)) { cursor = (cursor + 1) & (elements.length - 1); fence = head; } lastRet = -1; } } /** * Returns true if this deque contains the specified element. * More formally, returns true if and only if this deque contains * at least one element e such that o.equals(e). * * @param o object to be checked for containment in this deque * @return true if this deque contains the specified element */ public boolean contains(Object o) { if (o == null) return false; int mask = elements.length - 1; int i = head; Object x; while ( (x = elements[i]) != null) { if (o.equals(x)) return true; i = (i + 1) & mask; } return false; } /** * Removes a single instance of the specified element from this deque. * If the deque does not contain the element, it is unchanged. * More formally, removes the first element e such that * o.equals(e) (if such an element exists). * Returns true if this deque contained the specified element * (or equivalently, if this deque changed as a result of the call). * *

This method is equivalent to {@link #removeFirstOccurrence}. * * @param o element to be removed from this deque, if present * @return true if this deque contained the specified element */ public boolean remove(Object o) { return removeFirstOccurrence(o); } /** * Removes all of the elements from this deque. * The deque will be empty after this call returns. */ public void clear() { int h = head; int t = tail; if (h != t) { // clear all cells head = tail = 0; int i = h; int mask = elements.length - 1; do { elements[i] = null; i = (i + 1) & mask; } while (i != t); } } /** * Returns an array containing all of the elements in this deque * in proper sequence (from first to last element). * *

The returned array will be "safe" in that no references to it are * maintained by this deque. (In other words, this method must allocate * a new array). The caller is thus free to modify the returned array. * *

This method acts as bridge between array-based and collection-based * APIs. * * @return an array containing all of the elements in this deque */ public Object[] toArray() { return copyElements(new Object[size()]); } /** * Returns an array containing all of the elements in this deque in * proper sequence (from first to last element); the runtime type of the * returned array is that of the specified array. If the deque fits in * the specified array, it is returned therein. Otherwise, a new array * is allocated with the runtime type of the specified array and the * size of this deque. * *

If this deque fits in the specified array with room to spare * (i.e., the array has more elements than this deque), the element in * the array immediately following the end of the deque is set to * null. * *

Like the {@link #toArray()} method, this method acts as bridge between * array-based and collection-based APIs. Further, this method allows * precise control over the runtime type of the output array, and may, * under certain circumstances, be used to save allocation costs. * *

Suppose x is a deque known to contain only strings. * The following code can be used to dump the deque into a newly * allocated array of String: * *

 {@code String[] y = x.toArray(new String[0]);}
* * Note that toArray(new Object[0]) is identical in function to * toArray(). * * @param a the array into which the elements of the deque are to * be stored, if it is big enough; otherwise, a new array of the * same runtime type is allocated for this purpose * @return an array containing all of the elements in this deque * @throws ArrayStoreException if the runtime type of the specified array * is not a supertype of the runtime type of every element in * this deque * @throws NullPointerException if the specified array is null */ @SuppressWarnings("unchecked") public T[] toArray(T[] a) { int size = size(); if (a.length < size) a = (T[])java.lang.reflect.Array.newInstance( a.getClass().getComponentType(), size); copyElements(a); if (a.length > size) a[size] = null; return a; } // *** Object methods *** /** * Returns a copy of this deque. * * @return a copy of this deque */ public ArrayDeque clone() { try { @SuppressWarnings("unchecked") ArrayDeque result = (ArrayDeque) super.clone(); System.arraycopy(elements, 0, result.elements, 0, elements.length); return result; } catch (CloneNotSupportedException e) { throw new AssertionError(); } } /** * Appease the serialization gods. */ private static final long serialVersionUID = 2340985798034038923L; /** * Serialize this deque. * * @serialData The current size (int) of the deque, * followed by all of its elements (each an object reference) in * first-to-last order. */ private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException { s.defaultWriteObject(); // Write out size s.writeInt(size()); // Write out elements in order. int mask = elements.length - 1; for (int i = head; i != tail; i = (i + 1) & mask) s.writeObject(elements[i]); } /** * Deserialize this deque. */ private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { s.defaultReadObject(); // Read in size and allocate array int size = s.readInt(); allocateElements(size); head = 0; tail = size; // Read in all elements in the proper order. for (int i = 0; i < size; i++) elements[i] = s.readObject(); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/util/Charsets.java ================================================ package com.koushikdutta.async.util; import java.nio.charset.Charset; /** From java.nio.charset.Charsets */ public class Charsets { public static final Charset US_ASCII = Charset.forName("US-ASCII"); public static final Charset UTF_8 = Charset.forName("UTF-8"); public static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1"); } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/util/Deque.java ================================================ /* * Written by Doug Lea and Josh Bloch with assistance from members of * JCP JSR-166 Expert Group and released to the public domain, as explained * at http://creativecommons.org/publicdomain/zero/1.0/ */ package com.koushikdutta.async.util; // BEGIN android-note // removed link to collections framework docs // END android-note import java.util.Iterator; import java.util.Queue; /** * A linear collection that supports element insertion and removal at * both ends. The name deque is short for "double ended queue" * and is usually pronounced "deck". Most Deque * implementations place no fixed limits on the number of elements * they may contain, but this interface supports capacity-restricted * deques as well as those with no fixed size limit. * *

This interface defines methods to access the elements at both * ends of the deque. Methods are provided to insert, remove, and * examine the element. Each of these methods exists in two forms: * one throws an exception if the operation fails, the other returns a * special value (either null or false, depending on * the operation). The latter form of the insert operation is * designed specifically for use with capacity-restricted * Deque implementations; in most implementations, insert * operations cannot fail. * *

The twelve methods described above are summarized in the * following table: * *

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
First Element (Head) Last Element (Tail)
Throws exceptionSpecial valueThrows exceptionSpecial value
Insert{@link #addFirst addFirst(e)}{@link #offerFirst offerFirst(e)}{@link #addLast addLast(e)}{@link #offerLast offerLast(e)}
Remove{@link #removeFirst removeFirst()}{@link #pollFirst pollFirst()}{@link #removeLast removeLast()}{@link #pollLast pollLast()}
Examine{@link #getFirst getFirst()}{@link #peekFirst peekFirst()}{@link #getLast getLast()}{@link #peekLast peekLast()}
* *

This interface extends the {@link Queue} interface. When a deque is * used as a queue, FIFO (First-In-First-Out) behavior results. Elements are * added at the end of the deque and removed from the beginning. The methods * inherited from the Queue interface are precisely equivalent to * Deque methods as indicated in the following table: * *

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
Queue Method Equivalent Deque Method
{@link java.util.Queue#add add(e)}{@link #addLast addLast(e)}
{@link java.util.Queue#offer offer(e)}{@link #offerLast offerLast(e)}
{@link java.util.Queue#remove remove()}{@link #removeFirst removeFirst()}
{@link java.util.Queue#poll poll()}{@link #pollFirst pollFirst()}
{@link java.util.Queue#element element()}{@link #getFirst getFirst()}
{@link java.util.Queue#peek peek()}{@link #peek peekFirst()}
* *

Deques can also be used as LIFO (Last-In-First-Out) stacks. This * interface should be used in preference to the legacy {@link Stack} class. * When a deque is used as a stack, elements are pushed and popped from the * beginning of the deque. Stack methods are precisely equivalent to * Deque methods as indicated in the table below: * *

* * * * * * * * * * * * * * * * * *
Stack Method Equivalent Deque Method
{@link #push push(e)}{@link #addFirst addFirst(e)}
{@link #pop pop()}{@link #removeFirst removeFirst()}
{@link #peek peek()}{@link #peekFirst peekFirst()}
* *

Note that the {@link #peek peek} method works equally well when * a deque is used as a queue or a stack; in either case, elements are * drawn from the beginning of the deque. * *

This interface provides two methods to remove interior * elements, {@link #removeFirstOccurrence removeFirstOccurrence} and * {@link #removeLastOccurrence removeLastOccurrence}. * *

Unlike the {@link List} interface, this interface does not * provide support for indexed access to elements. * *

While Deque implementations are not strictly required * to prohibit the insertion of null elements, they are strongly * encouraged to do so. Users of any Deque implementations * that do allow null elements are strongly encouraged not to * take advantage of the ability to insert nulls. This is so because * null is used as a special return value by various methods * to indicated that the deque is empty. * *

Deque implementations generally do not define * element-based versions of the equals and hashCode * methods, but instead inherit the identity-based versions from class * Object. * * @author Doug Lea * @author Josh Bloch * @since 1.6 * @param the type of elements held in this collection */ public interface Deque extends Queue { /** * Inserts the specified element at the front of this deque if it is * possible to do so immediately without violating capacity restrictions. * When using a capacity-restricted deque, it is generally preferable to * use method {@link #offerFirst}. * * @param e the element to add * @throws IllegalStateException if the element cannot be added at this * time due to capacity restrictions * @throws ClassCastException if the class of the specified element * prevents it from being added to this deque * @throws NullPointerException if the specified element is null and this * deque does not permit null elements * @throws IllegalArgumentException if some property of the specified * element prevents it from being added to this deque */ void addFirst(E e); /** * Inserts the specified element at the end of this deque if it is * possible to do so immediately without violating capacity restrictions. * When using a capacity-restricted deque, it is generally preferable to * use method {@link #offerLast}. * *

This method is equivalent to {@link #add}. * * @param e the element to add * @throws IllegalStateException if the element cannot be added at this * time due to capacity restrictions * @throws ClassCastException if the class of the specified element * prevents it from being added to this deque * @throws NullPointerException if the specified element is null and this * deque does not permit null elements * @throws IllegalArgumentException if some property of the specified * element prevents it from being added to this deque */ void addLast(E e); /** * Inserts the specified element at the front of this deque unless it would * violate capacity restrictions. When using a capacity-restricted deque, * this method is generally preferable to the {@link #addFirst} method, * which can fail to insert an element only by throwing an exception. * * @param e the element to add * @return true if the element was added to this deque, else * false * @throws ClassCastException if the class of the specified element * prevents it from being added to this deque * @throws NullPointerException if the specified element is null and this * deque does not permit null elements * @throws IllegalArgumentException if some property of the specified * element prevents it from being added to this deque */ boolean offerFirst(E e); /** * Inserts the specified element at the end of this deque unless it would * violate capacity restrictions. When using a capacity-restricted deque, * this method is generally preferable to the {@link #addLast} method, * which can fail to insert an element only by throwing an exception. * * @param e the element to add * @return true if the element was added to this deque, else * false * @throws ClassCastException if the class of the specified element * prevents it from being added to this deque * @throws NullPointerException if the specified element is null and this * deque does not permit null elements * @throws IllegalArgumentException if some property of the specified * element prevents it from being added to this deque */ boolean offerLast(E e); /** * Retrieves and removes the first element of this deque. This method * differs from {@link #pollFirst pollFirst} only in that it throws an * exception if this deque is empty. * * @return the head of this deque * @throws NoSuchElementException if this deque is empty */ E removeFirst(); /** * Retrieves and removes the last element of this deque. This method * differs from {@link #pollLast pollLast} only in that it throws an * exception if this deque is empty. * * @return the tail of this deque * @throws NoSuchElementException if this deque is empty */ E removeLast(); /** * Retrieves and removes the first element of this deque, * or returns null if this deque is empty. * * @return the head of this deque, or null if this deque is empty */ E pollFirst(); /** * Retrieves and removes the last element of this deque, * or returns null if this deque is empty. * * @return the tail of this deque, or null if this deque is empty */ E pollLast(); /** * Retrieves, but does not remove, the first element of this deque. * * This method differs from {@link #peekFirst peekFirst} only in that it * throws an exception if this deque is empty. * * @return the head of this deque * @throws NoSuchElementException if this deque is empty */ E getFirst(); /** * Retrieves, but does not remove, the last element of this deque. * This method differs from {@link #peekLast peekLast} only in that it * throws an exception if this deque is empty. * * @return the tail of this deque * @throws NoSuchElementException if this deque is empty */ E getLast(); /** * Retrieves, but does not remove, the first element of this deque, * or returns null if this deque is empty. * * @return the head of this deque, or null if this deque is empty */ E peekFirst(); /** * Retrieves, but does not remove, the last element of this deque, * or returns null if this deque is empty. * * @return the tail of this deque, or null if this deque is empty */ E peekLast(); /** * Removes the first occurrence of the specified element from this deque. * If the deque does not contain the element, it is unchanged. * More formally, removes the first element e such that * (o==null ? e==null : o.equals(e)) * (if such an element exists). * Returns true if this deque contained the specified element * (or equivalently, if this deque changed as a result of the call). * * @param o element to be removed from this deque, if present * @return true if an element was removed as a result of this call * @throws ClassCastException if the class of the specified element * is incompatible with this deque (optional) * @throws NullPointerException if the specified element is null and this * deque does not permit null elements (optional) */ boolean removeFirstOccurrence(Object o); /** * Removes the last occurrence of the specified element from this deque. * If the deque does not contain the element, it is unchanged. * More formally, removes the last element e such that * (o==null ? e==null : o.equals(e)) * (if such an element exists). * Returns true if this deque contained the specified element * (or equivalently, if this deque changed as a result of the call). * * @param o element to be removed from this deque, if present * @return true if an element was removed as a result of this call * @throws ClassCastException if the class of the specified element * is incompatible with this deque (optional) * @throws NullPointerException if the specified element is null and this * deque does not permit null elements (optional) */ boolean removeLastOccurrence(Object o); // *** Queue methods *** /** * Inserts the specified element into the queue represented by this deque * (in other words, at the tail of this deque) if it is possible to do so * immediately without violating capacity restrictions, returning * true upon success and throwing an * IllegalStateException if no space is currently available. * When using a capacity-restricted deque, it is generally preferable to * use {@link #offer(Object) offer}. * *

This method is equivalent to {@link #addLast}. * * @param e the element to add * @return true (as specified by {@link Collection#add}) * @throws IllegalStateException if the element cannot be added at this * time due to capacity restrictions * @throws ClassCastException if the class of the specified element * prevents it from being added to this deque * @throws NullPointerException if the specified element is null and this * deque does not permit null elements * @throws IllegalArgumentException if some property of the specified * element prevents it from being added to this deque */ boolean add(E e); /** * Inserts the specified element into the queue represented by this deque * (in other words, at the tail of this deque) if it is possible to do so * immediately without violating capacity restrictions, returning * true upon success and false if no space is currently * available. When using a capacity-restricted deque, this method is * generally preferable to the {@link #add} method, which can fail to * insert an element only by throwing an exception. * *

This method is equivalent to {@link #offerLast}. * * @param e the element to add * @return true if the element was added to this deque, else * false * @throws ClassCastException if the class of the specified element * prevents it from being added to this deque * @throws NullPointerException if the specified element is null and this * deque does not permit null elements * @throws IllegalArgumentException if some property of the specified * element prevents it from being added to this deque */ boolean offer(E e); /** * Retrieves and removes the head of the queue represented by this deque * (in other words, the first element of this deque). * This method differs from {@link #poll poll} only in that it throws an * exception if this deque is empty. * *

This method is equivalent to {@link #removeFirst()}. * * @return the head of the queue represented by this deque * @throws NoSuchElementException if this deque is empty */ E remove(); /** * Retrieves and removes the head of the queue represented by this deque * (in other words, the first element of this deque), or returns * null if this deque is empty. * *

This method is equivalent to {@link #pollFirst()}. * * @return the first element of this deque, or null if * this deque is empty */ E poll(); /** * Retrieves, but does not remove, the head of the queue represented by * this deque (in other words, the first element of this deque). * This method differs from {@link #peek peek} only in that it throws an * exception if this deque is empty. * *

This method is equivalent to {@link #getFirst()}. * * @return the head of the queue represented by this deque * @throws NoSuchElementException if this deque is empty */ E element(); /** * Retrieves, but does not remove, the head of the queue represented by * this deque (in other words, the first element of this deque), or * returns null if this deque is empty. * *

This method is equivalent to {@link #peekFirst()}. * * @return the head of the queue represented by this deque, or * null if this deque is empty */ E peek(); // *** Stack methods *** /** * Pushes an element onto the stack represented by this deque (in other * words, at the head of this deque) if it is possible to do so * immediately without violating capacity restrictions, returning * true upon success and throwing an * IllegalStateException if no space is currently available. * *

This method is equivalent to {@link #addFirst}. * * @param e the element to push * @throws IllegalStateException if the element cannot be added at this * time due to capacity restrictions * @throws ClassCastException if the class of the specified element * prevents it from being added to this deque * @throws NullPointerException if the specified element is null and this * deque does not permit null elements * @throws IllegalArgumentException if some property of the specified * element prevents it from being added to this deque */ void push(E e); /** * Pops an element from the stack represented by this deque. In other * words, removes and returns the first element of this deque. * *

This method is equivalent to {@link #removeFirst()}. * * @return the element at the front of this deque (which is the top * of the stack represented by this deque) * @throws NoSuchElementException if this deque is empty */ E pop(); // *** Collection methods *** /** * Removes the first occurrence of the specified element from this deque. * If the deque does not contain the element, it is unchanged. * More formally, removes the first element e such that * (o==null ? e==null : o.equals(e)) * (if such an element exists). * Returns true if this deque contained the specified element * (or equivalently, if this deque changed as a result of the call). * *

This method is equivalent to {@link #removeFirstOccurrence}. * * @param o element to be removed from this deque, if present * @return true if an element was removed as a result of this call * @throws ClassCastException if the class of the specified element * is incompatible with this deque (optional) * @throws NullPointerException if the specified element is null and this * deque does not permit null elements (optional) */ boolean remove(Object o); /** * Returns true if this deque contains the specified element. * More formally, returns true if and only if this deque contains * at least one element e such that * (o==null ? e==null : o.equals(e)). * * @param o element whose presence in this deque is to be tested * @return true if this deque contains the specified element * @throws ClassCastException if the type of the specified element * is incompatible with this deque (optional) * @throws NullPointerException if the specified element is null and this * deque does not permit null elements (optional) */ boolean contains(Object o); /** * Returns the number of elements in this deque. * * @return the number of elements in this deque */ public int size(); /** * Returns an iterator over the elements in this deque in proper sequence. * The elements will be returned in order from first (head) to last (tail). * * @return an iterator over the elements in this deque in proper sequence */ Iterator iterator(); /** * Returns an iterator over the elements in this deque in reverse * sequential order. The elements will be returned in order from * last (tail) to first (head). * * @return an iterator over the elements in this deque in reverse * sequence */ Iterator descendingIterator(); } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/util/FileCache.java ================================================ package com.koushikdutta.async.util; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.math.BigInteger; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.Provider; import java.security.Security; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.Random; import java.util.Set; /** * Created by koush on 4/12/14. */ public class FileCache { class CacheEntry { final long size; public CacheEntry(File file) { size = file.length(); } } public static class Snapshot { FileInputStream[] fins; long[] lens; Snapshot(FileInputStream[] fins, long[] lens) { this.fins = fins; this.lens = lens; } public long getLength(int index) { return lens[index]; } public void close() { StreamUtility.closeQuietly(fins); } } private static String hashAlgorithm = "MD5"; private static MessageDigest findAlternativeMessageDigest() { if ("MD5".equals(hashAlgorithm)) { for (Provider provider : Security.getProviders()) { for (Provider.Service service : provider.getServices()) { hashAlgorithm = service.getAlgorithm(); try { MessageDigest messageDigest = MessageDigest.getInstance(hashAlgorithm); if (messageDigest != null) return messageDigest; } catch (NoSuchAlgorithmException ignored) { } } } } return null; } static MessageDigest messageDigest; static { try { messageDigest = MessageDigest.getInstance(hashAlgorithm); } catch (NoSuchAlgorithmException e) { messageDigest = findAlternativeMessageDigest(); if (null == messageDigest) throw new RuntimeException(e); } try { messageDigest = (MessageDigest)messageDigest.clone(); } catch (CloneNotSupportedException e) { } } public static synchronized String toKeyString(Object... parts) { messageDigest.reset(); for (Object part : parts) { messageDigest.update(part.toString().getBytes()); } byte[] md5bytes = messageDigest.digest(); return new BigInteger(1, md5bytes).toString(16); } boolean loadAsync; Random random = new Random(); public File getTempFile() { File f; while ((f = new File(directory, new BigInteger(128, random).toString(16))).exists()); return f; } public File[] getTempFiles(int count) { File[] ret = new File[count]; for (int i = 0; i < count; i++) { ret[i] = getTempFile(); } return ret; } public static void removeFiles(File... files) { if (files == null) return; for (File file: files) { file.delete(); } } public void remove(String key) { int i = 0; while (cache.remove(getPartName(key, i)) != null) { i++; } removePartFiles(key); } public boolean exists(String key, int part) { return getPartFile(key, part).exists(); } public boolean exists(String key) { return getPartFile(key, 0).exists(); } public File touch(File file) { cache.get(file.getName()); file.setLastModified(System.currentTimeMillis()); return file; } public FileInputStream get(String key) throws IOException { return new FileInputStream(touch(getPartFile(key, 0))); } public File getFile(String key) { return touch(getPartFile(key, 0)); } public FileInputStream[] get(String key, int count) throws IOException { FileInputStream[] ret = new FileInputStream[count]; try { for (int i = 0; i < count; i++) { ret[i] = new FileInputStream(touch(getPartFile(key, i))); } } catch (IOException e) { // if we can't get all the parts, delete everything for (FileInputStream fin: ret) { StreamUtility.closeQuietly(fin); } remove(key); throw e; } return ret; } String getPartName(String key, int part) { return key + "." + part; } public void commitTempFiles(String key, File... tempFiles) { removePartFiles(key); // try to rename everything for (int i = 0; i < tempFiles.length; i++) { File tmp = tempFiles[i]; File partFile = getPartFile(key, i); if (!tmp.renameTo(partFile)) { // if any rename fails, delete everything removeFiles(tempFiles); remove(key); return; } remove(tmp.getName()); cache.put(getPartName(key, i), new CacheEntry(partFile)); } } void removePartFiles(String key) { int i = 0; File f; while ((f = getPartFile(key, i)).exists()) { f.delete(); i++; } } File getPartFile(String key, int part) { return new File(directory, getPartName(key, part)); } long blockSize = 4096; public void setBlockSize(long blockSize) { this.blockSize = blockSize; } class InternalCache extends LruCache { public InternalCache() { super(size); } @Override protected long sizeOf(String key, CacheEntry value) { return Math.max(blockSize, value.size); } @Override protected void entryRemoved(boolean evicted, String key, CacheEntry oldValue, CacheEntry newValue) { super.entryRemoved(evicted, key, oldValue, newValue); if (newValue != null) return; if (loading) return; new File(directory, key).delete(); } } InternalCache cache; File directory; long size; Comparator dateCompare = new Comparator() { @Override public int compare(File lhs, File rhs) { long l = lhs.lastModified(); long r = rhs.lastModified(); if (l < r) return -1; if (r > l) return 1; return 0; } }; boolean loading; void load() { loading = true; try { File[] files = directory.listFiles(); if (files == null) return; ArrayList list = new ArrayList(); Collections.addAll(list, files); Collections.sort(list, dateCompare); for (File file: list) { String name = file.getName(); CacheEntry entry = new CacheEntry(file); cache.put(name, entry); cache.get(name); } } finally { loading = false; } } private void doLoad() { if (loadAsync) { new Thread() { @Override public void run() { load(); } }.start(); } else { load(); } } public FileCache(File directory, long size, boolean loadAsync) { this.directory = directory; this.size = size; this.loadAsync = loadAsync; cache = new InternalCache(); directory.mkdirs(); doLoad(); } public long size() { return cache.size(); } public void clear() { removeFiles(directory.listFiles()); cache.evictAll(); } public Set keySet() { HashSet ret = new HashSet(); File[] files = directory.listFiles(); if (files == null) return ret; for (File file: files) { String name = file.getName(); int last = name.lastIndexOf('.'); if (last != -1) ret.add(name.substring(0, last)); } return ret; } public void setMaxSize(long maxSize) { cache.setMaxSize(maxSize); doLoad(); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/util/FileUtility.java ================================================ package com.koushikdutta.async.util; import java.io.File; /** * Created by koush on 4/7/14. */ public class FileUtility { static public boolean deleteDirectory(File path) { if (path.exists()) { File[] files = path.listFiles(); if (files != null) { for (int i = 0; i < files.length; i++) { if (files[i].isDirectory()) { deleteDirectory(files[i]); } else { files[i].delete(); } } } } return (path.delete()); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/util/HashList.java ================================================ package com.koushikdutta.async.util; import java.util.ArrayList; import java.util.Hashtable; import java.util.Set; /** * Created by koush on 5/27/13. */ public class HashList { Hashtable> internal = new Hashtable>(); public HashList() { } public Set keySet() { return internal.keySet(); } public synchronized V tag(String key) { TaggedList list = internal.get(key); if (list == null) return null; return list.tag(); } public synchronized void tag(String key, V tag) { TaggedList list = internal.get(key); if (list == null) { list = new TaggedList(); internal.put(key, list); } list.tag(tag); } public synchronized ArrayList remove(String key) { return internal.remove(key); } public synchronized int size() { return internal.size(); } public synchronized ArrayList get(String key) { return internal.get(key); } synchronized public boolean contains(String key) { ArrayList check = get(key); return check != null && check.size() > 0; } synchronized public void add(String key, T value) { ArrayList ret = get(key); if (ret == null) { TaggedList put = new TaggedList(); ret = put; internal.put(key, put); } ret.add(value); } synchronized public T pop(String key) { TaggedList values = internal.get(key); if (values == null) return null; if (values.size() == 0) return null; return values.remove(values.size() - 1); } synchronized public boolean removeItem(String key, T value) { TaggedList values = internal.get(key); if (values == null) return false; values.remove(value); return values.size() == 0; } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/util/IdleTimeout.java ================================================ package com.koushikdutta.async.util; import android.os.Handler; import com.koushikdutta.async.AsyncServer; public class IdleTimeout extends TimeoutBase { Runnable callback; public IdleTimeout(AsyncServer server, long delay) { super(server, delay); } public IdleTimeout(Handler handler, long delay) { super(handler, delay); } public void setTimeout(Runnable callback) { this.callback = callback; } Object cancellable; public void reset() { handlerish.removeAllCallbacks(cancellable); cancellable = handlerish.postDelayed(callback, delay); } public void cancel() { // must post this, so that when it runs it removes everything in the queue, // preventing any rescheduling. // posting gaurantees there is not a reschedule in progress. handlerish.post(() -> handlerish.removeAllCallbacks(cancellable)); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/util/LruCache.java ================================================ /* * 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.util; import java.util.LinkedHashMap; import java.util.Locale; import java.util.Map; /** * Static library version of {@link android.util.LruCache}. Used to write apps * that run on API levels prior to 12. When running on API level 12 or above, * this implementation is still used; it does not try to switch to the * framework's implementation. See the framework SDK documentation for a class * overview. */ public class LruCache { private final LinkedHashMap map; /** Size of this cache in units. Not necessarily the number of elements. */ private long size; private long maxSize; private int putCount; private int createCount; private int evictionCount; private int hitCount; private int missCount; /** * @param maxSize for caches that do not override {@link #sizeOf}, this is * the maximum number of entries in the cache. For all other caches, * this is the maximum sum of the sizes of the entries in this cache. */ public LruCache(long maxSize) { if (maxSize <= 0) { throw new IllegalArgumentException("maxSize <= 0"); } this.maxSize = maxSize; this.map = new LinkedHashMap(0, 0.75f, true); } /** * Returns the value for {@code key} if it exists in the cache or can be * created by {@code #create}. If a value was returned, it is moved to the * head of the queue. This returns null if a value is not cached and cannot * be created. */ public final V get(K key) { if (key == null) { throw new NullPointerException("key == null"); } V mapValue; synchronized (this) { mapValue = map.get(key); if (mapValue != null) { hitCount++; return mapValue; } missCount++; } /* * Attempt to create a value. This may take a long time, and the map * may be different when create() returns. If a conflicting value was * added to the map while create() was working, we leave that value in * the map and release the created value. */ V createdValue = create(key); if (createdValue == null) { return null; } synchronized (this) { createCount++; mapValue = map.put(key, createdValue); if (mapValue != null) { // There was a conflict so undo that last put map.put(key, mapValue); } else { size += safeSizeOf(key, createdValue); } } if (mapValue != null) { entryRemoved(false, key, createdValue, mapValue); return mapValue; } else { trimToSize(maxSize); return createdValue; } } /** * Caches {@code value} for {@code key}. The value is moved to the head of * the queue. * * @return the previous value mapped by {@code key}. */ public final V put(K key, V value) { if (key == null || value == null) { throw new NullPointerException("key == null || value == null"); } V previous; synchronized (this) { putCount++; size += safeSizeOf(key, value); previous = map.put(key, value); if (previous != null) { size -= safeSizeOf(key, previous); } } if (previous != null) { entryRemoved(false, key, previous, value); } trimToSize(maxSize); return previous; } /** * @param maxSize the maximum size of the cache before returning. May be -1 * to evict even 0-sized elements. */ private void trimToSize(long maxSize) { while (true) { K key; V value; synchronized (this) { if (size < 0 || (map.isEmpty() && size != 0)) { throw new IllegalStateException(getClass().getName() + ".sizeOf() is reporting inconsistent results!"); } if (size <= maxSize || map.isEmpty()) { break; } Map.Entry toEvict = map.entrySet().iterator().next(); key = toEvict.getKey(); value = toEvict.getValue(); map.remove(key); size -= safeSizeOf(key, value); evictionCount++; } entryRemoved(true, key, value, null); } } /** * Removes the entry for {@code key} if it exists. * * @return the previous value mapped by {@code key}. */ public final V remove(K key) { if (key == null) { throw new NullPointerException("key == null"); } V previous; synchronized (this) { previous = map.remove(key); if (previous != null) { size -= safeSizeOf(key, previous); } } if (previous != null) { entryRemoved(false, key, previous, null); } return previous; } /** * Called for entries that have been evicted or removed. This method is * invoked when a value is evicted to make space, removed by a call to * {@link #remove}, or replaced by a call to {@link #put}. The default * implementation does nothing. * *

The method is called without synchronization: other threads may * access the cache while this method is executing. * * @param evicted true if the entry is being removed to make space, false * if the removal was caused by a {@link #put} or {@link #remove}. * @param newValue the new value for {@code key}, if it exists. If non-null, * this removal was caused by a {@link #put}. Otherwise it was caused by * an eviction or a {@link #remove}. */ protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {} /** * Called after a cache miss to compute a value for the corresponding key. * Returns the computed value or null if no value can be computed. The * default implementation returns null. * *

The method is called without synchronization: other threads may * access the cache while this method is executing. * *

If a value for {@code key} exists in the cache when this method * returns, the created value will be released with {@link #entryRemoved} * and discarded. This can occur when multiple threads request the same key * at the same time (causing multiple values to be created), or when one * thread calls {@link #put} while another is creating a value for the same * key. */ protected V create(K key) { return null; } private long safeSizeOf(K key, V value) { long result = sizeOf(key, value); if (result < 0) { throw new IllegalStateException("Negative size: " + key + "=" + value); } return result; } /** * Returns the size of the entry for {@code key} and {@code value} in * user-defined units. The default implementation returns 1 so that size * is the number of entries and max size is the maximum number of entries. * *

An entry's size must not change while it is in the cache. */ protected long sizeOf(K key, V value) { return 1; } /** * Clear the cache, calling {@link #entryRemoved} on each removed entry. */ public final void evictAll() { trimToSize(-1); // -1 will evict 0-sized elements } /** * For caches that do not override {@link #sizeOf}, this returns the number * of entries in the cache. For all other caches, this returns the sum of * the sizes of the entries in this cache. */ public synchronized final long size() { return size; } public void setMaxSize(long maxSize) { this.maxSize = maxSize; } /** * For caches that do not override {@link #sizeOf}, this returns the maximum * number of entries in the cache. For all other caches, this returns the * maximum sum of the sizes of the entries in this cache. */ public synchronized final long maxSize() { return maxSize; } /** * Returns the number of times {@link #get} returned a value. */ public synchronized final int hitCount() { return hitCount; } /** * Returns the number of times {@link #get} returned null or required a new * value to be created. */ public synchronized final int missCount() { return missCount; } /** * Returns the number of times {@link #create(Object)} returned a value. */ public synchronized final int createCount() { return createCount; } /** * Returns the number of times {@link #put} was called. */ public synchronized final int putCount() { return putCount; } /** * Returns the number of values that have been evicted. */ public synchronized final int evictionCount() { return evictionCount; } /** * Returns a copy of the current contents of the cache, ordered from least * recently accessed to most recently accessed. */ public synchronized final Map snapshot() { return new LinkedHashMap(map); } @Override public synchronized final String toString() { int accesses = hitCount + missCount; int hitPercent = accesses != 0 ? (100 * hitCount / accesses) : 0; return String.format(Locale.ENGLISH, "LruCache[maxSize=%d,hits=%d,misses=%d,hitRate=%d%%]", maxSize, hitCount, missCount, hitPercent); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/util/StreamUtility.java ================================================ package com.koushikdutta.async.util; import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; import java.nio.channels.WritableByteChannel; public class StreamUtility { public static void fastChannelCopy(final ReadableByteChannel src, final WritableByteChannel dest) throws IOException { final ByteBuffer buffer = ByteBuffer.allocateDirect(16 * 1024); while (src.read(buffer) != -1) { // prepare the buffer to be drained buffer.flip(); // write to the channel, may block dest.write(buffer); // If partial transfer, shift remainder down // If buffer is empty, same as doing recycle() buffer.compact(); } // EOF will leave buffer in fill state buffer.flip(); // make sure the buffer is fully drained. while (buffer.hasRemaining()) { dest.write(buffer); } } public static void copyStream(InputStream input, OutputStream output) throws IOException { final ReadableByteChannel inputChannel = Channels.newChannel(input); final WritableByteChannel outputChannel = Channels.newChannel(output); // copy the channels fastChannelCopy(inputChannel, outputChannel); } public static byte[] readToEndAsArray(InputStream input) throws IOException { DataInputStream dis = new DataInputStream(input); byte[] stuff = new byte[1024]; ByteArrayOutputStream buff = new ByteArrayOutputStream(); int read = 0; while ((read = dis.read(stuff)) != -1) { buff.write(stuff, 0, read); } dis.close(); return buff.toByteArray(); } public static String readToEnd(InputStream input) throws IOException { return new String(readToEndAsArray(input)); } static public String readFile(String filename) throws IOException { return readFile(new File(filename)); } static public String readFileSilent(String filename) { try { return readFile(new File(filename)); } catch (IOException e) { return null; } } static public String readFile(File file) throws IOException { byte[] buffer = new byte[(int) file.length()]; DataInputStream input = null; try { input = new DataInputStream(new FileInputStream(file)); input.readFully(buffer); } finally { closeQuietly(input); } return new String(buffer); } public static void writeFile(File file, String string) throws IOException { file.getParentFile().mkdirs(); DataOutputStream dout = new DataOutputStream(new FileOutputStream(file)); dout.write(string.getBytes()); dout.close(); } public static void writeFile(String file, String string) throws IOException { writeFile(new File(file), string); } public static void closeQuietly(Closeable... closeables) { if (closeables == null) return; for (Closeable closeable : closeables) { if (closeable != null) { try { closeable.close(); } catch (Exception e) { // http://stackoverflow.com/a/156525/9636 // also, catch all exceptions because some implementations throw random crap // like ArrayStoreException } } } } public static void eat(InputStream input) throws IOException { byte[] stuff = new byte[1024]; while (input.read(stuff) != -1); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/util/TaggedList.java ================================================ package com.koushikdutta.async.util; import java.util.ArrayList; public class TaggedList extends ArrayList { private Object tag; public synchronized V tag() { return (V)tag; } public synchronized void tag(V tag) { this.tag = tag; } public synchronized void tagNull(V tag) { if (this.tag == null) this.tag = tag; } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/util/ThrottleTimeout.java ================================================ package com.koushikdutta.async.util; import android.os.Handler; import com.koushikdutta.async.AsyncServer; import com.koushikdutta.async.callback.ValueCallback; import java.util.ArrayList; import java.util.List; /** * Created by koush on 7/19/16. */ public class ThrottleTimeout extends TimeoutBase { ValueCallback> callback; ArrayList values = new ArrayList<>(); ThrottleMode throttleMode = ThrottleMode.Collect; public enum ThrottleMode { /** * The timeout will keep resetting until it expires, at which point all * the collected values will be invoked on the callback. */ Collect, /** * The callback will be invoked immediately with the first, but future values will be * metered until it expires. */ Meter, } public ThrottleTimeout(final AsyncServer server, long delay, ValueCallback> callback) { super(server, delay); this.callback = callback; } public ThrottleTimeout(final Handler handler, long delay, ValueCallback> callback) { super(handler, delay); this.callback = callback; } public void setCallback(ValueCallback> callback) { this.callback = callback; } private void runCallback() { cancellable = null; ArrayList v = new ArrayList<>(values); values.clear(); callback.onResult(v); } Object cancellable; public synchronized void postThrottled(final T value) { handlerish.post(() -> { values.add(value); if (throttleMode == ThrottleMode.Collect) { // cancel the existing, schedule a new one, and wait. handlerish.removeAllCallbacks(cancellable); cancellable = handlerish.postDelayed(this::runCallback, delay); } else { // nothing is pending, so this can be fired off immediately if (cancellable == null) { runCallback(); // meter future invocations cancellable = handlerish.postDelayed(this::runCallback, delay); } } }); } public void setThrottleMode(ThrottleMode throttleMode) { this.throttleMode = throttleMode; } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/util/TimeoutBase.java ================================================ package com.koushikdutta.async.util; import android.os.Handler; import com.koushikdutta.async.AsyncServer; import com.koushikdutta.async.future.Cancellable; public class TimeoutBase { protected Handlerish handlerish; protected long delay; interface Handlerish { void post(Runnable r); Object postDelayed(Runnable r, long delay); void removeAllCallbacks(Object cancellable); } protected void onCallback() { } public TimeoutBase(final AsyncServer server, long delay) { this.delay = delay; this.handlerish = new Handlerish() { @Override public void post(Runnable r) { server.post(r); } @Override public Object postDelayed(Runnable r, long delay) { return server.postDelayed(r, delay); } @Override public void removeAllCallbacks(Object cancellable) { if (cancellable == null) return; ((Cancellable)cancellable).cancel(); } }; } public TimeoutBase(final Handler handler, long delay) { this.delay = delay; this.handlerish = new Handlerish() { @Override public void post(Runnable r) { handler.post(r); } @Override public Object postDelayed(Runnable r, long delay) { handler.postDelayed(r, delay); return r; } @Override public void removeAllCallbacks(Object cancellable) { if (cancellable == null) return; handler.removeCallbacks((Runnable)cancellable); } }; } public void setDelay(long delay) { this.delay = delay; } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/util/UntypedHashtable.java ================================================ package com.koushikdutta.async.util; import java.util.Hashtable; public class UntypedHashtable { private Hashtable hash = new Hashtable(); public void put(String key, Object value) { hash.put(key, value); } public void remove(String key) { hash.remove(key); } public T get(String key, T defaultValue) { T ret = get(key); if (ret == null) return defaultValue; return ret; } public T get(String key) { return (T)hash.get(key); } } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/wrapper/AsyncSocketWrapper.java ================================================ package com.koushikdutta.async.wrapper; import com.koushikdutta.async.AsyncSocket; public interface AsyncSocketWrapper extends AsyncSocket, DataEmitterWrapper { public AsyncSocket getSocket(); } ================================================ FILE: AndroidAsync/src/com/koushikdutta/async/wrapper/DataEmitterWrapper.java ================================================ package com.koushikdutta.async.wrapper; import com.koushikdutta.async.DataEmitter; public interface DataEmitterWrapper extends DataEmitter { public DataEmitter getDataEmitter(); } ================================================ FILE: AndroidAsync/test/assets/hello.txt ================================================ hello world ================================================ FILE: AndroidAsync/test/assets/test.json ================================================ { "foo": "bar" } ================================================ FILE: AndroidAsync/test/res/values/strings.xml ================================================ AndroidAsyncTestTest ================================================ FILE: AndroidAsync/test/src/com/koushikdutta/async/test/BodyTests.java ================================================ package com.koushikdutta.async.test; import androidx.test.runner.AndroidJUnit4; import com.koushikdutta.async.http.Multimap; import com.koushikdutta.async.http.body.UrlEncodedFormBody; import org.junit.Test; import org.junit.runner.RunWith; /** * Created by koush on 3/19/14. */ @RunWith(AndroidJUnit4.class) public class BodyTests { @Test public void testNullValue() throws Exception { Multimap mm = new Multimap(); mm.add("hello", null); UrlEncodedFormBody body = new UrlEncodedFormBody(mm); int length = body.length(); } } ================================================ FILE: AndroidAsync/test/src/com/koushikdutta/async/test/ByteUtilTests.java ================================================ package com.koushikdutta.async.test; import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.FilteredDataEmitter; import com.koushikdutta.async.PushParser; import com.koushikdutta.async.TapCallback; import com.koushikdutta.async.Util; import com.koushikdutta.async.callback.DataCallback; import junit.framework.TestCase; /** * Created by koush on 5/17/13. */ public class ByteUtilTests extends TestCase { int valRead; public void testPushParserUntil() { valRead = 0; FilteredDataEmitter mock = new FilteredDataEmitter() { @Override public boolean isPaused() { return false; } }; new PushParser(mock) .until((byte)0, new DataCallback.NullDataCallback()) .readInt(new PushParser.ParseCallback() { public void parsed(Integer arg) { valRead = arg; } }); byte[] bytes = new byte[] { 5, 5, 5, 5, 0, 10, 5, 5, 5 }; Util.emitAllData(mock, new ByteBufferList(bytes)); assertEquals(valRead, 0x0A050505); } public void testPushParserTapUntil() { valRead = 0; FilteredDataEmitter mock = new FilteredDataEmitter() { @Override public boolean isPaused() { return false; } }; new PushParser(mock) .until((byte)0, new DataCallback.NullDataCallback()) .readInt() .tap(new TapCallback() { public void parsed(int arg) { valRead = arg; } }); byte[] bytes = new byte[] { 5, 5, 5, 5, 0, 10, 5, 5, 5 }; Util.emitAllData(mock, new ByteBufferList(bytes)); assertEquals(valRead, 0x0A050505); } int readInt; byte readByte; String readString; public void testTapCallback() { readInt = 0; readByte = 0; readString = ""; FilteredDataEmitter mock = new FilteredDataEmitter() { @Override public boolean isPaused() { return false; } }; new PushParser(mock) .readInt() .readByte() .readString() .tap(new TapCallback() { void tap(int i, byte b, String s) { readInt = i; readByte = b; readString = s; } }); byte[] bytes = new byte[] { 10, 5, 5, 5, 3, 0, 0, 0, 4, 116, 101, 115, 116 }; Util.emitAllData(mock, new ByteBufferList(bytes)); assertEquals(readInt, 0x0A050505); assertEquals(readByte, (byte) 3); assertEquals(readString, "test"); } } ================================================ FILE: AndroidAsync/test/src/com/koushikdutta/async/test/CacheTests.java ================================================ package com.koushikdutta.async.test; import android.content.res.AssetManager; import androidx.test.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; import com.koushikdutta.async.AsyncServer; import com.koushikdutta.async.AsyncServerSocket; import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.FilteredDataEmitter; import com.koushikdutta.async.callback.DataCallback; import com.koushikdutta.async.http.AsyncHttpClient; import com.koushikdutta.async.http.AsyncHttpGet; import com.koushikdutta.async.http.AsyncHttpResponse; import com.koushikdutta.async.http.HttpDate; import com.koushikdutta.async.http.cache.ResponseCacheMiddleware; import com.koushikdutta.async.http.server.AsyncHttpServer; import com.koushikdutta.async.http.server.AsyncHttpServerRequest; import com.koushikdutta.async.http.server.AsyncHttpServerResponse; import com.koushikdutta.async.http.server.HttpServerRequestCallback; import org.junit.Test; import org.junit.runner.RunWith; import java.io.File; import java.nio.ByteBuffer; import java.util.Date; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import static androidx.test.InstrumentationRegistry.getContext; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; /** * Created by koush on 6/13/13. */ @RunWith(AndroidJUnit4.class) public class CacheTests { public void testMaxAgePrivate() throws Exception { AsyncHttpClient client = new AsyncHttpClient(AsyncServer.getDefault()); ResponseCacheMiddleware cache = ResponseCacheMiddleware.addCache(client, new File(getContext().getFilesDir(), "AndroidAsyncTest"), 1024 * 1024 * 10); AsyncHttpServer httpServer = new AsyncHttpServer(); try { httpServer.get("/uname/(.*)", new HttpServerRequestCallback() { @Override public void onRequest(AsyncHttpServerRequest request, AsyncHttpServerResponse response) { response.getHeaders().set("Date", HttpDate.format(new Date())); response.getHeaders().set("Cache-Control", "private, max-age=10000"); response.send(request.getMatcher().group(1)); } }); AsyncServerSocket socket = httpServer.listen(AsyncServer.getDefault(), 0); int port = socket.getLocalPort(); // clear the old cache cache.clear(); client.executeString(new AsyncHttpGet("http://localhost:" + port + "/uname/43434"), null).get(); client.executeString(new AsyncHttpGet("http://localhost:" + port + "/uname/43434"), null).get(); assertEquals(cache.getCacheHitCount(), 1); assertEquals(cache.getNetworkCount(), 1); } finally { AsyncServer.getDefault().stop(); client.getMiddleware().remove(cache); } } final static String dataNameAndHash = "6691924d7d24237d3b3679310157d640"; @Test public void test304() throws Exception { try { AsyncHttpServer httpServer = new AsyncHttpServer(); AsyncServerSocket socket = httpServer.listen(AsyncServer.getDefault(), 0); int port = socket.getLocalPort(); AssetManager am = InstrumentationRegistry.getTargetContext().getAssets(); httpServer.directory(InstrumentationRegistry.getTargetContext(), "/.*?", ""); AsyncHttpClient client = new AsyncHttpClient(AsyncServer.getDefault()); ByteBufferList bb = client.executeByteBufferList(new AsyncHttpGet("http://localhost:" + port + "/" + dataNameAndHash), new AsyncHttpClient.DownloadCallback() { @Override public void onCompleted(Exception e, AsyncHttpResponse source, ByteBufferList result) { System.out.println(source.headers()); } }) .get(); } finally { AsyncServer.getDefault().stop(); } } private static final long TIMEOUT = 1000L; public void testFilteredDataEmitter() throws Exception { final Semaphore semaphore = new Semaphore(0); FilteredDataEmitter f = new FilteredDataEmitter() { @Override public boolean isPaused() { return false; } }; f.setDataCallback(new DataCallback() { @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { assertEquals(bb.readString(), "hello"); bb.recycle(); semaphore.release(); } }); f.onDataAvailable(f, new ByteBufferList().add(ByteBuffer.wrap("hello".getBytes()))); assertTrue("timeout", semaphore.tryAcquire(TIMEOUT, TimeUnit.MILLISECONDS)); f.setDataCallback(new DataCallback() { @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { fail(); } }); f.close(); f.onDataAvailable(f, new ByteBufferList().add(ByteBuffer.wrap("hello".getBytes()))); } } ================================================ FILE: AndroidAsync/test/src/com/koushikdutta/async/test/CallbackTests.java ================================================ package com.koushikdutta.async.test; import androidx.test.runner.AndroidJUnit4; import com.koushikdutta.async.AsyncServer; import com.koushikdutta.async.AsyncServerSocket; import com.koushikdutta.async.AsyncSocket; import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.Util; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.ConnectCallback; import com.koushikdutta.async.callback.DataCallback; import com.koushikdutta.async.callback.ListenCallback; import com.koushikdutta.async.future.FutureCallback; import com.koushikdutta.async.http.AsyncHttpClient; import com.koushikdutta.async.http.AsyncHttpGet; import com.koushikdutta.async.http.server.AsyncHttpServer; import com.koushikdutta.async.http.server.AsyncHttpServerRequest; import com.koushikdutta.async.http.server.AsyncHttpServerResponse; import com.koushikdutta.async.http.server.HttpServerRequestCallback; import org.junit.Test; import org.junit.runner.RunWith; import static org.junit.Assert.fail; /** * Testing Fundamentals */ @RunWith(AndroidJUnit4.class) public class CallbackTests { @Test public void testThrow() throws Exception { int port = AsyncServer.getDefault().listen(null, 0, new ListenCallback() { @Override public void onAccepted(AsyncSocket socket) { Util.writeAll(socket, "poop".getBytes(), new CompletedCallback() { @Override public void onCompleted(Exception ex) { } }); socket.setDataCallback(new DataCallback.NullDataCallback()); } @Override public void onListening(AsyncServerSocket socket) { } @Override public void onCompleted(Exception ex) { } }).getLocalPort(); AsyncServer.getDefault().connectSocket("localhost", port, new ConnectCallback() { @Override public void onConnectCompleted(Exception ex, AsyncSocket socket) { socket.setDataCallback(new DataCallback() { @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { bb.recycle(); throw new NullPointerException("this should crash?"); } }); } }); Thread.sleep(1000000); fail(); } @Test public void testHttpServerThrow() throws Exception { AsyncHttpServer server = new AsyncHttpServer(); int port = server.listen(0).getLocalPort(); server.get("/", new HttpServerRequestCallback() { @Override public void onRequest(AsyncHttpServerRequest request, AsyncHttpServerResponse response) { AsyncHttpClient.getDefaultInstance().executeString(new AsyncHttpGet("https://google.com"), null) .setCallback(new FutureCallback() { @Override public void onCompleted(Exception e, String result) { throw new NullPointerException(); } }); } }); String result = AsyncHttpClient.getDefaultInstance().executeString(new AsyncHttpGet("http://localhost:" + port + "/"), null).get(); Thread.sleep(100000000); fail(); } } ================================================ FILE: AndroidAsync/test/src/com/koushikdutta/async/test/ConscryptTests.java ================================================ /* * Copyright 2013 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.test; import android.content.Context; import androidx.test.runner.AndroidJUnit4; import org.junit.runner.RunWith; import java.io.InputStream; import java.io.OutputStream; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.net.InetSocketAddress; import java.net.Socket; import java.nio.ByteBuffer; import java.nio.charset.Charset; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLEngineResult; import static androidx.test.InstrumentationRegistry.getContext; import static org.junit.Assert.assertEquals; /** * Created by koush on 7/15/14. */ @RunWith(AndroidJUnit4.class) public class ConscryptTests { boolean initialized; Field peerHost; Field peerPort; Field sslParameters; Field npnProtocols; Field alpnProtocols; Field sslNativePointer; Field useSni; Method nativeGetNpnNegotiatedProtocol; Method nativeGetAlpnNegotiatedProtocol; private void configure(SSLEngine engine, String host, int port) throws Exception { if (!initialized) { initialized = true; peerHost = engine.getClass().getSuperclass().getDeclaredField("peerHost"); peerPort = engine.getClass().getSuperclass().getDeclaredField("peerPort"); sslParameters = engine.getClass().getDeclaredField("sslParameters"); npnProtocols = sslParameters.getType().getDeclaredField("npnProtocols"); alpnProtocols = sslParameters.getType().getDeclaredField("alpnProtocols"); useSni = sslParameters.getType().getDeclaredField("useSni"); sslNativePointer = engine.getClass().getDeclaredField("sslNativePointer"); String nativeCryptoName = sslParameters.getType().getPackage().getName() + ".NativeCrypto"; nativeGetNpnNegotiatedProtocol = Class.forName(nativeCryptoName, true, sslParameters.getType().getClassLoader()) .getDeclaredMethod("SSL_get_npn_negotiated_protocol", long.class); nativeGetAlpnNegotiatedProtocol = Class.forName(nativeCryptoName, true, sslParameters.getType().getClassLoader()) .getDeclaredMethod("SSL_get0_alpn_selected", long.class); peerHost.setAccessible(true); peerPort.setAccessible(true); sslParameters.setAccessible(true); npnProtocols.setAccessible(true); alpnProtocols.setAccessible(true); useSni.setAccessible(true); sslNativePointer.setAccessible(true); nativeGetNpnNegotiatedProtocol.setAccessible(true); nativeGetAlpnNegotiatedProtocol.setAccessible(true); } byte[] protocols = concatLengthPrefixed( "http/1.1", "spdy/3.1" ); peerHost.set(engine, host); peerPort.set(engine, port); Object sslp = sslParameters.get(engine); // npnProtocols.set(sslp, protocols); alpnProtocols.set(sslp, protocols); useSni.set(sslp, true); } static byte[] concatLengthPrefixed(String... protocols) { ByteBuffer result = ByteBuffer.allocate(8192); for (String protocol: protocols) { result.put((byte) protocol.toString().length()); result.put(protocol.toString().getBytes(Charset.forName("UTF-8"))); } result.flip(); byte[] ret = new byte[result.remaining()]; result.get(ret); return ret; } public void testConscryptSSLEngineNPNHandshakeBug() throws Exception { // Security.insertProviderAt(new OpenSSLProvider("MyNameBlah"), 1); Context gms = getContext().createPackageContext("com.google.android.gms", Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY); gms .getClassLoader() .loadClass("com.google.android.gms.common.security.ProviderInstallerImpl") .getMethod("insertProvider", Context.class) .invoke(null, getContext()); SSLContext ctx = SSLContext.getInstance("TLS"); ctx.init(null, null, null); SSLEngine engine = ctx.createSSLEngine(); configure(engine, "www.google.com", 443); engine.setUseClientMode(true); engine.beginHandshake(); Socket socket = new Socket(); socket.connect(new InetSocketAddress("www.google.com", 443)); InputStream is = socket.getInputStream(); OutputStream os = socket.getOutputStream(); byte[] buf = new byte[65536]; ByteBuffer unwrap = null; ByteBuffer dummy = ByteBuffer.allocate(65536); SSLEngineResult.HandshakeStatus handshakeStatus = engine.getHandshakeStatus(); while (handshakeStatus != SSLEngineResult.HandshakeStatus.FINISHED && handshakeStatus != SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING) { if (handshakeStatus == SSLEngineResult.HandshakeStatus.NEED_UNWRAP) { System.out.println("waiting for read... " + engine.getHandshakeStatus()); int read = is.read(buf); System.out.println("read: " + read); if (read <= 0) throw new Exception("closed!"); if (unwrap != null) { int bufLen = unwrap.remaining() + read; ByteBuffer b = ByteBuffer.allocate(bufLen); b.put(unwrap); b.put(buf, 0, read); b.flip(); unwrap = b; } else { unwrap = ByteBuffer.wrap(buf, 0, read); } if (!unwrap.hasRemaining()) { unwrap = null; } dummy.clear(); SSLEngineResult res = engine.unwrap(unwrap, dummy); System.out.println("data remaining after unwrap: " + unwrap.remaining()); handshakeStatus = res.getHandshakeStatus(); } if (handshakeStatus == SSLEngineResult.HandshakeStatus.NEED_WRAP) { dummy.clear(); SSLEngineResult res = engine.wrap(ByteBuffer.allocate(0), dummy); handshakeStatus = res.getHandshakeStatus(); dummy.flip(); if (dummy.hasRemaining()) { os.write(dummy.array(), 0, dummy.remaining()); } } else if (handshakeStatus == SSLEngineResult.HandshakeStatus.NEED_TASK) { engine.getDelegatedTask().run(); } } System.out.println("Done handshaking! Thank you come again."); long ptr = (Long)sslNativePointer.get(engine); byte[] proto = (byte[]) nativeGetAlpnNegotiatedProtocol.invoke(null, ptr); // byte[] proto = (byte[]) nativeGetNpnNegotiatedProtocol.invoke(null, ptr); String protoString = new String(proto); System.out.println("negotiated protocol was: " + protoString); assertEquals(protoString, "spdy/3.1"); socket.close(); } } ================================================ FILE: AndroidAsync/test/src/com/koushikdutta/async/test/ConvertTests.java ================================================ package com.koushikdutta.async.test; import androidx.test.runner.AndroidJUnit4; import com.koushikdutta.async.future.SimpleFuture; import org.json.JSONObject; import org.junit.Test; import org.junit.runner.RunWith; import java.nio.ByteBuffer; import static com.koushikdutta.async.future.Converter.convert; import static org.junit.Assert.assertEquals; @RunWith(AndroidJUnit4.class) public class ConvertTests { @Test public void testConvert() throws Exception { ByteBuffer buf = convert(new SimpleFuture<>(new JSONObject())) .to(ByteBuffer.class) .get(); assertEquals(buf.remaining(), 2); } } ================================================ FILE: AndroidAsync/test/src/com/koushikdutta/async/test/DnsTests.java ================================================ package com.koushikdutta.async.test; import com.koushikdutta.async.AsyncDatagramSocket; import com.koushikdutta.async.AsyncServer; import com.koushikdutta.async.AsyncSocket; import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.callback.ConnectCallback; import com.koushikdutta.async.callback.DataCallback; import com.koushikdutta.async.dns.Dns; import com.koushikdutta.async.dns.DnsResponse; import com.koushikdutta.async.future.FutureCallback; import junit.framework.TestCase; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.Inet4Address; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.MulticastSocket; import java.net.UnknownHostException; import java.nio.channels.DatagramChannel; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; /** * Created by koush on 10/20/13. */ public class DnsTests extends TestCase { public void testLookup() throws Exception { // final Semaphore semaphore = new Semaphore(0); // Dns.lookup("google.com") // .setCallback(new FutureCallback() { // @Override // public void onCompleted(Exception e, DnsResponse result) { // semaphore.release(); // } // }); // // semaphore.tryAcquire(1000000, TimeUnit.MILLISECONDS); } public void testMulticastLookup() throws Exception { // MulticastSocket socket = new MulticastSocket(5353); // socket.joinGroup(InetAddress.getByName("224.0.0.251")); // DatagramChannel channel = socket.getChannel(); // assertNotNull(channel); // while (true) { // DatagramPacket packet = new DatagramPacket(new byte[2048], 2048); // socket.receive(packet); // System.out.println(new String(packet.getData())); // } // AsyncDatagramSocket dgram = AsyncServer.getDefault().openDatagram(new InetSocketAddress(5353), true); // ((DatagramSocket)dgram.getSocket()).setReuseAddress(true); // dgram.setDataCallback(new DataCallback() { // @Override // public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { // System.out.println(bb.readString()); // } // }); // ((DatagramSocket)dgram.getSocket()).setBroadcast(true); // final Semaphore semaphore = new Semaphore(0); // Dns.multicastLookup("_airplay._tcp.local", new FutureCallback() { // @Override // public void onCompleted(Exception e, DnsResponse result) { //// semaphore.release(); // } // }); // // semaphore.tryAcquire(1000000, TimeUnit.MILLISECONDS); } public void testNoDomain() throws Exception { AsyncServer server = new AsyncServer(); try { final Semaphore semaphore = new Semaphore(0); server.connectSocket("www.clockworkmod-notfound.com", 8080, new ConnectCallback() { @Override public void onConnectCompleted(Exception ex, AsyncSocket socket) { assertTrue(ex instanceof UnknownHostException); semaphore.release(); } }); assertTrue(semaphore.tryAcquire(5000, TimeUnit.MILLISECONDS)); } finally { server.stop(); } } } ================================================ FILE: AndroidAsync/test/src/com/koushikdutta/async/test/FileCacheTests.java ================================================ package com.koushikdutta.async.test; import androidx.test.runner.AndroidJUnit4; import com.koushikdutta.async.util.FileCache; import com.koushikdutta.async.util.StreamUtility; import org.junit.runner.RunWith; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import static androidx.test.InstrumentationRegistry.getContext; import static junit.framework.TestCase.assertFalse; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; /** * Created by koush on 4/13/14. */ @RunWith(AndroidJUnit4.class) public class FileCacheTests { protected void setUp() throws Exception { File dir = new File(getContext().getCacheDir(), "filecache"); File[] files = dir.listFiles(); if (files == null) return; for (File f: files) f.delete(); } public void testSimple() throws Exception { setUp(); FileCache cache = new FileCache(new File(getContext().getCacheDir(), "filecache"), 100000, false); cache.setBlockSize(1); File temp = cache.getTempFile(); StreamUtility.writeFile(temp, "hello"); cache.commitTempFiles("test", temp); String value = StreamUtility.readToEnd(cache.get("test")); assertEquals(value, "hello"); } public void testEviction() throws Exception { setUp(); FileCache cache = new FileCache(new File(getContext().getCacheDir(), "filecache"), 25, false); cache.setBlockSize(1); for (int i = 0; i < 10; i++) { File temp = cache.getTempFile(); StreamUtility.writeFile(temp, "hello"); cache.commitTempFiles("test" + i, temp); String value = StreamUtility.readToEnd(cache.get("test" + i)); assertEquals(value, "hello"); } assertEquals(cache.size(), 25); File dir = new File(getContext().getCacheDir(), "filecache"); File[] files = dir.listFiles(); assertEquals(files.length, 5); for (int i = 5; i < 10; i++) { assertTrue(cache.exists("test" + i)); } } public void testMultipleParts() throws Exception { setUp(); FileCache cache = new FileCache(new File(getContext().getCacheDir(), "filecache"), 100000, false); cache.setBlockSize(1); File[] temps = new File[10]; for (int i = 0; i < temps.length; i++) { File temp = temps[i] = cache.getTempFile(); StreamUtility.writeFile(temp, "hello" + i); } cache.commitTempFiles("test", temps); assertEquals(cache.size(), temps.length * 6); File dir = new File(getContext().getCacheDir(), "filecache"); File[] files = dir.listFiles(); assertEquals(files.length, temps.length); for (int i = 5; i < 10; i++) { assertTrue(cache.exists("test", i)); } } public void testMultipartEviction() throws Exception { setUp(); FileCache cache = new FileCache(new File(getContext().getCacheDir(), "filecache"), 12, false); cache.setBlockSize(1); File[] temps = new File[10]; for (int i = 0; i < temps.length; i++) { File temp = temps[i] = cache.getTempFile(); StreamUtility.writeFile(temp, "hello" + i); } cache.commitTempFiles("test", temps); assertEquals(cache.size(), 12); File dir = new File(getContext().getCacheDir(), "filecache"); File[] files = dir.listFiles(); assertEquals(files.length, 2); for (int i = 8; i < 10; i++) { assertTrue(cache.exists("test", i)); } try { FileInputStream[] fins = cache.get("test", temps.length); fail(); } catch (IOException e) { } } public void testMultipartEvictionAgain() throws Exception { setUp(); FileCache cache = new FileCache(new File(getContext().getCacheDir(), "filecache"), 72, false); cache.setBlockSize(1); File[] temps = new File[10]; for (int i = 0; i < temps.length; i++) { File temp = temps[i] = cache.getTempFile(); StreamUtility.writeFile(temp, "hello" + i); } cache.commitTempFiles("test", temps); assertEquals(cache.size(), 60); File dir = new File(getContext().getCacheDir(), "filecache"); File[] files = dir.listFiles(); assertEquals(files.length, 10); for (int i = 0; i < temps.length; i++) { assertTrue(cache.exists("test", i)); } FileInputStream[] fins = cache.get("test", temps.length); StreamUtility.closeQuietly(fins); temps = new File[10]; for (int i = 0; i < temps.length; i++) { File temp = temps[i] = cache.getTempFile(); StreamUtility.writeFile(temp, "hello" + i); } cache.commitTempFiles("test2", temps); assertEquals(cache.size(), 72); fins = cache.get("test2", temps.length); StreamUtility.closeQuietly(fins); try { fins = cache.get("test", temps.length); fail(); } catch (IOException e) { } } public void testReinit() throws Exception { setUp(); FileCache cache = new FileCache(new File(getContext().getCacheDir(), "filecache"), 10, false); cache.setBlockSize(1); File temp = cache.getTempFile(); StreamUtility.writeFile(temp, "hello"); cache.commitTempFiles("test", temp); temp = cache.getTempFile(); StreamUtility.writeFile(temp, "hello"); cache.commitTempFiles("test2", temp); assertEquals(cache.size(), 10); cache = new FileCache(new File(getContext().getCacheDir(), "filecache"), 10, false); cache.setBlockSize(1); String value = StreamUtility.readToEnd(cache.get("test")); assertEquals(value, "hello"); value = StreamUtility.readToEnd(cache.get("test2")); assertEquals(value, "hello"); } public void testCacheOrder() throws Exception { setUp(); FileCache cache = new FileCache(new File(getContext().getCacheDir(), "filecache"), 10, false); cache.setBlockSize(1); File temp = cache.getTempFile(); StreamUtility.writeFile(temp, "hello"); cache.commitTempFiles("test", temp); temp = cache.getTempFile(); StreamUtility.writeFile(temp, "hello"); cache.commitTempFiles("test2", temp); assertEquals(cache.size(), 10); String value = StreamUtility.readToEnd(cache.get("test")); assertEquals(value, "hello"); // should push test2 off temp = cache.getTempFile(); StreamUtility.writeFile(temp, "hello"); cache.commitTempFiles("test3", temp); assertTrue(cache.exists("test")); assertFalse(cache.exists("test2")); assertTrue(cache.exists("test3")); } } ================================================ FILE: AndroidAsync/test/src/com/koushikdutta/async/test/FileTests.java ================================================ package com.koushikdutta.async.test; import androidx.test.runner.AndroidJUnit4; import com.koushikdutta.async.AsyncServer; import com.koushikdutta.async.FileDataEmitter; import com.koushikdutta.async.future.Future; import com.koushikdutta.async.future.FutureCallback; import com.koushikdutta.async.parser.StringParser; import com.koushikdutta.async.util.StreamUtility; import org.junit.runner.RunWith; import java.io.File; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import static androidx.test.InstrumentationRegistry.getContext; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; /** * Created by koush on 5/22/13. */ @RunWith(AndroidJUnit4.class) public class FileTests { public static final long TIMEOUT = 1000L; public void testFileDataEmitter() throws Exception { final Semaphore semaphore = new Semaphore(0); File f = getContext().getFileStreamPath("test.txt"); StreamUtility.writeFile(f, "hello world"); FileDataEmitter fdm = new FileDataEmitter(AsyncServer.getDefault(), f); final Md5 md5 = Md5.createInstance(); Future stringBody = new StringParser().parse(fdm); stringBody .setCallback(new FutureCallback() { @Override public void onCompleted(Exception e, String result) { semaphore.release(); } }); fdm.resume(); assertTrue("timeout", semaphore.tryAcquire(TIMEOUT, TimeUnit.MILLISECONDS)); assertEquals("hello world", stringBody.get()); } } ================================================ FILE: AndroidAsync/test/src/com/koushikdutta/async/test/FutureTests.java ================================================ package com.koushikdutta.async.test; import android.os.Handler; import android.os.Looper; import androidx.test.runner.AndroidJUnit4; import com.koushikdutta.async.AsyncServer; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.ContinuationCallback; import com.koushikdutta.async.future.Continuation; import com.koushikdutta.async.future.FutureCallback; import com.koushikdutta.async.future.MultiFuture; import com.koushikdutta.async.future.SimpleFuture; import com.koushikdutta.async.future.SuccessCallback; import com.koushikdutta.async.future.ThenCallback; import junit.framework.TestCase; import org.junit.Test; import org.junit.runner.RunWith; import java.util.ArrayList; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @RunWith(AndroidJUnit4.class) public class FutureTests extends TestCase { @Test public void testChain() throws Exception { SimpleFuture foo = new SimpleFuture<>(); foo .thenConvert(new ThenCallback() { @Override public Integer then(Integer from) { return null; } }) .thenConvert(new ThenCallback() { @Override public Integer then(Integer from) { return null; } }) .thenConvert(new ThenCallback() { @Override public Integer then(Integer from) { return null; } }) .thenConvert(new ThenCallback() { @Override public Integer then(Integer from) { return null; } }); foo.setComplete(3); } int sum = 0; @Test public void multifutureTest() throws Exception { MultiFuture foo = new MultiFuture<>(); foo.success(value -> sum += value + 10); foo.success(value -> sum += value + 20); foo.setComplete(1); assertEquals(sum, 32); } private static class IntegerFuture extends SimpleFuture { private IntegerFuture() { } public static IntegerFuture create(final int value, final long timeout) { final IntegerFuture ret = new IntegerFuture(); new Thread() { public void run() { try { Thread.sleep(timeout); ret.setComplete(value); } catch (Exception e) { ret.setComplete(e); } }; }.start(); return ret; } } @Test public void testFutureCallback() throws Exception { final Semaphore semaphore = new Semaphore(0); final IntegerFuture future = IntegerFuture.create(20, 1000); final Thread mainThread = Thread.currentThread(); future.setCallback(new FutureCallback() { @Override public void onCompleted(Exception e, Integer result) { assertNotSame(Thread.currentThread(), mainThread); semaphore.release(); } }); assertTrue(semaphore.tryAcquire(3000, TimeUnit.MILLISECONDS)); } @Test public void testFutureFinishedCallback() throws Exception { final Semaphore semaphore = new Semaphore(0); final IntegerFuture future = IntegerFuture.create(20, 1); Thread.sleep(1000); final Thread mainThread = Thread.currentThread(); future.setCallback(new FutureCallback() { @Override public void onCompleted(Exception e, Integer result) { assertEquals(Thread.currentThread(), mainThread); semaphore.release(); } }); assertTrue(semaphore.tryAcquire(3000, TimeUnit.MILLISECONDS)); } @Test public void testFutureCancel() throws Exception { // test a future being cancelled while waiting final IntegerFuture future = IntegerFuture.create(20, 2000); new Thread() { public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { } future.cancel(); }; } .start(); try { future.get(3000, TimeUnit.MILLISECONDS); // this should never reach here as it was cancelled fail(); } catch (TimeoutException e) { // timeout should also fail, since it was cancelled fail(); } catch (ExecutionException e) { // execution exception is correct, make sure inner exception is cancellation assertTrue(e.getCause() instanceof CancellationException); } } @Test public void testIntegerFuture() throws Exception { IntegerFuture i = IntegerFuture.create(10, 500L); assertEquals((int)i.get(), 10); } int someValue; @Test public void testContinuation() throws Exception { final Semaphore semaphore = new Semaphore(0); someValue = 0; final Continuation c = new Continuation(new CompletedCallback() { @Override public void onCompleted(Exception ex) { assertNull(ex); semaphore.release(); } }); c.add(new ContinuationCallback() { @Override public void onContinue(Continuation continuation, final CompletedCallback next) throws Exception { new Thread() { public void run() { someValue++; next.onCompleted(null); }; }.start(); } }); c.add(new ContinuationCallback() { @Override public void onContinue(Continuation continuation, final CompletedCallback next) throws Exception { new Thread() { public void run() { someValue++; next.onCompleted(null); }; }.start(); } }); c.add(new ContinuationCallback() { @Override public void onContinue(Continuation continuation, final CompletedCallback next) throws Exception { someValue++; next.onCompleted(null); } }); new Thread() { public void run() { c.start(); }; }.start(); assertTrue(semaphore.tryAcquire(3000, TimeUnit.MILLISECONDS)); assertEquals(someValue, 3); } @Test public void testFutureChain() throws Exception { final Semaphore semaphore = new Semaphore(0); final Continuation c = new Continuation(new CompletedCallback() { @Override public void onCompleted(Exception ex) { semaphore.release(); } }); IntegerFuture i1; c.add(i1 = IntegerFuture.create(2, 200)); IntegerFuture i2; c.add(i2 = IntegerFuture.create(3, 200)); new Thread() { public void run() { c.start(); }; }.start(); assertTrue(semaphore.tryAcquire(3000, TimeUnit.MILLISECONDS)); assertEquals((int)i1.get(), 2); assertEquals((int)i2.get(), 3); } @Test public void testContinuationFail() throws Exception { final Semaphore semaphore = new Semaphore(0); final Continuation c = new Continuation(new CompletedCallback() { @Override public void onCompleted(Exception ex) { assertNotNull(ex); semaphore.release(); } }); c.add(new ContinuationCallback() { @Override public void onContinue(Continuation continuation, CompletedCallback next) throws Exception { throw new Exception("fail"); } }); new Thread() { public void run() { c.start(); }; }.start(); assertTrue(semaphore.tryAcquire(3000, TimeUnit.MILLISECONDS)); } @Test public void testContinuationCancel() throws Exception { final Semaphore semaphore = new Semaphore(0); final Continuation c = new Continuation(new CompletedCallback() { @Override public void onCompleted(Exception ex) { fail(); semaphore.release(); } }); c.setCancelCallback(new Runnable() { @Override public void run() { semaphore.release(); } }); c.add(new ContinuationCallback() { @Override public void onContinue(Continuation continuation, CompletedCallback next) throws Exception { Thread.sleep(10000); } }); new Thread() { public void run() { c.start(); }; }.start(); new Thread() { public void run() { try { Thread.sleep(1000); } catch (Exception e) { } c.cancel(); }; }.start(); assertTrue(semaphore.tryAcquire(3000, TimeUnit.MILLISECONDS)); } @Test public void testChildContinuationCancel() throws Exception { final Semaphore semaphore = new Semaphore(0); final Continuation c = new Continuation(new CompletedCallback() { @Override public void onCompleted(Exception ex) { fail(); semaphore.release(); } }); c.setCancelCallback(new Runnable() { @Override public void run() { semaphore.release(); } }); c.add(new ContinuationCallback() { @Override public void onContinue(Continuation continuation, CompletedCallback next) throws Exception { Thread.sleep(10000); } }); final Continuation child = new Continuation(); child.add(new ContinuationCallback() { @Override public void onContinue(Continuation continuation, CompletedCallback next) throws Exception { Thread.sleep(10000); } }); c.add(child); new Thread() { public void run() { c.start(); }; }.start(); new Thread() { public void run() { try { Thread.sleep(1000); } catch (Exception e) { } child.cancel(); }; }.start(); assertTrue(semaphore.tryAcquire(3000, TimeUnit.MILLISECONDS)); } public void testContinuationArray() throws Exception { final ArrayList results = new ArrayList(); final Semaphore semaphore = new Semaphore(0); final Continuation c = new Continuation(new CompletedCallback() { @Override public void onCompleted(Exception ex) { semaphore.release(); } }); for (int i = 0; i < 10; i++) { final int j = i; c.add(new ContinuationCallback() { @Override public void onContinue(Continuation continuation, CompletedCallback next) throws Exception { results.add(j); next.onCompleted(null); } }); } new Thread() { public void run() { c.start(); }; }.start(); assertTrue(semaphore.tryAcquire(3000, TimeUnit.MILLISECONDS)); assertEquals(10, results.size()); for (int i = 0; i < 10; i++) { assertEquals((int)results.get(i), i); } } @Test public void testReentrancy() throws Exception { if (true) { // disabled cause test framework no longer has a looper return; } // verify reentrancy will work assertNotNull(Looper.myLooper()); final Thread originalThread = Thread.currentThread(); final TriggerFuture trigger = new TriggerFuture(); final Handler handler = new Handler(); AsyncServer.getDefault().post(new Runnable() { @Override public void run() { AsyncServer.post(handler, new Runnable() { @Override public void run() { final TriggerFuture trigger2 = new TriggerFuture(); AsyncServer.getDefault().post(new Runnable() { @Override public void run() { AsyncServer.post(handler, new Runnable() { @Override public void run() { assertEquals(Thread.currentThread(), originalThread); trigger2.trigger(); } }); } }); try { assertEquals((int)trigger2.get(5000, TimeUnit.MILLISECONDS), 2020); } catch (Exception e) { fail(); } // callstack here should be on top of trigger.get below. // reentrant. assertEquals(Thread.currentThread(), originalThread); trigger.trigger(); } }); } }); // trigger.get will do a reentrant block. assertEquals((int)trigger.get(5000, TimeUnit.MILLISECONDS), 2020); } @Test public void testPostCancelCallback() throws Exception { SimpleFuture future = new SimpleFuture(); final Semaphore semaphore = new Semaphore(0); future.cancel(); future.setCallback(new FutureCallback() { @Override public void onCompleted(Exception e, String result) { assertTrue(e instanceof CancellationException); semaphore.release(); } }); assertTrue(semaphore.tryAcquire(1000, TimeUnit.MILLISECONDS)); assertNull(future.getCallback()); } @Test public void testPreCancelCallback() throws Exception { final Semaphore semaphore = new Semaphore(0); SimpleFuture future = new SimpleFuture(); future.setCallback(new FutureCallback() { @Override public void onCompleted(Exception e, String result) { assertTrue(e instanceof CancellationException); semaphore.release(); } }); assertNotNull(future.getCallback()); future.cancel(); assertTrue(semaphore.tryAcquire(1000, TimeUnit.MILLISECONDS)); assertNull(future.getCallback()); } } ================================================ FILE: AndroidAsync/test/src/com/koushikdutta/async/test/HttpClientTests.java ================================================ package com.koushikdutta.async.test; import android.net.Uri; import androidx.test.runner.AndroidJUnit4; import android.text.TextUtils; import android.util.Log; import com.koushikdutta.async.AsyncServer; import com.koushikdutta.async.AsyncServerSocket; import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.DataCallback; import com.koushikdutta.async.future.Future; import com.koushikdutta.async.future.FutureCallback; import com.koushikdutta.async.http.AsyncHttpClient; import com.koushikdutta.async.http.AsyncHttpClient.StringCallback; import com.koushikdutta.async.http.AsyncHttpGet; import com.koushikdutta.async.http.AsyncHttpHead; import com.koushikdutta.async.http.AsyncHttpPost; import com.koushikdutta.async.http.AsyncHttpRequest; import com.koushikdutta.async.http.AsyncHttpResponse; import com.koushikdutta.async.http.body.JSONObjectBody; import com.koushikdutta.async.http.cache.ResponseCacheMiddleware; import com.koushikdutta.async.http.callback.HttpConnectCallback; import com.koushikdutta.async.http.server.AsyncHttpServerRequest; import com.koushikdutta.async.http.server.AsyncHttpServerResponse; import com.koushikdutta.async.http.server.AsyncProxyServer; import org.json.JSONObject; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import java.io.File; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import static androidx.test.InstrumentationRegistry.getContext; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @RunWith(AndroidJUnit4.class) public class HttpClientTests { AsyncServer server = new AsyncServer(); AsyncHttpClient client = new AsyncHttpClient(server); @Override protected void finalize() throws Throwable { super.finalize(); client.getSSLSocketMiddleware().setConnectAllAddresses(false); client.getSocketMiddleware().setConnectAllAddresses(false); client.getSocketMiddleware().disableProxy(); server.stop(); } /* public void testConnectAllAddresses() throws Exception { assertEquals(client.getSSLSocketMiddleware().getConnectionPoolCount(), 0); assertEquals(client.getSocketMiddleware().getConnectionPoolCount(), 0); client.getSSLSocketMiddleware().setConnectAllAddresses(true); client.getSocketMiddleware().setConnectAllAddresses(true); final Semaphore semaphore = new Semaphore(0); final Md5 md5 = Md5.createInstance(); AsyncHttpGet get = new AsyncHttpGet("http://www.clockworkmod.com"); get.setLogging("ConnectionPool", Log.VERBOSE); client.execute(get, new HttpConnectCallback() { @Override public void onConnectCompleted(Exception ex, AsyncHttpResponse response) { // make sure gzip decoding works, as that is generally what github sends. Assert.assertEquals("gzip", response.getHeaders().getContentEncoding()); response.setDataCallback(new DataCallback() { @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { md5.update(bb); } }); response.setEndCallback(new CompletedCallback() { @Override public void onCompleted(Exception ex) { semaphore.release(); } }); } }); assertTrue("timeout", semaphore.tryAcquire(TIMEOUT, TimeUnit.MILLISECONDS)); long start = System.currentTimeMillis(); while (client.getSocketMiddleware().getConnectionPoolCount() != 2) { Thread.sleep(50); if (start + 5000L < System.currentTimeMillis()) fail(); } } */ private static final long TIMEOUT = 10000L; @Test public void testHomepage() throws Exception { Future ret = client.executeString(new AsyncHttpGet("http://google.com"), null); assertNotNull(ret.get(TIMEOUT, TimeUnit.MILLISECONDS)); } @Test public void testClockworkMod() throws Exception { final Semaphore semaphore = new Semaphore(0); final Md5 md5 = Md5.createInstance(); client.execute("http://www.clockworkmod.com", new HttpConnectCallback() { @Override public void onConnectCompleted(Exception ex, AsyncHttpResponse response) { // make sure gzip decoding works, as that is generally what github sends. Assert.assertEquals("gzip", response.headers().get("Content-Encoding")); response.setDataCallback(new DataCallback() { @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { md5.update(bb); } }); response.setEndCallback(new CompletedCallback() { @Override public void onCompleted(Exception ex) { semaphore.release(); } }); } }); assertTrue("timeout", semaphore.tryAcquire(TIMEOUT, TimeUnit.MILLISECONDS)); } // this testdata file was generated using /dev/random. filename is also the md5 of the file. final static String dataNameAndHash = "6691924d7d24237d3b3679310157d640"; final static String githubPath = "raw.githubusercontent.com/koush/AndroidAsync/master/AndroidAsync/test/assets/"; final static String github = "https://" + githubPath + dataNameAndHash; public void testGithubRandomData() throws Exception { final Semaphore semaphore = new Semaphore(0); final Md5 md5 = Md5.createInstance(); AsyncHttpGet get = new AsyncHttpGet(github); get.setLogging("AsyncTest", Log.VERBOSE); client.execute(get, new HttpConnectCallback() { @Override public void onConnectCompleted(Exception ex, AsyncHttpResponse response) { assertNull(ex); // make sure gzip decoding works, as that is generally what github sends. // this broke sometime in 03/2014 // Assert.assertEquals("gzip", response.getHeaders().getContentEncoding()); response.setDataCallback(new DataCallback() { @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { md5.update(bb); } }); response.setEndCallback(new CompletedCallback() { @Override public void onCompleted(Exception ex) { semaphore.release(); } }); } }); assertTrue("timeout", semaphore.tryAcquire(TIMEOUT, TimeUnit.MILLISECONDS)); assertEquals(md5.digest(), dataNameAndHash); } public void testGithubRandomDataWithFuture() throws Exception { final Md5 md5 = Md5.createInstance(); Future bb = client.executeByteBufferList(new AsyncHttpGet(github), null); md5.update(bb.get(TIMEOUT, TimeUnit.MILLISECONDS)); assertEquals(md5.digest(), dataNameAndHash); } public void testSni() throws Exception { // ProviderInstaller.installIfNeeded(getContext()); // AsyncHttpClient.getDefaultInstance().getSSLSocketMiddleware().setSSLContext(SSLContext.getInstance("TLS")); // this server requires SNI as it serves multiple SSL certificates // LOLLIPOP_MR1 and lower requires SSLEngineSNIConfigurator to set the appropriate fields via reflection. // Higher than LOLLIPOP_MR1 can use createSSLEngine(host, port) as it is based off recent-ish versions of Conscrypt // Conscrypt, if it is being used in GPS ProviderInstaller, can also use createSSLEngine(host, port) Future string = client.executeString(new AsyncHttpGet("https://koush.com/"), null); string.get(TIMEOUT, TimeUnit.MILLISECONDS); } public void testGithubHelloWithFuture() throws Exception { Future string = client.executeString(new AsyncHttpGet("https://" + githubPath + "hello.txt"), null); assertEquals(string.get(TIMEOUT, TimeUnit.MILLISECONDS), "hello world"); } public void testGithubHelloWithFutureCallback() throws Exception { final Semaphore semaphore = new Semaphore(0); client.executeString(new AsyncHttpGet("https://" + githubPath + "hello.txt"), null) .setCallback(new FutureCallback() { @Override public void onCompleted(Exception e, String result) { assertEquals(result, "hello world"); semaphore.release(); } }); assertTrue("timeout", semaphore.tryAcquire(TIMEOUT, TimeUnit.MILLISECONDS)); } Future future; public void testCancel() throws Exception { future = client.executeString(new AsyncHttpGet("http://yahoo.com"), new StringCallback() { @Override public void onCompleted(Exception e, AsyncHttpResponse source, String result) { fail(); } @Override public void onConnect(AsyncHttpResponse response) { future.cancel(); } }); try { future.get(TIMEOUT, TimeUnit.MILLISECONDS); // this should never reach here as it was cancelled fail(); } catch (TimeoutException e) { // timeout should also fail, since it was cancelled fail(); } catch (ExecutionException e) { // execution exception is correct, make sure inner exception is cancellation assertTrue(e.getCause() instanceof CancellationException); } } public void testCache() throws Exception { ResponseCacheMiddleware cache = ResponseCacheMiddleware.addCache(client, new File(getContext().getFilesDir(), "AndroidAsyncTest"), 1024 * 1024 * 10); try { // clear the old cache cache.clear(); // populate the cache testGithubRandomData(); // this should result in a conditional cache hit testGithubRandomData(); assertEquals(cache.getCacheHitCount(), 1); } finally { client.getMiddleware().remove(cache); } } Future fileFuture; public void testFileCancel() throws Exception { final Semaphore semaphore = new Semaphore(0); File f = getContext().getFileStreamPath("test.txt"); fileFuture = client.executeFile(new AsyncHttpGet(github), f.getAbsolutePath(), new AsyncHttpClient.FileCallback() { @Override public void onCompleted(Exception e, AsyncHttpResponse source, File result) { fail(); } @Override public void onProgress(AsyncHttpResponse response, long downloaded, long total) { semaphore.release(); } }); fileFuture.setCallback(new FutureCallback() { @Override public void onCompleted(Exception e, File result) { assertTrue(e instanceof CancellationException); } }); try { assertTrue("timeout", semaphore.tryAcquire(TIMEOUT, TimeUnit.MILLISECONDS)); assertTrue(fileFuture.cancel()); fileFuture.get(); fail(); } catch (ExecutionException ex) { assertTrue(ex.getCause() instanceof CancellationException); } // Thread.sleep(1000); // assertTrue("timeout", semaphore.tryAcquire(TIMEOUT, TimeUnit.MILLISECONDS)); assertFalse(f.exists()); } boolean wasProxied; public void testProxy() throws Exception { wasProxied = false; final AsyncServer proxyServer = new AsyncServer(); try { AsyncProxyServer httpServer = new AsyncProxyServer(proxyServer) { @Override protected boolean onRequest(AsyncHttpServerRequest request, AsyncHttpServerResponse response) { wasProxied = true; return super.onRequest(request, response); } }; AsyncServerSocket socket = httpServer.listen(proxyServer, 0); // client.getSocketMiddleware().enableProxy("localhost", 5555); AsyncHttpGet get = new AsyncHttpGet("http://www.clockworkmod.com"); get.enableProxy("localhost", socket.getLocalPort()); Future ret = client.executeString(get, null); String data; assertNotNull(data = ret.get(TIMEOUT, TimeUnit.MILLISECONDS)); assertTrue(data.contains("ClockworkMod")); assertTrue(wasProxied); } finally { proxyServer.stop(); } } public void testUriPathWithSpaces() throws Exception { AsyncHttpRequest request = new AsyncHttpRequest(Uri.parse("http://jpkc.seiee.sjtu.edu.cn/ds/ds2/Course%20lecture/chapter%2010.pdf"), AsyncHttpGet.METHOD); String requestLine = request.getRequestLine().toString(); assertEquals("GET /ds/ds2/Course%20lecture/chapter%2010.pdf HTTP/1.1", requestLine); } public void testHEAD() throws Exception { AsyncHttpHead req = new AsyncHttpHead(Uri.parse("http://31.media.tumblr.com/9606dcaa33b6877b7c485040393b9392/tumblr_mrtnysMonE1r4vl1yo1_500.jpg")); Future str = AsyncHttpClient.getDefaultInstance().executeString(req, null); assertTrue(TextUtils.isEmpty(str.get(TIMEOUT, TimeUnit.MILLISECONDS))); } public void testPostJsonObject() throws Exception { JSONObject post = new JSONObject(); post.put("ping", "pong"); AsyncHttpPost p = new AsyncHttpPost("https://koush.clockworkmod.com/test/echo"); p.setBody(new JSONObjectBody(post)); JSONObject ret = AsyncHttpClient.getDefaultInstance().executeJSONObject(p, null).get(); assertEquals("pong", ret.getString("ping")); } } ================================================ FILE: AndroidAsync/test/src/com/koushikdutta/async/test/HttpServerTests.java ================================================ package com.koushikdutta.async.test; import com.koushikdutta.async.AsyncServer; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.http.AsyncHttpClient; import com.koushikdutta.async.http.AsyncHttpPost; import com.koushikdutta.async.http.NameValuePair; import com.koushikdutta.async.http.body.JSONObjectBody; import com.koushikdutta.async.http.body.MultipartFormDataBody; import com.koushikdutta.async.http.body.StringBody; import com.koushikdutta.async.http.body.UrlEncodedFormBody; import com.koushikdutta.async.http.server.AsyncHttpServer; import com.koushikdutta.async.http.server.AsyncHttpServerRequest; import com.koushikdutta.async.http.server.AsyncHttpServerResponse; import com.koushikdutta.async.http.server.HttpServerRequestCallback; import com.koushikdutta.async.util.StreamUtility; import junit.framework.TestCase; import org.json.JSONObject; import java.io.InputStream; import java.net.URL; import java.net.URLConnection; public class HttpServerTests extends TestCase { AsyncHttpServer httpServer; @Override protected void setUp() throws Exception { super.setUp(); httpServer = new AsyncHttpServer(); httpServer.setErrorCallback(new CompletedCallback() { @Override public void onCompleted(Exception ex) { fail(); } }); httpServer.listen(AsyncServer.getDefault(), 5000); httpServer.get("/hello", new HttpServerRequestCallback() { @Override public void onRequest(AsyncHttpServerRequest request, AsyncHttpServerResponse response) { assertNotNull(request.getHeaders().get("Host")); response.send("hello"); } }); httpServer.post("/echo", new HttpServerRequestCallback() { @Override public void onRequest(AsyncHttpServerRequest request, final AsyncHttpServerResponse response) { try { assertNotNull(request.getHeaders().get("Host")); JSONObject json = new JSONObject(); if (request.getBody() instanceof UrlEncodedFormBody) { UrlEncodedFormBody body = (UrlEncodedFormBody)request.getBody(); for (NameValuePair pair: body.get()) { json.put(pair.getName(), pair.getValue()); } } else if (request.getBody() instanceof JSONObjectBody) { json = ((JSONObjectBody)request.getBody()).get(); } else if (request.getBody() instanceof StringBody) { json.put("foo", ((StringBody)request.getBody()).get()); } else if (request.getBody() instanceof MultipartFormDataBody) { MultipartFormDataBody body = (MultipartFormDataBody)request.getBody(); for (NameValuePair pair: body.get()) { json.put(pair.getName(), pair.getValue()); } } response.send(json); } catch (Exception e) { } } }); } public void testJSONObject() throws Exception { JSONObject json = new JSONObject(); json.put("foo", "bar"); JSONObjectBody body = new JSONObjectBody(json); AsyncHttpPost post = new AsyncHttpPost("http://localhost:5000/echo"); post.setBody(body); json = AsyncHttpClient.getDefaultInstance().executeJSONObject(post, null).get(); assertEquals(json.getString("foo"), "bar"); } public void testString() throws Exception { StringBody body = new StringBody("bar"); AsyncHttpPost post = new AsyncHttpPost("http://localhost:5000/echo"); post.setBody(body); JSONObject json = AsyncHttpClient.getDefaultInstance().executeJSONObject(post, null).get(); assertEquals(json.getString("foo"), "bar"); } // public void testUrlEncodedFormBody() throws Exception { // List params = new ArrayList(); // params.add(new BasicNameValuePair("foo", "bar")); // HttpPost post = new HttpPost("http://localhost:5000/echo"); // post.setEntity(new UrlEncodedFormEntity(params)); // // HttpResponse response = new DefaultHttpClient().execute(post); // String contents = StreamUtility.readToEnd(response.getEntity().getContent()); // JSONObject json = new JSONObject(contents); // assertEquals(json.getString("foo"), "bar"); // } public void testServerHello() throws Exception { URL url = new URL("http://localhost:5000/hello"); URLConnection conn = url.openConnection(); InputStream is = conn.getInputStream(); String contents = StreamUtility.readToEnd(is); is.close(); assertEquals(contents, "hello"); } public void testServerHelloAgain() throws Exception { URL url = new URL("http://localhost:5000/hello"); URLConnection conn = url.openConnection(); InputStream is = conn.getInputStream(); String contents = StreamUtility.readToEnd(is); is.close(); assertEquals(contents, "hello"); } @Override protected void tearDown() throws Exception { super.tearDown(); httpServer.stop(); AsyncServer.getDefault().stop(); } } ================================================ FILE: AndroidAsync/test/src/com/koushikdutta/async/test/Issue59.java ================================================ package com.koushikdutta.async.test; import android.util.Log; import com.koushikdutta.async.AsyncServer; import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.Util; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.http.AsyncHttpClient; import com.koushikdutta.async.http.AsyncHttpGet; import com.koushikdutta.async.http.server.AsyncHttpServer; import com.koushikdutta.async.http.server.AsyncHttpServerRequest; import com.koushikdutta.async.http.server.AsyncHttpServerResponse; import com.koushikdutta.async.http.server.HttpServerRequestCallback; import junit.framework.TestCase; import java.util.concurrent.TimeUnit; /** * Created by koush on 8/31/13. */ public class Issue59 extends TestCase { public void testIssue() throws Exception { AsyncHttpServer httpServer = new AsyncHttpServer(); try { httpServer.get("/", new HttpServerRequestCallback() { @Override public void onRequest(AsyncHttpServerRequest request, final AsyncHttpServerResponse response) { // setting this to empty is a hacky way of telling the framework not to use // transfer-encoding. It will get removed. response.getHeaders().set("Transfer-Encoding", ""); response.code(200); Util.writeAll(response, "foobarbeepboop".getBytes(), new CompletedCallback() { @Override public void onCompleted(Exception ex) { response.end(); } }); } }); httpServer.listen(5959); AsyncHttpGet get = new AsyncHttpGet("http://localhost:5959/"); get.setLogging("issue59", Log.VERBOSE); get.getHeaders().removeAll("Connection"); get.getHeaders().removeAll("Accept-Encoding"); assertEquals("foobarbeepboop", AsyncHttpClient.getDefaultInstance().executeString(get, null).get(1000, TimeUnit.MILLISECONDS)); } finally { httpServer.stop(); AsyncServer.getDefault().stop(); } } public void testIon428() throws Exception { ByteBufferList bb = AsyncHttpClient.getDefaultInstance().executeByteBufferList(new AsyncHttpGet("https://cdn2.vox-cdn.com/thumbor/KxtZNw37jKNfxdA0hX5edHvbTBE=/0x0:2039x1359/800x536/cdn0.vox-cdn.com/uploads/chorus_image/image/44254028/lg-g-watch.0.0.jpg"), null) .get(); } } ================================================ FILE: AndroidAsync/test/src/com/koushikdutta/async/test/IssueWithWebSocketFuturesTests.java ================================================ package com.koushikdutta.async.test; import com.koushikdutta.async.future.Future; import com.koushikdutta.async.http.AsyncHttpClient; import com.koushikdutta.async.http.WebSocket; import com.koushikdutta.async.http.server.AsyncHttpServer; import com.koushikdutta.async.http.server.AsyncHttpServerRequest; import junit.framework.TestCase; import java.util.concurrent.CountDownLatch; public class IssueWithWebSocketFuturesTests extends TestCase { //testing that websocket callback gets called with the correct parameters. public void testWebSocketFutureWithHandshakeFailureCallback() throws Exception { //creating a faulty server! AsyncHttpServer httpServer = new AsyncHttpServer(); httpServer.websocket(".*", new AsyncHttpServer.WebSocketRequestCallback() { @Override public void onConnected(WebSocket webSocket, AsyncHttpServerRequest request) { } }); httpServer.listen(6666); final Exception[] callbackException = {null}; final WebSocket[] callbackWs = {null}; final CountDownLatch countDownLatch = new CountDownLatch(1); //for some reason, it fails with a WebSocketHandshakeException. //But in general, if the handshake fails, the callback must be called with an exception. Future wsFuture = AsyncHttpClient.getDefaultInstance().websocket("ws://127.0.0.1:6666", "ws", new AsyncHttpClient.WebSocketConnectCallback() { @Override public void onCompleted(Exception ex, WebSocket webSocket) { callbackException[0] = ex; callbackWs[0] = webSocket; countDownLatch.countDown(); } }); //wait for the future to complete countDownLatch.await(); //exactly one mut be null assertTrue(callbackWs[0] == null ^ callbackException[0] == null); //callback parameters must be the same as the future's result assertEquals(wsFuture.tryGet(), callbackWs[0]); assertEquals(wsFuture.tryGetException(), callbackException[0]); } } ================================================ FILE: AndroidAsync/test/src/com/koushikdutta/async/test/LineEmitterTests.java ================================================ package com.koushikdutta.async.test; import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.LineEmitter; import com.koushikdutta.async.util.Charsets; import junit.framework.TestCase; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.util.concurrent.Semaphore; /** * Created by koush on 6/9/16. */ public class LineEmitterTests extends TestCase { public void testFunnyCharacter() { final String stuff = "é\n"; LineEmitter emitter = new LineEmitter(Charsets.UTF_8); emitter.setLineCallback(new LineEmitter.StringCallback() { @Override public void onStringAvailable(String s) { assertEquals(s + '\n', stuff); } }); assertEquals(stuff.charAt(0), 233); ByteBufferList bb = new ByteBufferList(ByteBuffer.wrap(stuff.getBytes(Charsets.UTF_8))); emitter.onDataAvailable(null, bb); } } ================================================ FILE: AndroidAsync/test/src/com/koushikdutta/async/test/Md5.java ================================================ package com.koushikdutta.async.test; import com.koushikdutta.async.ByteBufferList; import java.math.BigInteger; import java.nio.ByteBuffer; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; public class Md5 { private MessageDigest digest; public static Md5 createInstance() throws NoSuchAlgorithmException { Md5 md5 = new Md5(); md5.digest = MessageDigest.getInstance("MD5"); return md5; } private Md5() { } public void update(ByteBufferList bb) { while (bb.size() > 0) { ByteBuffer b = bb.remove(); digest.update(b); } } public String digest() { String hash = new BigInteger(digest.digest()).toString(16); return hash; } } ================================================ FILE: AndroidAsync/test/src/com/koushikdutta/async/test/MultipartTests.java ================================================ package com.koushikdutta.async.test; import androidx.test.runner.AndroidJUnit4; import com.koushikdutta.async.AsyncServer; import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.DataCallback; import com.koushikdutta.async.future.Future; import com.koushikdutta.async.http.AsyncHttpClient; import com.koushikdutta.async.http.AsyncHttpClient.StringCallback; import com.koushikdutta.async.http.AsyncHttpPost; import com.koushikdutta.async.http.AsyncHttpResponse; import com.koushikdutta.async.http.body.MultipartFormDataBody; import com.koushikdutta.async.http.body.MultipartFormDataBody.MultipartCallback; import com.koushikdutta.async.http.body.Part; import com.koushikdutta.async.http.server.AsyncHttpServer; import com.koushikdutta.async.http.server.AsyncHttpServerRequest; import com.koushikdutta.async.http.server.AsyncHttpServerResponse; import com.koushikdutta.async.http.server.HttpServerRequestCallback; import org.junit.runner.RunWith; import java.io.File; import java.io.FileOutputStream; import java.util.concurrent.TimeUnit; import static androidx.test.InstrumentationRegistry.getContext; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; @RunWith(AndroidJUnit4.class) public class MultipartTests { AsyncHttpServer httpServer; protected void setUp() throws Exception { httpServer = new AsyncHttpServer(); httpServer.setErrorCallback(new CompletedCallback() { @Override public void onCompleted(Exception ex) { fail(); } }); httpServer.listen(AsyncServer.getDefault(), 5000); httpServer.post("/", new HttpServerRequestCallback() { int gotten = 0; @Override public void onRequest(final AsyncHttpServerRequest request, final AsyncHttpServerResponse response) { final MultipartFormDataBody body = (MultipartFormDataBody)request.getBody(); body.setMultipartCallback(new MultipartCallback() { @Override public void onPart(Part part) { if (part.isFile()) { body.setDataCallback(new DataCallback() { @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { gotten += bb.remaining(); bb.recycle(); } }); } } }); request.setEndCallback(new CompletedCallback() { @Override public void onCompleted(Exception ex) { response.send(body.getField("baz") + gotten + body.getField("foo")); } }); } }); } protected void tearDown() throws Exception { httpServer.stop(); AsyncServer.getDefault().stop(); } public void testUpload() throws Exception { setUp(); try { File dummy = getContext().getFileStreamPath("dummy.txt"); final String FIELD_VAL = "bar"; dummy.getParentFile().mkdirs(); FileOutputStream fout = new FileOutputStream(dummy); byte[] zeroes = new byte[100000]; for (int i = 0; i < 10; i++) { fout.write(zeroes); } fout.close(); // StreamUtility.writeFile(dummy, DUMMY_VAL); AsyncHttpPost post = new AsyncHttpPost("http://localhost:5000"); MultipartFormDataBody body = new MultipartFormDataBody(); body.addStringPart("foo", FIELD_VAL); body.addFilePart("my-file", dummy); body.addStringPart("baz", FIELD_VAL); post.setBody(body); Future ret = AsyncHttpClient.getDefaultInstance().executeString(post, new StringCallback() { @Override public void onCompleted(Exception e, AsyncHttpResponse source, String result) { } }); String data = ret.get(10000, TimeUnit.MILLISECONDS); assertEquals(data, FIELD_VAL + (zeroes.length * 10) + FIELD_VAL); } finally { tearDown(); } } } ================================================ FILE: AndroidAsync/test/src/com/koushikdutta/async/test/ParserTests.java ================================================ package com.koushikdutta.async.test; import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.FilteredDataEmitter; import com.koushikdutta.async.future.Future; import com.koushikdutta.async.parser.DocumentParser; import com.koushikdutta.async.parser.StringParser; import com.koushikdutta.async.util.Charsets; import junit.framework.TestCase; import org.w3c.dom.Document; import java.nio.ByteBuffer; import java.nio.charset.Charset; /** * Created by koush on 7/10/14. */ public class ParserTests extends TestCase { public void testString() throws Exception { StringParser p = new StringParser(); FilteredDataEmitter f = new FilteredDataEmitter() { @Override public boolean isPaused() { return false; } }; Future ret = p.parse(f); ByteBufferList l = new ByteBufferList(); l.add(ByteBuffer.wrap("foo".getBytes(Charsets.US_ASCII.name()))); f.onDataAvailable(f, l); f.getEndCallback().onCompleted(null); String s = ret.get(); assertEquals(s, "foo"); } public void testUtf8String() throws Exception { StringParser p = new StringParser(); FilteredDataEmitter f = new FilteredDataEmitter() { @Override public String charset() { return Charsets.UTF_8.name(); } @Override public boolean isPaused() { return false; } }; Future ret = p.parse(f); ByteBufferList l = new ByteBufferList(); l.add(ByteBuffer.wrap("æææ".getBytes(Charsets.UTF_8.name()))); f.onDataAvailable(f, l); f.getEndCallback().onCompleted(null); String s = ret.get(); assertEquals(s, "æææ"); } public void testAsyncParserBase() throws Exception { DocumentParser parser = new DocumentParser(); assertEquals(parser.getType(), Document.class); } } ================================================ FILE: AndroidAsync/test/src/com/koushikdutta/async/test/ProxyTests.java ================================================ package com.koushikdutta.async.test; import android.util.Log; import com.koushikdutta.async.http.AsyncHttpClient; import com.koushikdutta.async.http.AsyncHttpGet; import junit.framework.TestCase; import java.util.concurrent.TimeUnit; /** * Created by koush on 4/20/14. */ public class ProxyTests extends TestCase { private void disabledTestSSLProxy() throws Exception { AsyncHttpGet get = new AsyncHttpGet("http://yahoo.com"); get.enableProxy("192.168.2.21", 8888); get.setLogging("SSLProxy", Log.VERBOSE); String ret = AsyncHttpClient.getDefaultInstance().executeString(get, null).get(5000, TimeUnit.DAYS.MILLISECONDS); } } ================================================ FILE: AndroidAsync/test/src/com/koushikdutta/async/test/RedirectTests.java ================================================ package com.koushikdutta.async.test; import com.koushikdutta.async.AsyncServer; import com.koushikdutta.async.http.AsyncHttpClient; import com.koushikdutta.async.http.AsyncHttpGet; import com.koushikdutta.async.http.server.AsyncHttpServer; import com.koushikdutta.async.http.server.AsyncHttpServerRequest; import com.koushikdutta.async.http.server.AsyncHttpServerResponse; import com.koushikdutta.async.http.server.HttpServerRequestCallback; import junit.framework.TestCase; /** * Created by koush on 11/4/13. */ public class RedirectTests extends TestCase { @Override protected void setUp() throws Exception { super.setUp(); AsyncHttpServer server = new AsyncHttpServer(); server.listen(6003); server.get("/foo", new HttpServerRequestCallback() { @Override public void onRequest(AsyncHttpServerRequest request, final AsyncHttpServerResponse response) { response.redirect("/bar"); } }); server.get("/bar", new HttpServerRequestCallback() { @Override public void onRequest(AsyncHttpServerRequest request, final AsyncHttpServerResponse response) { response.send("BORAT!"); } }); server.get("/foo/poo", new HttpServerRequestCallback() { @Override public void onRequest(AsyncHttpServerRequest request, final AsyncHttpServerResponse response) { response.redirect("../poo"); } }); server.get("/poo", new HttpServerRequestCallback() { @Override public void onRequest(AsyncHttpServerRequest request, final AsyncHttpServerResponse response) { response.send("SWEET!"); } }); server.get("/foo/bar", new HttpServerRequestCallback() { @Override public void onRequest(AsyncHttpServerRequest request, final AsyncHttpServerResponse response) { response.redirect("baz"); } }); server.get("/foo/baz", new HttpServerRequestCallback() { @Override public void onRequest(AsyncHttpServerRequest request, final AsyncHttpServerResponse response) { response.send("SUCCESS!"); } }); } @Override protected void tearDown() throws Exception { super.tearDown(); AsyncServer.getDefault().stop(); } public void testRelativeRedirect() throws Exception { String ret = AsyncHttpClient.getDefaultInstance() .executeString(new AsyncHttpGet("http://localhost:6003/foo/bar"), null) .get(); assertEquals(ret, "SUCCESS!"); ret = AsyncHttpClient.getDefaultInstance() .executeString(new AsyncHttpGet("http://localhost:6003/foo"), null) .get(); assertEquals(ret, "BORAT!"); ret = AsyncHttpClient.getDefaultInstance() .executeString(new AsyncHttpGet("http://localhost:6003/foo/poo"), null) .get(); assertEquals(ret, "SWEET!"); } } ================================================ FILE: AndroidAsync/test/src/com/koushikdutta/async/test/SSLTests.java ================================================ package com.koushikdutta.async.test; import androidx.test.runner.AndroidJUnit4; import com.koushikdutta.async.AsyncServer; import com.koushikdutta.async.http.AsyncHttpClient; import com.koushikdutta.async.http.AsyncHttpGet; import com.koushikdutta.async.http.server.AsyncHttpServer; import com.koushikdutta.async.http.server.AsyncHttpServerRequest; import com.koushikdutta.async.http.server.AsyncHttpServerResponse; import com.koushikdutta.async.http.server.HttpServerRequestCallback; import org.json.JSONObject; import org.junit.runner.RunWith; import java.security.KeyStore; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManagerFactory; import static androidx.test.InstrumentationRegistry.getContext; /** * Created by koush on 6/4/13. */ @RunWith(AndroidJUnit4.class) public class SSLTests { public void testKeys() throws Exception { KeyManagerFactory kmf = KeyManagerFactory.getInstance("X509"); KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); ks.load(getContext().getResources().openRawResource(R.raw.keystore), "storepass".toCharArray()); kmf.init(ks, "storepass".toCharArray()); TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); KeyStore ts = KeyStore.getInstance(KeyStore.getDefaultType()); ts.load(getContext().getResources().openRawResource(R.raw.keystore), "storepass".toCharArray()); tmf.init(ts); SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); AsyncHttpServer httpServer = new AsyncHttpServer(); httpServer.listenSecure(8888, sslContext); httpServer.get("/", new HttpServerRequestCallback() { @Override public void onRequest(AsyncHttpServerRequest request, AsyncHttpServerResponse response) { response.send("hello"); } }); Thread.sleep(1000); AsyncHttpClient.getDefaultInstance().getSSLSocketMiddleware().setSSLContext(sslContext); AsyncHttpClient.getDefaultInstance().getSSLSocketMiddleware().setTrustManagers(tmf.getTrustManagers()); AsyncHttpClient.getDefaultInstance().executeString(new AsyncHttpGet("https://localhost:8888/"), null).get(); } public void disabled__testClientCertificateIssue163() throws Exception { // https://security.springthroughtest.com/hello.json AsyncServer server = new AsyncServer(); try { AsyncHttpClient client = new AsyncHttpClient(server); JSONObject json = client.executeJSONObject(new AsyncHttpGet("https://security.springthroughtest.com/hello.json"), null).get(); } finally { server.stop(); } } } ================================================ FILE: AndroidAsync/test/src/com/koushikdutta/async/test/SanityChecks.java ================================================ package com.koushikdutta.async.test; import junit.framework.TestCase; import java.nio.ByteBuffer; import java.nio.ByteOrder; /** * Created by koush on 5/15/13. */ public class SanityChecks extends TestCase { public void testByteOrder() { assertTrue(ByteBuffer.allocate(0).order().equals(ByteOrder.BIG_ENDIAN)); } } ================================================ FILE: AndroidAsync/test/src/com/koushikdutta/async/test/TimeoutTests.java ================================================ package com.koushikdutta.async.test; import android.net.Uri; import android.util.Log; import com.koushikdutta.async.AsyncServer; import com.koushikdutta.async.DataSink; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.http.AsyncHttpClient; import com.koushikdutta.async.http.AsyncHttpRequest; import com.koushikdutta.async.http.body.StringBody; import com.koushikdutta.async.http.server.AsyncHttpServer; import com.koushikdutta.async.http.server.AsyncHttpServerRequest; import com.koushikdutta.async.http.server.AsyncHttpServerResponse; import com.koushikdutta.async.http.server.HttpServerRequestCallback; import junit.framework.TestCase; import java.net.URI; import java.util.concurrent.TimeoutException; /** * Created by koush on 7/11/13. */ public class TimeoutTests extends TestCase { public TimeoutTests() { server.get("/3", new HttpServerRequestCallback() { @Override public void onRequest(AsyncHttpServerRequest request, final AsyncHttpServerResponse response) { // never respond AsyncServer.getDefault().postDelayed(new Runnable() { @Override public void run() { response.send("3"); } }, 1000); } }); server.post("/now", new HttpServerRequestCallback() { @Override public void onRequest(AsyncHttpServerRequest request, AsyncHttpServerResponse response) { StringBody body = (StringBody)request.getBody(); response.send(body.get()); } }); } AsyncHttpServer server = new AsyncHttpServer(); @Override protected void setUp() throws Exception { super.setUp(); server.listen(AsyncServer.getDefault(), 5000); } @Override protected void tearDown() throws Exception { super.tearDown(); server.stop(); AsyncServer.getDefault().stop(); } public void testTimeout() throws Exception { AsyncHttpRequest req = new AsyncHttpRequest(Uri.parse("http://localhost:5000/3"), "GET"); req.setTimeout(1000); try { AsyncHttpClient.getDefaultInstance().executeString(req, null).get(); fail(); } catch (Exception e) { Log.d("timeout", "error", e); assertTrue(e.getCause() instanceof TimeoutException); } req = new AsyncHttpRequest(Uri.parse("http://localhost:5000/3"), "GET"); assertEquals("3", AsyncHttpClient.getDefaultInstance().executeString(req, null).get()); } public void testSlowBody() throws Exception { AsyncHttpRequest req = new AsyncHttpRequest(Uri.parse("http://localhost:5000/now"), "POST"); req.setTimeout(1000); req.setLogging("slowbody", Log.VERBOSE); req.setBody(new DelayedStringBody("foo")); assertEquals("foo", AsyncHttpClient.getDefaultInstance().executeString(req, null).get()); req = new AsyncHttpRequest(Uri.parse("http://localhost:5000/3"), "GET"); req.setLogging("slowbody", Log.VERBOSE); req.setTimeout(100); req.setBody(new DelayedStringBody("foo")); try { AsyncHttpClient.getDefaultInstance().executeString(req, null).get(); fail(); } catch (Exception e) { Log.d("timeout", "error", e); assertTrue(e.getCause() instanceof TimeoutException); } } class DelayedStringBody extends StringBody { public DelayedStringBody(String value) { super(value); } @Override public void write(final AsyncHttpRequest request, final DataSink sink, final CompletedCallback completed) { AsyncServer.getDefault().postDelayed(new Runnable() { @Override public void run() { DelayedStringBody.super.write(request, sink, completed); } }, 1000); } } } ================================================ FILE: AndroidAsync/test/src/com/koushikdutta/async/test/TriggerFuture.java ================================================ package com.koushikdutta.async.test; import com.koushikdutta.async.future.SimpleFuture; class TriggerFuture extends SimpleFuture { public void trigger() { setComplete(2020); } } ================================================ FILE: AndroidAsync/test/src/com/koushikdutta/async/test/WebSocketTests.java ================================================ package com.koushikdutta.async.test; import com.koushikdutta.async.AsyncServer; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.http.AsyncHttpClient; import com.koushikdutta.async.http.AsyncHttpClient.WebSocketConnectCallback; import com.koushikdutta.async.http.Headers; import com.koushikdutta.async.http.WebSocket; import com.koushikdutta.async.http.WebSocket.StringCallback; import com.koushikdutta.async.http.server.AsyncHttpServer; import com.koushikdutta.async.http.server.AsyncHttpServer.WebSocketRequestCallback; import com.koushikdutta.async.http.server.AsyncHttpServerRequest; import junit.framework.TestCase; import org.junit.Test; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; public class WebSocketTests extends TestCase { AsyncHttpServer httpServer; @Override protected void setUp() throws Exception { super.setUp(); httpServer = new AsyncHttpServer(); httpServer.setErrorCallback(new CompletedCallback() { @Override public void onCompleted(Exception ex) { fail(); } }); httpServer.listen(AsyncServer.getDefault(), 5000); httpServer.websocket("/ws", new WebSocketRequestCallback() { @Override public void onConnected(final WebSocket webSocket, AsyncHttpServerRequest request) { webSocket.setStringCallback(new StringCallback() { @Override public void onStringAvailable(String s) { webSocket.send(s); } }); } }); } private static final long TIMEOUT = 60000L; @Test public void testWebSocket() throws Exception { final Semaphore semaphore = new Semaphore(0); AsyncHttpClient.getDefaultInstance().websocket("http://localhost:5000/ws", (String)null, new WebSocketConnectCallback() { @Override public void onCompleted(Exception ex, WebSocket webSocket) { webSocket.send("hello"); webSocket.setStringCallback(new StringCallback() { @Override public void onStringAvailable(String s) { assertEquals(s, "hello"); semaphore.release(); } }); } }); assertTrue(semaphore.tryAcquire(TIMEOUT, TimeUnit.MILLISECONDS)); } // public void testDisconnect() throws Exception { // final Semaphore semaphore = new Semaphore(0); // // AsyncHttpClient.getDefaultInstance().websocket("http://192.168.1.2:3005", null, new WebSocketConnectCallback() { // @Override // public void onCompleted(Exception ex, WebSocket webSocket) { // webSocket.setClosedCallback(new CompletedCallback() { // @Override // public void onCompleted(Exception ex) { // semaphore.release(); // } // }); // } // }); // // assertTrue(semaphore.tryAcquire(TIMEOUT, TimeUnit.MILLISECONDS)); // } @Override protected void tearDown() throws Exception { super.tearDown(); httpServer.stop(); } } ================================================ FILE: AndroidAsync-Kotlin/.gitignore ================================================ /build ================================================ FILE: AndroidAsync-Kotlin/README.md ================================================ # Support for Kotlin Coroutines in AndroidAsync and Ion Adds coroutines support to AndroidAsync and [Ion](https://github.com/koush/ion). Maven: ```xml com.koushikdutta.async androidasync-kotlin (insert latest version) ``` Gradle: ```groovy dependencies { compile 'com.koushikdutta.async:androidasync-kotlin:' } ``` Since AndroidAsync and Ion operations all returned futures, you can simply call await() on them within a Kotlin suspend function. ```kotlin suspend fun getTheRobotsTxt() { val googleRobots = Ion.with(context) .load("https://google.com/robots.txt") .asString() .await() val githubRobots = Ion.with(context) .load("https://github.com/robots.txt") .asString() .await() return googleRobots + githubRobots } ``` That's it! But remember that the await() suspends, so if you want to fetch both robots.txt at the same time: ```kotlin suspend fun getTheRobotsTxt() { val googleRobots = Ion.with(context) .load("https://google.com/robots.txt") .asString() val githubRobots = Ion.with(context) .load("https://github.com/robots.txt") .asString() return googleRobots.await() + githubRobots.await() } ``` ================================================ FILE: AndroidAsync-Kotlin/build.gradle ================================================ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' buildscript { ext.kotlin_version = '1.3.61' repositories { mavenCentral() } dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } android { compileSdkVersion 29 buildToolsVersion "29.0.2" kotlinOptions { apiVersion = "1.3" languageVersion = "1.3" } compileOptions { sourceCompatibility 1.8 targetCompatibility 1.8 } defaultConfig { minSdkVersion 14 targetSdkVersion 29 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles 'consumer-rules.pro' } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) api project(':AndroidAsync:AndroidAsync') implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" testImplementation 'junit:junit:4.12' testImplementation 'org.jetbrains.kotlin:kotlin-test-junit:1.3.61' androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' } // upload to maven task if (false && System.getenv().I_AM_KOUSH == 'true') { apply from: '/Users/koush/cfg/maven.gradle' } ================================================ FILE: AndroidAsync-Kotlin/consumer-rules.pro ================================================ ================================================ FILE: AndroidAsync-Kotlin/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile ================================================ FILE: AndroidAsync-Kotlin/src/androidTest/java/com/koushikdutta/async/kotlin/ExampleInstrumentedTest.kt ================================================ package com.koushikdutta.async.kotlin import androidx.test.platform.app.InstrumentationRegistry import androidx.test.runner.AndroidJUnit4 import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith /** * Instrumented test, which will execute on an Android device. * * See [testing documentation](http://d.android.com/tools/testing). */ @RunWith(AndroidJUnit4::class) class ExampleInstrumentedTest { @Test fun useAppContext() { // Context of the app under test. val appContext = InstrumentationRegistry.getInstrumentation().targetContext assertEquals("com.koushikdutta.async.kotlin.test", appContext.packageName) } } ================================================ FILE: AndroidAsync-Kotlin/src/main/AndroidManifest.xml ================================================ ================================================ FILE: AndroidAsync-Kotlin/src/main/java/com/koushikdutta/async/kotlin/FutureExtensions.kt ================================================ package com.koushikdutta.async.kotlin import com.koushikdutta.async.future.Future import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine suspend fun Future.await(): T { return suspendCoroutine { this.setCallback { e, result -> if (e != null) it.resumeWithException(e) else it.resume(result) } } } ================================================ FILE: AndroidAsync-Kotlin/src/test/java/com/koushikdutta/async/kotlin/ExampleUnitTest.kt ================================================ package com.koushikdutta.async.kotlin import org.junit.Test import org.junit.Assert.* /** * Example local unit test, which will execute on the development machine (host). * * See [testing documentation](http://d.android.com/tools/testing). */ class ExampleUnitTest { @Test fun addition_isCorrect() { assertEquals(4, 2 + 2) } } ================================================ FILE: AndroidAsyncSample/AndroidManifest.xml ================================================ ================================================ FILE: AndroidAsyncSample/build.gradle ================================================ apply plugin: 'com.android.application' dependencies { compile project(':AndroidAsync') } android { sourceSets { main { manifest.srcFile 'AndroidManifest.xml' res.srcDirs = ['res/'] java.srcDirs = ['src/'] } } defaultConfig { targetSdkVersion 24 minSdkVersion 9 } compileSdkVersion project.hasProperty('global_compileSdkVersion') ? global_compileSdkVersion : 25 buildToolsVersion project.hasProperty('global_buildToolsVersion') ? global_buildToolsVersion : '25.0.2' } ================================================ FILE: AndroidAsyncSample/proguard-project.txt ================================================ # To enable ProGuard in your project, edit project.properties # to define the proguard.config property as described in that file. # # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in ${sdk.dir}/tools/proguard/proguard-android.txt # You can edit the include path and order by changing the ProGuard # include property in project.properties. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} ================================================ FILE: AndroidAsyncSample/project.properties ================================================ # This file is automatically generated by Android Tools. # Do not modify this file -- YOUR CHANGES WILL BE ERASED! # # This file must be checked in Version Control Systems. # # To customize properties used by the Ant build system edit # "ant.properties", and override values to adapt the script to your # project structure. # # To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt # Project target. target=android-17 android.library.reference.1=../AndroidAsync ================================================ FILE: AndroidAsyncSample/res/layout/activity_main.xml ================================================ ================================================ FILE: AndroidAsyncSample/res/menu/activity_main.xml ================================================

================================================ FILE: AndroidAsyncSample/res/values/strings.xml ================================================ AndroidAsyncSample Hello world! Settings AndroidAsync Sample Download Images ================================================ FILE: AndroidAsyncSample/res/values/styles.xml ================================================