Index: components/cronet/android/java/src/org/chromium/net/ChromiumUrlRequest.java |
diff --git a/components/cronet/android/java/src/org/chromium/net/ChromiumUrlRequest.java b/components/cronet/android/java/src/org/chromium/net/ChromiumUrlRequest.java |
index a643a1456230dc0d57adbba85a7644b460518b5a..db833d782842dcf26d6094659e6e6332da7ce776 100644 |
--- a/components/cronet/android/java/src/org/chromium/net/ChromiumUrlRequest.java |
+++ b/components/cronet/android/java/src/org/chromium/net/ChromiumUrlRequest.java |
@@ -4,35 +4,63 @@ |
package org.chromium.net; |
+import org.apache.http.conn.ConnectTimeoutException; |
+import org.chromium.base.CalledByNative; |
+import org.chromium.base.JNINamespace; |
+ |
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.ArrayList; |
+import java.util.HashMap; |
+import java.util.List; |
import java.util.Map; |
+import java.util.Map.Entry; |
/** |
* Network request using the native http stack implementation. |
*/ |
-public class ChromiumUrlRequest extends UrlRequest implements HttpUrlRequest { |
- |
+@JNINamespace("cronet") |
+public class ChromiumUrlRequest implements HttpUrlRequest { |
+ /** |
+ * Native adapter object, owned by UrlRequest. |
+ */ |
+ private long mUrlRequestAdapter; |
+ private final ChromiumUrlRequestContext 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 mUploadContentType; |
+ private String mMethod; |
+ private byte[] mUploadData; |
+ private ReadableByteChannel mUploadChannel; |
+ private WritableByteChannel mOutputChannel; |
+ private IOException mSinkException; |
+ private volatile boolean mStarted; |
+ private volatile boolean mCanceled; |
+ private volatile boolean mRecycled; |
+ private volatile boolean mFinished; |
+ private boolean mHeadersAvailable; |
+ private String mContentType; |
+ private long mUploadContentLength; |
private final HttpUrlRequestListener mListener; |
- |
private boolean mBufferFullResponse; |
- |
private long mOffset; |
- |
private long mContentLength; |
- |
private long mContentLengthLimit; |
- |
private boolean mCancelIfContentLengthOverLimit; |
- |
private boolean mContentLengthOverLimit; |
- |
private boolean mSkippingToOffset; |
- |
private long mSize; |
+ private final Object mLock = new Object(); |
- public ChromiumUrlRequest(UrlRequestContext requestContext, |
+ public ChromiumUrlRequest(ChromiumUrlRequestContext requestContext, |
String url, int priority, Map<String, String> headers, |
HttpUrlRequestListener listener) { |
this(requestContext, url, priority, headers, |
@@ -40,29 +68,35 @@ public class ChromiumUrlRequest extends UrlRequest implements HttpUrlRequest { |
mBufferFullResponse = true; |
} |
- public ChromiumUrlRequest(UrlRequestContext requestContext, |
+ /** |
+ * 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. |
+ */ |
+ public ChromiumUrlRequest(ChromiumUrlRequestContext requestContext, |
String url, int priority, Map<String, String> headers, |
WritableByteChannel sink, HttpUrlRequestListener listener) { |
- super(requestContext, url, convertRequestPriority(priority), headers, |
- sink); |
- mListener = listener; |
- } |
- |
- private static int convertRequestPriority(int priority) { |
- switch (priority) { |
- case HttpUrlRequest.REQUEST_PRIORITY_IDLE: |
- return UrlRequestPriority.IDLE; |
- case HttpUrlRequest.REQUEST_PRIORITY_LOWEST: |
- return UrlRequestPriority.LOWEST; |
- case HttpUrlRequest.REQUEST_PRIORITY_LOW: |
- return UrlRequestPriority.LOW; |
- case HttpUrlRequest.REQUEST_PRIORITY_MEDIUM: |
- return UrlRequestPriority.MEDIUM; |
- case HttpUrlRequest.REQUEST_PRIORITY_HIGHEST: |
- return UrlRequestPriority.HIGHEST; |
- default: |
- return UrlRequestPriority.MEDIUM; |
+ if (requestContext == null) { |
+ throw new NullPointerException("Context is required"); |
} |
+ if (url == null) { |
+ throw new NullPointerException("URL is required"); |
+ } |
+ mRequestContext = requestContext; |
+ mUrl = url; |
+ mPriority = convertRequestPriority(priority); |
+ mHeaders = headers; |
+ mSink = sink; |
+ mUrlRequestAdapter = nativeCreateRequestAdapter( |
+ mRequestContext.getChromiumUrlRequestContextAdapter(), |
+ mUrl, |
+ mPriority); |
+ mListener = listener; |
} |
@Override |
@@ -73,6 +107,13 @@ public class ChromiumUrlRequest extends UrlRequest implements HttpUrlRequest { |
} |
} |
+ /** |
+ * The compressed content length as reported by the server. May be -1 if |
+ * the server did not provide a length. Some servers may also report the |
+ * wrong number. Since this is the compressed content length, and only |
+ * uncompressed content is returned by the consumer, the consumer should |
+ * not rely on this value. |
+ */ |
@Override |
public long getContentLength() { |
return mContentLength; |
@@ -85,10 +126,289 @@ public class ChromiumUrlRequest extends UrlRequest implements HttpUrlRequest { |
} |
@Override |
- protected void onResponseStarted() { |
- super.onResponseStarted(); |
+ public int getHttpStatusCode() { |
+ int httpStatusCode = nativeGetHttpStatusCode(mUrlRequestAdapter); |
+ |
+ // TODO(mef): Investigate the following: |
+ // If we have been able to successfully resume a previously interrupted |
+ // download, the status code will be 206, not 200. Since the rest of the |
+ // application is expecting 200 to indicate success, we need to fake it. |
+ if (httpStatusCode == 206) { |
+ httpStatusCode = 200; |
+ } |
+ return httpStatusCode; |
+ } |
+ |
+ /** |
+ * Returns an exception if any, or null if the request was completed |
+ * successfully. |
+ */ |
+ @Override |
+ public IOException getException() { |
+ if (mSinkException != null) { |
+ return mSinkException; |
+ } |
+ |
+ validateNotRecycled(); |
+ |
+ int errorCode = nativeGetErrorCode(mUrlRequestAdapter); |
+ switch (errorCode) { |
+ case ChromiumUrlRequestError.SUCCESS: |
+ if (mContentLengthOverLimit) { |
+ return new ResponseTooLargeException(); |
+ } |
+ return null; |
+ case ChromiumUrlRequestError.UNKNOWN: |
+ return new IOException( |
+ nativeGetErrorString(mUrlRequestAdapter)); |
+ case ChromiumUrlRequestError.MALFORMED_URL: |
+ return new MalformedURLException("Malformed URL: " + mUrl); |
+ case ChromiumUrlRequestError.CONNECTION_TIMED_OUT: |
+ return new ConnectTimeoutException("Connection timed out"); |
+ case ChromiumUrlRequestError.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); |
+ } |
+ } |
+ |
+ @Override |
+ public ByteBuffer getByteBuffer() { |
+ return ((ChunkedWritableByteChannel)getSink()).getByteBuffer(); |
+ } |
+ |
+ @Override |
+ public byte[] getResponseAsBytes() { |
+ return ((ChunkedWritableByteChannel)getSink()).getBytes(); |
+ } |
+ |
+ /** |
+ * Adds a request header. Must be done before request has started. |
+ */ |
+ public void addHeader(String header, String value) { |
+ synchronized (mLock) { |
+ validateNotStarted(); |
+ if (mAdditionalHeaders == null) { |
+ mAdditionalHeaders = new HashMap<String, String>(); |
+ } |
+ mAdditionalHeaders.put(header, value); |
+ } |
+ } |
+ |
+ /** |
+ * Sets data to upload as part of a POST or PUT request. |
+ * |
+ * @param contentType MIME type of the upload content or null if this is not |
+ * an upload. |
+ * @param data The content that needs to be uploaded. |
+ */ |
+ public void setUploadData(String contentType, byte[] data) { |
+ synchronized (mLock) { |
+ validateNotStarted(); |
+ mUploadContentType = contentType; |
+ mUploadData = data; |
+ mUploadChannel = null; |
+ } |
+ } |
+ |
+ /** |
+ * Sets a readable byte channel to upload as part of a POST or PUT request. |
+ * |
+ * @param contentType MIME type of the upload content or null if this is not |
+ * an upload request. |
+ * @param channel The channel to read to read upload data from if this is an |
+ * upload request. |
+ * @param contentLength The length of data to upload. |
+ */ |
+ public void setUploadChannel(String contentType, |
+ ReadableByteChannel channel, long contentLength) { |
+ synchronized (mLock) { |
+ validateNotStarted(); |
+ mUploadContentType = contentType; |
+ mUploadChannel = channel; |
+ mUploadContentLength = contentLength; |
+ mUploadData = null; |
+ } |
+ } |
+ |
+ /** |
+ * Sets HTTP method for upload request. Only PUT or POST are allowed. |
+ */ |
+ public void setHttpMethod(String method) { |
+ validateNotStarted(); |
+ if (!("PUT".equals(method) || "POST".equals(method))) { |
+ throw new IllegalArgumentException("Only PUT or POST are allowed."); |
+ } |
+ mMethod = method; |
+ } |
+ |
+ public WritableByteChannel getSink() { |
+ return mSink; |
+ } |
+ |
+ public void start() { |
+ synchronized (mLock) { |
+ if (mCanceled) { |
+ return; |
+ } |
+ |
+ validateNotStarted(); |
+ validateNotRecycled(); |
+ |
+ mStarted = true; |
+ |
+ String method = mMethod; |
+ if (method == null && |
+ ((mUploadData != null && mUploadData.length > 0) || |
+ mUploadChannel != null)) { |
+ // Default to POST if there is data to upload but no method was |
+ // specified. |
+ method = "POST"; |
+ } |
+ |
+ if (method != null) { |
+ nativeSetMethod(mUrlRequestAdapter, method); |
+ } |
+ |
+ if (mHeaders != null && !mHeaders.isEmpty()) { |
+ for (Entry<String, String> entry : mHeaders.entrySet()) { |
+ nativeAddHeader(mUrlRequestAdapter, entry.getKey(), |
+ entry.getValue()); |
+ } |
+ } |
+ |
+ if (mAdditionalHeaders != null) { |
+ for (Entry<String, String> entry : |
+ mAdditionalHeaders.entrySet()) { |
+ nativeAddHeader(mUrlRequestAdapter, entry.getKey(), |
+ entry.getValue()); |
+ } |
+ } |
+ |
+ if (mUploadData != null && mUploadData.length > 0) { |
+ nativeSetUploadData(mUrlRequestAdapter, mUploadContentType, |
+ mUploadData); |
+ } else if (mUploadChannel != null) { |
+ nativeSetUploadChannel(mUrlRequestAdapter, mUploadContentType, |
+ mUploadContentLength); |
+ } |
+ |
+ nativeStart(mUrlRequestAdapter); |
+ } |
+ } |
+ |
+ public void cancel() { |
+ synchronized (mLock) { |
+ if (mCanceled) { |
+ return; |
+ } |
+ |
+ mCanceled = true; |
+ |
+ if (!mRecycled) { |
+ nativeCancel(mUrlRequestAdapter); |
+ } |
+ } |
+ } |
+ |
+ public boolean isCanceled() { |
+ synchronized (mLock) { |
+ return mCanceled; |
+ } |
+ } |
+ |
+ public boolean isRecycled() { |
+ synchronized (mLock) { |
+ return mRecycled; |
+ } |
+ } |
+ |
+ public String getContentType() { |
+ return mContentType; |
+ } |
+ |
+ public String getHeader(String name) { |
+ validateHeadersAvailable(); |
+ return nativeGetHeader(mUrlRequestAdapter, name); |
+ } |
+ |
+ // All response headers. |
+ public Map<String, List<String>> getAllHeaders() { |
+ validateHeadersAvailable(); |
+ ResponseHeadersMap result = new ResponseHeadersMap(); |
+ nativeGetAllHeaders(mUrlRequestAdapter, result); |
+ return result; |
+ } |
+ |
+ public String getUrl() { |
+ return mUrl; |
+ } |
+ |
+ private static int convertRequestPriority(int priority) { |
+ switch (priority) { |
+ case HttpUrlRequest.REQUEST_PRIORITY_IDLE: |
+ return ChromiumUrlRequestPriority.IDLE; |
+ case HttpUrlRequest.REQUEST_PRIORITY_LOWEST: |
+ return ChromiumUrlRequestPriority.LOWEST; |
+ case HttpUrlRequest.REQUEST_PRIORITY_LOW: |
+ return ChromiumUrlRequestPriority.LOW; |
+ case HttpUrlRequest.REQUEST_PRIORITY_MEDIUM: |
+ return ChromiumUrlRequestPriority.MEDIUM; |
+ case HttpUrlRequest.REQUEST_PRIORITY_HIGHEST: |
+ return ChromiumUrlRequestPriority.HIGHEST; |
+ default: |
+ return ChromiumUrlRequestPriority.MEDIUM; |
+ } |
+ } |
+ |
+ private void onContentLengthOverLimit() { |
+ mContentLengthOverLimit = true; |
+ cancel(); |
+ } |
+ |
+ /** |
+ * A callback invoked when the response has been fully consumed. |
+ */ |
+ private void onRequestComplete() { |
+ mListener.onRequestComplete(this); |
+ } |
+ |
+ private void validateNotRecycled() { |
+ if (mRecycled) { |
+ throw new IllegalStateException("Accessing recycled request"); |
+ } |
+ } |
+ |
+ private void validateNotStarted() { |
+ if (mStarted) { |
+ throw new IllegalStateException("Request already started"); |
+ } |
+ } |
+ |
+ private void validateHeadersAvailable() { |
+ if (!mHeadersAvailable) { |
+ throw new IllegalStateException("Response headers not available"); |
+ } |
+ } |
+ |
+ // Private methods called by native library. |
+ |
+ /** |
+ * A callback invoked when the first chunk of the response has arrived. |
+ */ |
+ @CalledByNative |
+ private void onResponseStarted() { |
+ mContentType = nativeGetContentType(mUrlRequestAdapter); |
+ mContentLength = nativeGetContentLength(mUrlRequestAdapter); |
+ mHeadersAvailable = true; |
- mContentLength = super.getContentLength(); |
if (mContentLengthLimit > 0 && mContentLength > mContentLengthLimit |
&& mCancelIfContentLengthOverLimit) { |
onContentLengthOverLimit(); |
@@ -102,8 +422,11 @@ public class ChromiumUrlRequest extends UrlRequest implements HttpUrlRequest { |
} |
if (mOffset != 0) { |
- // The server may ignore the request for a byte range. |
- if (super.getHttpStatusCode() == 200) { |
+ // The server may ignore the request for a byte range, in which case |
+ // status code will be 200, instead of 206. Note that we cannot call |
+ // getHttpStatusCode as it rewrites 206 into 200. |
+ if (nativeGetHttpStatusCode(mUrlRequestAdapter) == 200) { |
mef
2014/08/14 21:46:22
Using nativeGetHttpStatusCode avoids rewriting in
|
+ // TODO(mef): Revisit this logic. |
if (mContentLength != -1) { |
mContentLength -= mOffset; |
} |
@@ -115,8 +438,15 @@ public class ChromiumUrlRequest extends UrlRequest implements HttpUrlRequest { |
mListener.onResponseStarted(this); |
} |
- @Override |
- protected void onBytesRead(ByteBuffer buffer) { |
+ /** |
+ * 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 |
+ private void onBytesRead(ByteBuffer buffer) { |
if (mContentLengthOverLimit) { |
return; |
} |
@@ -132,58 +462,130 @@ public class ChromiumUrlRequest extends UrlRequest implements HttpUrlRequest { |
} |
} |
- if (mContentLengthLimit != 0 && mSize > mContentLengthLimit) { |
+ boolean contentLengthOverLimit = |
+ (mContentLengthLimit != 0 && mSize > mContentLengthLimit); |
+ if (contentLengthOverLimit) { |
buffer.limit(size - (int)(mSize - mContentLengthLimit)); |
- super.onBytesRead(buffer); |
- onContentLengthOverLimit(); |
- return; |
} |
- super.onBytesRead(buffer); |
+ try { |
+ while (buffer.hasRemaining()) { |
+ mSink.write(buffer); |
+ } |
+ } catch (IOException e) { |
+ mSinkException = e; |
+ cancel(); |
+ } |
+ if (contentLengthOverLimit) { |
+ onContentLengthOverLimit(); |
+ } |
} |
- private void onContentLengthOverLimit() { |
- mContentLengthOverLimit = true; |
- cancel(); |
- } |
+ /** |
+ * Notifies the listener, releases native data structures. |
+ */ |
+ @SuppressWarnings("unused") |
+ @CalledByNative |
+ private void finish() { |
+ synchronized (mLock) { |
+ mFinished = true; |
- @Override |
- protected void onRequestComplete() { |
- mListener.onRequestComplete(this); |
+ if (mRecycled) { |
+ return; |
+ } |
+ try { |
+ mSink.close(); |
+ } catch (IOException e) { |
+ // Ignore |
+ } |
+ onRequestComplete(); |
+ nativeDestroyRequestAdapter(mUrlRequestAdapter); |
+ mUrlRequestAdapter = 0; |
+ mRecycled = true; |
+ } |
} |
- @Override |
- public int getHttpStatusCode() { |
- int httpStatusCode = super.getHttpStatusCode(); |
- |
- // TODO(mef): Investigate the following: |
- // If we have been able to successfully resume a previously interrupted |
- // download, |
- // the status code will be 206, not 200. Since the rest of the |
- // application is |
- // expecting 200 to indicate success, we need to fake it. |
- if (httpStatusCode == 206) { |
- httpStatusCode = 200; |
+ /** |
+ * Appends header |name| with value |value| to |headersMap|. |
+ */ |
+ @SuppressWarnings("unused") |
+ @CalledByNative |
+ private void onAppendResponseHeader(ResponseHeadersMap headersMap, |
+ String name, String value) { |
+ if (!headersMap.containsKey(name)) { |
+ headersMap.put(name, new ArrayList<String>()); |
} |
- return httpStatusCode; |
+ headersMap.get(name).add(value); |
} |
- @Override |
- public IOException getException() { |
- IOException ex = super.getException(); |
- if (ex == null && mContentLengthOverLimit) { |
- ex = new ResponseTooLargeException(); |
+ /** |
+ * Reads a sequence of bytes from upload channel into the given buffer. |
+ * @param dest The buffer into which bytes are to be transferred. |
+ * @return Returns number of bytes read (could be 0) or -1 and closes |
+ * the channel if error occured. |
+ */ |
+ @SuppressWarnings("unused") |
+ @CalledByNative |
+ private int readFromUploadChannel(ByteBuffer dest) { |
+ if (mUploadChannel == null || !mUploadChannel.isOpen()) |
+ return -1; |
+ try { |
+ int result = mUploadChannel.read(dest); |
+ if (result < 0) { |
+ mUploadChannel.close(); |
+ return 0; |
+ } |
+ return result; |
+ } catch (IOException e) { |
+ mSinkException = e; |
+ try { |
+ mUploadChannel.close(); |
+ } catch (IOException ignored) { |
+ // Ignore this exception. |
+ } |
+ cancel(); |
+ return -1; |
} |
- return ex; |
} |
- @Override |
- public ByteBuffer getByteBuffer() { |
- return ((ChunkedWritableByteChannel)getSink()).getByteBuffer(); |
- } |
+ // Native methods are implemented in chromium_url_request.cc. |
- @Override |
- public byte[] getResponseAsBytes() { |
- return ((ChunkedWritableByteChannel)getSink()).getBytes(); |
+ private native long nativeCreateRequestAdapter( |
+ long ChromiumUrlRequestContextAdapter, String url, int priority); |
+ |
+ private native void nativeAddHeader(long urlRequestAdapter, String name, |
+ String value); |
+ |
+ private native void nativeSetMethod(long urlRequestAdapter, String method); |
+ |
+ private native void nativeSetUploadData(long urlRequestAdapter, |
+ String contentType, byte[] content); |
+ |
+ private native void nativeSetUploadChannel(long urlRequestAdapter, |
+ String contentType, long contentLength); |
+ |
+ private native void nativeStart(long urlRequestAdapter); |
+ |
+ private native void nativeCancel(long urlRequestAdapter); |
+ |
+ private native void nativeDestroyRequestAdapter(long urlRequestAdapter); |
+ |
+ private native int nativeGetErrorCode(long urlRequestAdapter); |
+ |
+ private native int nativeGetHttpStatusCode(long urlRequestAdapter); |
+ |
+ private native String nativeGetErrorString(long urlRequestAdapter); |
+ |
+ private native String nativeGetContentType(long urlRequestAdapter); |
+ |
+ private native long nativeGetContentLength(long urlRequestAdapter); |
+ |
+ private native String nativeGetHeader(long urlRequestAdapter, String name); |
+ |
+ private native void nativeGetAllHeaders(long urlRequestAdapter, |
+ ResponseHeadersMap headers); |
+ |
+ // Explicit class to work around JNI-generator generics confusion. |
+ private class ResponseHeadersMap extends HashMap<String, List<String>> { |
} |
} |