Index: net/cronet/android/java/src/org/chromium/net/HttpUrlConnectionUrlRequest.java |
diff --git a/net/cronet/android/java/src/org/chromium/net/HttpUrlConnectionUrlRequest.java b/net/cronet/android/java/src/org/chromium/net/HttpUrlConnectionUrlRequest.java |
new file mode 100644 |
index 0000000000000000000000000000000000000000..21ce9c94bf196dfd40d18dee435d17c47a51b28c |
--- /dev/null |
+++ b/net/cronet/android/java/src/org/chromium/net/HttpUrlConnectionUrlRequest.java |
@@ -0,0 +1,433 @@ |
+// Copyright 2014 The Chromium Authors. All rights reserved. |
+// 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 android.content.Context; |
+import android.text.TextUtils; |
+ |
+import org.apache.http.HttpStatus; |
+ |
+import java.io.FileNotFoundException; |
+import java.io.IOException; |
+import java.io.InputStream; |
+import java.io.OutputStream; |
+import java.net.HttpURLConnection; |
+import java.net.URL; |
+import java.nio.ByteBuffer; |
+import java.nio.channels.ReadableByteChannel; |
+import java.nio.channels.WritableByteChannel; |
+import java.util.Map; |
+import java.util.Map.Entry; |
+import java.util.concurrent.ExecutorService; |
+import java.util.concurrent.Executors; |
+import java.util.concurrent.ThreadFactory; |
+import java.util.concurrent.atomic.AtomicInteger; |
+import java.util.zip.GZIPInputStream; |
+ |
+/** |
+ * Network request using the HttpUrlConnection implementation. |
+ */ |
+class HttpUrlConnectionUrlRequest implements HttpUrlRequest { |
+ |
+ private static final int MAX_CHUNK_SIZE = 8192; |
+ |
+ private static final int CONNECT_TIMEOUT = 3000; |
+ |
+ private static final int READ_TIMEOUT = 90000; |
+ |
+ private final Context mContext; |
+ |
+ private final String mUrl; |
+ |
+ private final Map<String, String> mHeaders; |
+ |
+ private final WritableByteChannel mSink; |
+ |
+ private final HttpUrlRequestListener mListener; |
+ |
+ private IOException mException; |
+ |
+ private HttpURLConnection mConnection; |
+ |
+ private long mOffset; |
+ |
+ private int mContentLength; |
+ |
+ private long mContentLengthLimit; |
+ |
+ private boolean mCancelIfContentLengthOverLimit; |
+ |
+ private boolean mContentLengthOverLimit; |
+ |
+ private boolean mSkippingToOffset; |
+ |
+ private long mSize; |
+ |
+ private String mPostContentType; |
+ |
+ private byte[] mPostData; |
+ |
+ private ReadableByteChannel mPostDataChannel; |
+ |
+ private String mContentType; |
+ |
+ private int mHttpStatusCode; |
+ |
+ private boolean mStarted; |
+ |
+ private boolean mCanceled; |
+ |
+ private InputStream mResponseStream; |
+ |
+ private final Object mLock; |
+ |
+ private static ExecutorService sExecutorService; |
+ |
+ private static final Object sExecutorServiceLock = new Object(); |
+ |
+ HttpUrlConnectionUrlRequest(Context context, String url, |
+ int requestPriority, Map<String, String> headers, |
+ HttpUrlRequestListener listener) { |
+ this(context, url, requestPriority, headers, |
+ new ChunkedWritableByteChannel(), listener); |
+ } |
+ |
+ HttpUrlConnectionUrlRequest(Context context, String url, |
+ int requestPriority, Map<String, String> headers, |
+ WritableByteChannel sink, HttpUrlRequestListener listener) { |
+ if (context == null) { |
+ throw new NullPointerException("Context is required"); |
+ } |
+ if (url == null) { |
+ throw new NullPointerException("URL is required"); |
+ } |
+ mContext = context; |
+ mUrl = url; |
+ mHeaders = headers; |
+ mSink = sink; |
+ mListener = listener; |
+ mLock = new Object(); |
+ } |
+ |
+ private static ExecutorService getExecutor() { |
+ synchronized (sExecutorServiceLock) { |
+ if (sExecutorService == null) { |
+ ThreadFactory threadFactory = new ThreadFactory() { |
+ private final AtomicInteger mCount = new AtomicInteger(1); |
+ |
+ @Override |
+ public Thread newThread(Runnable r) { |
+ Thread thread = new Thread(r, |
+ "HttpUrlConnection #" + |
+ mCount.getAndIncrement()); |
+ // Note that this thread is not doing actual networking. |
+ // It's only a controller. |
+ thread.setPriority(Thread.NORM_PRIORITY); |
+ return thread; |
+ } |
+ }; |
+ sExecutorService = Executors.newCachedThreadPool(threadFactory); |
+ } |
+ return sExecutorService; |
+ } |
+ } |
+ |
+ @Override |
+ public String getUrl() { |
+ return mUrl; |
+ } |
+ |
+ @Override |
+ public void setOffset(long offset) { |
+ mOffset = offset; |
+ } |
+ |
+ @Override |
+ public void setContentLengthLimit(long limit, boolean cancelEarly) { |
+ mContentLengthLimit = limit; |
+ mCancelIfContentLengthOverLimit = cancelEarly; |
+ } |
+ |
+ @Override |
+ public void setUploadData(String contentType, byte[] data) { |
+ validateNotStarted(); |
+ mPostContentType = contentType; |
+ mPostData = data; |
+ mPostDataChannel = null; |
+ } |
+ |
+ @Override |
+ public void setUploadChannel(String contentType, |
+ ReadableByteChannel channel) { |
+ validateNotStarted(); |
+ mPostContentType = contentType; |
+ mPostDataChannel = channel; |
+ mPostData = null; |
+ } |
+ |
+ @Override |
+ public void start() { |
+ boolean readingResponse = false; |
+ try { |
+ synchronized (mLock) { |
+ if (mCanceled) { |
+ return; |
+ } |
+ } |
+ |
+ URL url = new URL(mUrl); |
+ mConnection = (HttpURLConnection)url.openConnection(); |
+ mConnection.setConnectTimeout(CONNECT_TIMEOUT); |
+ mConnection.setReadTimeout(READ_TIMEOUT); |
+ mConnection.setInstanceFollowRedirects(true); |
+ if (mHeaders != null) { |
+ for (Entry<String, String> header : mHeaders.entrySet()) { |
+ mConnection.setRequestProperty(header.getKey(), |
+ header.getValue()); |
+ } |
+ } |
+ |
+ if (mOffset != 0) { |
+ mConnection.setRequestProperty("Range", |
+ "bytes=" + mOffset + "-"); |
+ } |
+ |
+ if (mConnection.getRequestProperty("User-Agent") == null) { |
+ mConnection.setRequestProperty("User-Agent", |
+ UserAgent.from(mContext)); |
+ } |
+ |
+ if (mPostData != null || mPostDataChannel != null) { |
+ uploadData(); |
+ } |
+ |
+ InputStream stream = null; |
+ try { |
+ // We need to open the stream before asking for the response |
+ // code. |
+ stream = mConnection.getInputStream(); |
+ } catch (FileNotFoundException ex) { |
+ // Ignore - the response has no body. |
+ } |
+ |
+ mHttpStatusCode = mConnection.getResponseCode(); |
+ mContentType = mConnection.getContentType(); |
+ mContentLength = mConnection.getContentLength(); |
+ if (mContentLengthLimit > 0 && mContentLength > mContentLengthLimit |
+ && mCancelIfContentLengthOverLimit) { |
+ onContentLengthOverLimit(); |
+ return; |
+ } |
+ |
+ mResponseStream = isError(mHttpStatusCode) ? mConnection |
+ .getErrorStream() |
+ : stream; |
+ |
+ if (mResponseStream != null |
+ && "gzip".equals(mConnection.getContentEncoding())) { |
+ mResponseStream = new GZIPInputStream(mResponseStream); |
+ mContentLength = -1; |
+ } |
+ |
+ if (mOffset != 0) { |
+ // The server may ignore the request for a byte range. |
+ if (mHttpStatusCode == HttpStatus.SC_OK) { |
+ if (mContentLength != -1) { |
+ mContentLength -= mOffset; |
+ } |
+ mSkippingToOffset = true; |
+ } else { |
+ mSize = mOffset; |
+ } |
+ } |
+ |
+ if (mResponseStream != null) { |
+ readingResponse = true; |
+ readResponseAsync(); |
+ } |
+ } catch (IOException e) { |
+ mException = e; |
+ } finally { |
+ // Don't call onRequestComplete yet if we are reading the response |
+ // on a separate thread |
+ if (!readingResponse) { |
+ mListener.onRequestComplete(this); |
+ } |
+ } |
+ } |
+ |
+ private void uploadData() throws IOException { |
+ mConnection.setDoOutput(true); |
+ if (!TextUtils.isEmpty(mPostContentType)) { |
+ mConnection.setRequestProperty("Content-Type", mPostContentType); |
+ } |
+ |
+ OutputStream uploadStream = null; |
+ try { |
+ if (mPostData != null) { |
+ mConnection.setFixedLengthStreamingMode(mPostData.length); |
+ uploadStream = mConnection.getOutputStream(); |
+ uploadStream.write(mPostData); |
+ } else { |
+ mConnection.setChunkedStreamingMode(MAX_CHUNK_SIZE); |
+ uploadStream = mConnection.getOutputStream(); |
+ byte[] bytes = new byte[MAX_CHUNK_SIZE]; |
+ ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); |
+ while (mPostDataChannel.read(byteBuffer) > 0) { |
+ byteBuffer.flip(); |
+ uploadStream.write(bytes, 0, byteBuffer.limit()); |
+ byteBuffer.clear(); |
+ } |
+ } |
+ } finally { |
+ if (uploadStream != null) { |
+ uploadStream.close(); |
+ } |
+ } |
+ } |
+ |
+ private void readResponseAsync() { |
+ getExecutor().execute(new Runnable() { |
+ @Override |
+ public void run() { |
+ readResponse(); |
+ } |
+ }); |
+ } |
+ |
+ private void readResponse() { |
+ try { |
+ if (mResponseStream != null) { |
+ readResponseStream(); |
+ } |
+ } catch (IOException e) { |
+ mException = e; |
+ } finally { |
+ try { |
+ mConnection.disconnect(); |
+ } catch (ArrayIndexOutOfBoundsException t) { |
+ // Ignore it. |
+ } |
+ |
+ try { |
+ mSink.close(); |
+ } catch (IOException e) { |
+ if (mException == null) { |
+ mException = e; |
+ } |
+ } |
+ } |
+ mListener.onRequestComplete(this); |
+ } |
+ |
+ private void readResponseStream() throws IOException { |
+ byte[] buffer = new byte[MAX_CHUNK_SIZE]; |
+ int size; |
+ while (!isCanceled() && (size = mResponseStream.read(buffer)) != -1) { |
+ int start = 0; |
+ int count = size; |
+ mSize += size; |
+ if (mSkippingToOffset) { |
+ if (mSize <= mOffset) { |
+ continue; |
+ } else { |
+ mSkippingToOffset = false; |
+ start = (int)(mOffset - (mSize - size)); |
+ count -= start; |
+ } |
+ } |
+ |
+ if (mContentLengthLimit != 0 && mSize > mContentLengthLimit) { |
+ count -= (int)(mSize - mContentLengthLimit); |
+ if (count > 0) { |
+ mSink.write(ByteBuffer.wrap(buffer, start, count)); |
+ } |
+ onContentLengthOverLimit(); |
+ return; |
+ } |
+ |
+ mSink.write(ByteBuffer.wrap(buffer, start, count)); |
+ } |
+ } |
+ |
+ @Override |
+ public void cancel() { |
+ synchronized (mLock) { |
+ if (mCanceled) { |
+ return; |
+ } |
+ |
+ mCanceled = true; |
+ } |
+ } |
+ |
+ @Override |
+ public boolean isCanceled() { |
+ synchronized (mLock) { |
+ return mCanceled; |
+ } |
+ } |
+ |
+ @Override |
+ public int getHttpStatusCode() { |
+ int httpStatusCode = mHttpStatusCode; |
+ |
+ // 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 == HttpStatus.SC_PARTIAL_CONTENT) { |
+ httpStatusCode = HttpStatus.SC_OK; |
+ } |
+ return httpStatusCode; |
+ } |
+ |
+ @Override |
+ public IOException getException() { |
+ if (mException == null && mContentLengthOverLimit) { |
+ mException = new ResponseTooLargeException(); |
+ } |
+ return mException; |
+ } |
+ |
+ private void onContentLengthOverLimit() { |
+ mContentLengthOverLimit = true; |
+ cancel(); |
+ } |
+ |
+ private static boolean isError(int statusCode) { |
+ return (statusCode / 100) != 2; |
+ } |
+ |
+ /** |
+ * Returns the response as a ByteBuffer. |
+ */ |
+ @Override |
+ public ByteBuffer getByteBuffer() { |
+ return ((ChunkedWritableByteChannel)mSink).getByteBuffer(); |
+ } |
+ |
+ @Override |
+ public byte[] getResponseAsBytes() { |
+ return ((ChunkedWritableByteChannel)mSink).getBytes(); |
+ } |
+ |
+ @Override |
+ public long getContentLength() { |
+ return mContentLength; |
+ } |
+ |
+ @Override |
+ public String getContentType() { |
+ return mContentType; |
+ } |
+ |
+ private void validateNotStarted() { |
+ if (mStarted) { |
+ throw new IllegalStateException("Request already started"); |
+ } |
+ } |
+} |