diff --git a/integration/cronet/build.gradle b/integration/cronet/build.gradle new file mode 100644 index 0000000000..088500354d --- /dev/null +++ b/integration/cronet/build.gradle @@ -0,0 +1,28 @@ +package com.bumptech.glide.integration.cronet + +apply plugin: 'com.android.library' + +dependencies { + implementation project(':library') + annotationProcessor project(':annotation:compiler') + + api "androidx.annotation:annotation:${ANDROID_X_VERSION}" +} + +android { + compileSdkVersion COMPILE_SDK_VERSION as int + + defaultConfig { + minSdkVersion MIN_SDK_VERSION as int + targetSdkVersion TARGET_SDK_VERSION as int + + versionName VERSION_NAME as String + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_7 + targetCompatibility JavaVersion.VERSION_1_7 + } +} + +apply from: "${rootProject.projectDir}/scripts/upload.gradle" diff --git a/integration/cronet/gradle.properties b/integration/cronet/gradle.properties new file mode 100644 index 0000000000..fbf92ce825 --- /dev/null +++ b/integration/cronet/gradle.properties @@ -0,0 +1,4 @@ +POM_NAME=Glide Cronet Integration +POM_ARTIFACT_ID=cronet-integration +POM_PACKAGING=aar +POM_DESCRIPTION=An integration library to use Cronet to fetch data over http/https in Glide diff --git a/integration/cronet/lint.xml b/integration/cronet/lint.xml new file mode 100644 index 0000000000..ff7e5955c4 --- /dev/null +++ b/integration/cronet/lint.xml @@ -0,0 +1,4 @@ + + + + diff --git a/integration/cronet/src/main/AndroidManifest.xml b/integration/cronet/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..5c2324a61a --- /dev/null +++ b/integration/cronet/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + + diff --git a/integration/cronet/src/main/java/com/bumptech/glide/integration/cronet/BufferQueue.java b/integration/cronet/src/main/java/com/bumptech/glide/integration/cronet/BufferQueue.java new file mode 100644 index 0000000000..172bf80437 --- /dev/null +++ b/integration/cronet/src/main/java/com/bumptech/glide/integration/cronet/BufferQueue.java @@ -0,0 +1,134 @@ +package com.bumptech.glide.integration.cronet; + +import java.nio.ByteBuffer; +import java.util.ArrayDeque; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.atomic.AtomicBoolean; +import org.chromium.net.UrlResponseInfo; + +/** + * A utility for processing response bodies, as one contiguous buffer rather than an asynchronous + * stream. + */ +final class BufferQueue { + public static final String CONTENT_LENGTH = "content-length"; + public static final String CONTENT_ENCODING = "content-encoding"; + private final Queue mBuffers; + private final AtomicBoolean mIsCoalesced = new AtomicBoolean(false); + + public static Builder builder() { + return new Builder(); + } + + /** + * Use this class during a request, to combine streamed buffers of a response into a single final + * buffer. + * + *

For example: {@code @Override public void onResponseStarted(UrlRequest request, + * UrlResponseInfo info) { request.read(builder.getFirstBuffer(info)); } @Override public void + * onReadCompleted(UrlRequest request, UrlResponseInfo info, ByteBuffer buffer) { + * request.read(builder.getNextBuffer(buffer)); } } + */ + public static final class Builder { + private ArrayDeque mBuffers = new ArrayDeque<>(); + private RuntimeException whenClosed; + + private Builder() {} + + /** Returns the next buffer to write data into. */ + public ByteBuffer getNextBuffer(ByteBuffer lastBuffer) { + if (mBuffers == null) { + throw new RuntimeException(whenClosed); + } + if (lastBuffer != mBuffers.peekLast()) { + mBuffers.addLast(lastBuffer); + } + if (lastBuffer.hasRemaining()) { + return lastBuffer; + } else { + return ByteBuffer.allocateDirect(8096); + } + } + + /** Returns a ByteBuffer heuristically sized to hold the whole response body. */ + public ByteBuffer getFirstBuffer(UrlResponseInfo info) { + // Security note - a malicious server could attempt to exhaust client memory by sending + // down a Content-Length of a very large size, which we would eagerly allocate without + // the server having to actually send those bytes. This isn't considered to be an + // issue, because that same malicious server could use our transparent gzip to force us + // to allocate 1032 bytes per byte sent by the server. + return ByteBuffer.allocateDirect((int) Math.min(bufferSizeHeuristic(info), 524288)); + } + + private static long bufferSizeHeuristic(UrlResponseInfo info) { + final Map> headers = info.getAllHeaders(); + if (headers.containsKey(CONTENT_LENGTH)) { + long contentLength = Long.parseLong(headers.get(CONTENT_LENGTH).get(0)); + boolean isCompressed = + !headers.containsKey(CONTENT_ENCODING) + || (headers.get(CONTENT_ENCODING).size() == 1 + && "identity".equals(headers.get(CONTENT_ENCODING).get(0))); + if (isCompressed) { + // We have to guess at the uncompressed size. In the future, consider guessing a + // compression ratio based on the content-type and content-encoding. For now, + // assume 2. + return 2 * contentLength; + } else { + // In this case, we know exactly how many bytes we're going to get, so we can + // size our buffer perfectly. However, we still have to call read() for the last time, + // even when we know there shouldn't be any more bytes coming. To avoid allocating another + // buffer for that case, add one more byte than we really need. + return contentLength + 1; + } + } else { + // No content-length. This means we're either being sent a chunked response, or the + // java stack stripped content length because of transparent gzip. In either case we really + // have no idea, and so we fall back to a reasonable guess. + return 8192; + } + } + + public BufferQueue build() { + whenClosed = new RuntimeException(); + final ArrayDeque buffers = mBuffers; + mBuffers = null; + return new BufferQueue(buffers); + } + } + + private BufferQueue(Queue buffers) { + mBuffers = buffers; + for (ByteBuffer buffer : mBuffers) { + buffer.flip(); + } + } + + /** Returns the response body as a single contiguous buffer. */ + public ByteBuffer coalesceToBuffer() { + markCoalesced(); + if (mBuffers.size() == 0) { + return ByteBuffer.allocateDirect(0); + } else if (mBuffers.size() == 1) { + return mBuffers.remove(); + } else { + int size = 0; + for (ByteBuffer buffer : mBuffers) { + size += buffer.remaining(); + } + ByteBuffer result = ByteBuffer.allocateDirect(size); + while (!mBuffers.isEmpty()) { + result.put(mBuffers.remove()); + } + result.flip(); + return result; + } + } + + private void markCoalesced() { + if (!mIsCoalesced.compareAndSet(false, true)) { + throw new IllegalStateException("This BufferQueue has already been consumed"); + } + } +} diff --git a/integration/cronet/src/main/java/com/bumptech/glide/integration/cronet/ByteBufferParser.java b/integration/cronet/src/main/java/com/bumptech/glide/integration/cronet/ByteBufferParser.java new file mode 100644 index 0000000000..fef4319a03 --- /dev/null +++ b/integration/cronet/src/main/java/com/bumptech/glide/integration/cronet/ByteBufferParser.java @@ -0,0 +1,15 @@ +package com.bumptech.glide.integration.cronet; + +import java.nio.ByteBuffer; + +/** + * Parses a {@link java.nio.ByteBuffer} to a particular data type. + * + * @param The type of data to parse the buffer to. + */ +interface ByteBufferParser { + /** Returns the required type of data parsed from the given {@link ByteBuffer}. */ + T parse(ByteBuffer byteBuffer); + /** Returns the {@link Class} of the data that will be parsed from {@link ByteBuffer}s. */ + Class getDataClass(); +} diff --git a/integration/cronet/src/main/java/com/bumptech/glide/integration/cronet/ChromiumRequestSerializer.java b/integration/cronet/src/main/java/com/bumptech/glide/integration/cronet/ChromiumRequestSerializer.java new file mode 100644 index 0000000000..455935e328 --- /dev/null +++ b/integration/cronet/src/main/java/com/bumptech/glide/integration/cronet/ChromiumRequestSerializer.java @@ -0,0 +1,389 @@ +package com.bumptech.glide.integration.cronet; + +import android.util.Log; +import androidx.annotation.Nullable; +import com.bumptech.glide.Priority; +import com.bumptech.glide.load.HttpException; +import com.bumptech.glide.load.engine.executor.GlideExecutor; +import com.bumptech.glide.load.engine.executor.GlideExecutor.UncaughtThrowableStrategy; +import com.bumptech.glide.load.model.GlideUrl; +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.nio.ByteBuffer; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executor; +import org.chromium.net.CronetException; +import org.chromium.net.UrlRequest; +import org.chromium.net.UrlRequest.Callback; +import org.chromium.net.UrlResponseInfo; + +/** + * Ensures that two simultaneous requests for exactly the same url make only a single http request. + * + *

Requests are started by Glide on multiple threads in a thread pool. An arbitrary number of + * threads may attempt to start or cancel requests for one or more urls at once. Our goal is to + * ensure: + *

  • + * + *