Chromium Code Reviews| Index: net/cronet/android/java/src/org/chromium/net/UrlRequest.java |
| diff --git a/net/cronet/android/java/src/org/chromium/net/UrlRequest.java b/net/cronet/android/java/src/org/chromium/net/UrlRequest.java |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..056fcdc2df6a190fc1f7edd8dde8aedfab88eeba |
| --- /dev/null |
| +++ b/net/cronet/android/java/src/org/chromium/net/UrlRequest.java |
| @@ -0,0 +1,388 @@ |
| +// Copyright 2014 The Chromium Authors. All rights reserved. |
|
mmenke
2014/02/27 23:06:02
Should these be the dates from the original files,
mef
2014/03/03 19:15:13
Good question. My reasoning was that these files a
|
| +// Use of this source code is governed by a BSD-style license that can be |
| +// found in the LICENSE file. |
| + |
| +package org.chromium.net; |
| + |
| +import org.apache.http.conn.ConnectTimeoutException; |
| +import org.chromium.base.AccessedByNative; |
| +import org.chromium.base.CalledByNative; |
| + |
| +import java.io.IOException; |
| +import java.net.MalformedURLException; |
| +import java.net.URL; |
| +import java.net.UnknownHostException; |
| +import java.nio.ByteBuffer; |
| +import java.nio.channels.ReadableByteChannel; |
| +import java.nio.channels.WritableByteChannel; |
| +import java.util.HashMap; |
| +import java.util.Map; |
| +import java.util.Map.Entry; |
| +import java.util.concurrent.Semaphore; |
| + |
| +/** |
| + * Network request using the native http stack implementation. |
| + */ |
| +public class UrlRequest { |
| + |
| + /* |
|
mmenke
2014/02/27 23:06:02
nit: All comments in this file other than this on
mef
2014/03/03 19:15:13
Done. Does /** mean something special for javadoc?
mmenke
2014/03/04 19:36:20
Honestly, I have no clue.
Charles
2014/03/04 21:17:40
Yes. The double star means that it's javadoc (as o
|
| + * Must match definitions in the native source. |
| + */ |
| + public static final int REQUEST_PRIORITY_IDLE = 0; |
| + public static final int REQUEST_PRIORITY_LOWEST = 1; |
| + public static final int REQUEST_PRIORITY_LOW = 2; |
| + public static final int REQUEST_PRIORITY_MEDIUM = 3; |
| + public static final int REQUEST_PRIORITY_HIGHEST = 4; |
| + |
| + /* |
| + * Must match definitions in the native source. |
| + */ |
| + private static final int SUCCESS = 0; |
| + private static final int UNKNOWN_ERROR = 1; |
| + private static final int MALFORMED_URL_ERROR = 2; |
| + private static final int CONNECTION_TIMED_OUT = 3; |
| + private static final int UNKNOWN_HOST = 4; |
| + |
| + private static final int UPLOAD_BYTE_BUFFER_SIZE = 32768; |
| + |
| + private final UrlRequestContext mRequestContext; |
| + private final String mUrl; |
| + private final int mPriority; |
| + private final Map<String, String> mHeaders; |
| + private final WritableByteChannel mSink; |
| + |
| + private Map<String, String> mAdditionalHeaders; |
| + private String mPostBodyContentType; |
| + private byte[] mPostBody; |
| + private ReadableByteChannel mPostBodyChannel; |
| + private WritableByteChannel mOutputChannel; |
| + private IOException mSinkException; |
| + private boolean mStarted; |
| + private boolean mCanceled; |
| + private boolean mRecycled; |
| + private boolean mFinished; |
| + private String mContentType; |
| + private long mContentLength; |
| + private Semaphore mAppendChunkSemaphore; |
|
mmenke
2014/02/27 23:06:02
All of these variable names violate the Google Jav
mef
2014/03/03 19:15:13
Added TODO()
Charles
2014/03/04 21:17:40
They're android-style java.
On 2014/03/03 19:15:1
|
| + |
| + /** |
| + * This field is accessed exclusively from the native layer. |
| + */ |
| + @AccessedByNative |
| + @SuppressWarnings("unused") |
| + private long mRequest; |
| + |
| + /** |
| + * Constructor. |
| + * |
| + * @param requestContext The context. |
| + * @param url The URL. |
| + * @param priority Request priority, e.g. {@link #REQUEST_PRIORITY_MEDIUM}. |
| + * @param headers HTTP headers. |
| + * @param sink The output channel into which downloaded content will be written. |
|
mmenke
2014/02/27 23:06:02
nit: While the Java style guide allows 100 charac
mef
2014/03/03 19:15:13
Done.
|
| + */ |
| + public UrlRequest(UrlRequestContext requestContext, String url, int priority, |
| + Map<String, String> headers, WritableByteChannel sink) { |
|
mmenke
2014/02/27 23:06:02
Suggest indenting lines like this to align with pa
mef
2014/03/03 19:15:13
Done.
|
| + mRequestContext = requestContext; |
| + mUrl = url; |
| + mPriority = priority; |
| + mHeaders = headers; |
| + mSink = sink; |
| + nativeInit(mRequestContext, mUrl, mPriority); |
| + } |
| + |
| + /** |
| + * Adds a request header. |
| + */ |
| + public void addHeader(String header, String value) { |
| + validateNotStarted(); |
| + if (mAdditionalHeaders == null) { |
| + mAdditionalHeaders = new HashMap<String, String>(); |
| + } |
| + mAdditionalHeaders.put(header, value); |
| + } |
| + |
| + /** |
| + * Sets data to upload as part of a POST request. |
| + * |
| + * @param contentType MIME type of the post content or null if this is not a POST. |
| + * @param data The content that needs to be uploaded if this is a POST request. |
| + */ |
| + public void setUploadData(String contentType, byte[] data) { |
| + validateNotStarted(); |
| + mPostBodyContentType = contentType; |
| + mPostBody = data; |
| + mPostBodyChannel = null; |
| + } |
| + |
| + /** |
| + * Sets a readable byte channel to upload as part of a POST request. |
| + * |
| + * @param contentType MIME type of the post content or null if this is not a POST. |
| + * @param channel The channel to read to read upload data from if this is a POST request. |
| + */ |
| + public void setUploadChannel(String contentType, ReadableByteChannel channel) { |
| + validateNotStarted(); |
| + mPostBodyContentType = contentType; |
| + mPostBodyChannel = channel; |
| + mPostBody = null; |
| + |
| + mAppendChunkSemaphore = new Semaphore(0); |
| + } |
| + |
| + public WritableByteChannel getSink() { |
| + return mSink; |
| + } |
| + |
| + public void start() { |
| + synchronized (this) { |
| + if (mCanceled) { |
| + return; |
| + } |
| + |
| + validateNotStarted(); |
| + validateNotRecycled(); |
| + |
| + mStarted = true; |
| + |
| + if (mPostBody != null && mPostBody.length > 0) { |
| + nativeSetPostData(mPostBodyContentType, mPostBody); |
| + } else if (mPostBodyChannel != null) { |
| + nativeBeginChunkedUpload(mPostBodyContentType); |
| + } |
| + |
| + if (mHeaders != null && !mHeaders.isEmpty()) { |
| + for (Entry<String, String> entry : mHeaders.entrySet()) { |
| + nativeAddHeader(entry.getKey(), entry.getValue()); |
| + } |
| + } |
| + |
| + if (mAdditionalHeaders != null && !mAdditionalHeaders.isEmpty()) { |
| + for (Entry<String, String> entry : mAdditionalHeaders.entrySet()) { |
| + nativeAddHeader(entry.getKey(), entry.getValue()); |
| + } |
| + } |
| + |
| + nativeStart(); |
| + } |
| + |
| + if (mPostBodyChannel != null) { |
| + uploadFromChannel(mPostBodyChannel); |
| + } |
| + } |
| + |
| + /** |
| + * Uploads data from a {@code ReadableByteChannel} using chunked transfer encoding. |
| + * |
| + * The native call to append a chunk is asynchronous so a semaphore is used to delay |
| + * writing into the buffer again until chromium is finished with it. |
| + * |
| + * @param channel the channel to read data from. |
| + */ |
| + private void uploadFromChannel(ReadableByteChannel channel) { |
| + ByteBuffer buffer = ByteBuffer.allocateDirect(UPLOAD_BYTE_BUFFER_SIZE); |
| + |
| + // The chromium API requires us to specify in advance if a chunk is the last one. |
| + // This extra ByteBuffer is needed to peek ahead and check for the end of the channel. |
| + ByteBuffer checkForEnd = ByteBuffer.allocate(1); |
| + |
| + try { |
| + boolean lastChunk; |
| + do { |
| + // First dump in the one byte we read to check for the end of the channel. |
| + // (The first time through the loop the checkForEnd buffer will be empty) |
| + checkForEnd.flip(); |
| + buffer.clear(); |
| + buffer.put(checkForEnd); |
| + checkForEnd.clear(); |
| + |
| + channel.read(buffer); |
| + lastChunk = channel.read(checkForEnd) <= 0; |
| + buffer.flip(); |
| + nativeAppendChunk(buffer, buffer.limit(), lastChunk); |
| + |
| + if (lastChunk) { |
| + break; |
| + } |
| + |
| + // Acquire permit before writing to the buffer again to ensure chromium is done with it. |
| + mAppendChunkSemaphore.acquire(); |
| + |
| + } while (!lastChunk && !mFinished); |
| + |
| + } catch (IOException e) { |
| + mSinkException = e; |
| + cancel(); |
| + } catch (InterruptedException e) { |
| + mSinkException = new IOException(e); |
| + cancel(); |
| + } finally { |
| + try { |
| + mPostBodyChannel.close(); |
| + } catch (IOException ignore) { |
| + ; |
| + } |
| + } |
| + } |
| + |
| + public synchronized void cancel() { |
| + if (mCanceled) { |
| + return; |
| + } |
| + |
| + mCanceled = true; |
| + |
| + if (!mRecycled) { |
| + nativeCancel(); |
| + } |
| + } |
| + |
| + public synchronized boolean isCanceled() { |
| + return mCanceled; |
| + } |
| + |
| + public synchronized boolean isRecycled() { |
| + return mRecycled; |
| + } |
| + |
| + /** |
| + * Returns an exception if any, or null if the request was completed successfully. |
| + */ |
| + public IOException getException() { |
| + if (mSinkException != null) { |
| + return mSinkException; |
| + } |
| + |
| + validateNotRecycled(); |
| + |
| + int errorCode = nativeGetErrorCode(); |
| + switch (errorCode) { |
| + case SUCCESS: |
| + return null; |
| + case UNKNOWN_ERROR: |
| + return new IOException(nativeGetErrorString()); |
| + case MALFORMED_URL_ERROR: |
| + return new MalformedURLException("Malformed URL: " + mUrl); |
| + case CONNECTION_TIMED_OUT: |
| + return new ConnectTimeoutException("Connection timed out"); |
| + case UNKNOWN_HOST: |
| + String host; |
| + try { |
| + host = new URL(mUrl).getHost(); |
| + } catch (MalformedURLException e) { |
| + host = mUrl; |
| + } |
| + return new UnknownHostException("Unknown host: " + host); |
| + default: |
| + throw new IllegalStateException("Unrecognized error code: " + errorCode); |
| + } |
| + } |
| + |
| + public native int getHttpStatusCode(); |
| + |
| + /** |
| + * Content length as reported by the server. May be -1 or incorrect if the server returns the |
| + * wrong number, which happens even with Google servers. |
| + */ |
| + public long getContentLength() { |
| + return mContentLength; |
| + } |
| + |
| + public String getContentType() { |
| + return mContentType; |
| + } |
| + |
| + /** |
| + * A callback invoked when appending a chunk to the request has completed. |
| + */ |
| + @CalledByNative |
| + protected void onAppendChunkCompleted() { |
| + mAppendChunkSemaphore.release(); |
| + } |
| + |
| + /** |
| + * A callback invoked when the first chunk of the response has arrived. |
| + */ |
| + @CalledByNative |
| + protected void onResponseStarted() { |
| + mContentType = nativeGetContentType(); |
| + mContentLength = nativeGetContentLength(); |
| + } |
| + |
| + /** |
| + * A callback invoked when the response has been fully consumed. |
| + */ |
| + protected void onRequestComplete() { |
| + } |
| + |
| + /** |
| + * Consumes a portion of the response. |
| + * |
| + * @param byteBuffer The ByteBuffer to append. Must be a direct buffer, and no references to it |
| + * may be retained after the method ends, as it wraps code managed on the native heap. |
| + */ |
| + @CalledByNative |
| + protected void onBytesRead(ByteBuffer byteBuffer) { |
| + try { |
| + while (byteBuffer.hasRemaining()) { |
| + mSink.write(byteBuffer); |
| + } |
| + } catch (IOException e) { |
| + mSinkException = e; |
| + cancel(); |
| + } |
| + } |
| + |
| + /** |
| + * Notifies the listener, releases native data structures. |
| + */ |
| + @SuppressWarnings("unused") |
| + @CalledByNative |
| + private synchronized void finish() { |
| + mFinished = true; |
| + if (mAppendChunkSemaphore != null) { |
| + mAppendChunkSemaphore.release(); |
| + } |
| + |
| + if (mRecycled) { |
| + return; |
| + } |
| + try { |
| + mSink.close(); |
| + } catch (IOException e) { |
| + // Ignore |
| + } |
| + onRequestComplete(); |
| + nativeRecycle(); |
| + mRecycled = true; |
| + } |
| + |
| + private void validateNotRecycled() { |
| + if (mRecycled) { |
| + throw new IllegalStateException("Accessing recycled request"); |
| + } |
| + } |
| + |
| + private void validateNotStarted() { |
| + if (mStarted) { |
| + throw new IllegalStateException("Request already started"); |
| + } |
| + } |
| + |
| + public String getUrl() { |
| + return mUrl; |
| + } |
| + |
| + private native void nativeInit(UrlRequestContext requestContext, String url, int priority); |
| + private native void nativeAddHeader(String name, String value); |
| + private native void nativeSetPostData(String contentType, byte[] content); |
| + private native void nativeBeginChunkedUpload(String contentType); |
| + private native void nativeAppendChunk(ByteBuffer chunk, int chunkSize, boolean isLastChunk); |
| + private native void nativeStart(); |
| + private native void nativeCancel(); |
| + private native void nativeRecycle(); |
| + private native int nativeGetErrorCode(); |
| + private native String nativeGetErrorString(); |
| + private native String nativeGetContentType(); |
| + private native long nativeGetContentLength(); |
| +} |