Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1136)

Unified Diff: net/cronet/android/java/src/org/chromium/net/HttpUrlConnectionUrlRequest.java

Issue 183333002: Cronet Java wrappers to fallback to HttpUrlConnection if Cronet is not available. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Address formatting comments. Created 6 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
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");
+ }
+ }
+}

Powered by Google App Engine
This is Rietveld 408576698