| // 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.ProtocolException; |
| import java.net.URL; |
| import java.nio.ByteBuffer; |
| import java.nio.channels.ReadableByteChannel; |
| import java.nio.channels.WritableByteChannel; |
| import java.util.List; |
| 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 int mUploadContentLength; |
| |
| 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 String mMethod; |
| |
| 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, long contentLength) { |
| validateNotStarted(); |
| if (contentLength > Integer.MAX_VALUE) { |
| throw new IllegalArgumentException( |
| "Upload contentLength is too big."); |
| } |
| mUploadContentLength = (int)contentLength; |
| mPostContentType = contentType; |
| mPostDataChannel = channel; |
| mPostData = null; |
| } |
| |
| |
| @Override |
| public void setHttpMethod(String method) { |
| validateNotStarted(); |
| if (!("PUT".equals(method) || "POST".equals(method))) { |
| throw new IllegalArgumentException( |
| "Only PUT and POST are allowed."); |
| } |
| mMethod = method; |
| } |
| |
| @Override |
| public void start() { |
| getExecutor().execute(new Runnable() { |
| @Override |
| public void run() { |
| startOnExecutorThread(); |
| } |
| }); |
| } |
| |
| private void startOnExecutorThread() { |
| boolean readingResponse = false; |
| try { |
| synchronized (mLock) { |
| if (mCanceled) { |
| return; |
| } |
| } |
| |
| URL url = new URL(mUrl); |
| mConnection = (HttpURLConnection)url.openConnection(); |
| // If configured, use the provided http verb. |
| if (mMethod != null) { |
| try { |
| mConnection.setRequestMethod(mMethod); |
| } catch (ProtocolException e) { |
| // Since request hasn't started earlier, it |
| // must be an illegal HTTP verb. |
| throw new IllegalArgumentException(e); |
| } |
| } |
| 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; |
| } |
| |
| mListener.onResponseStarted(this); |
| |
| 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 { |
| if (mPostDataChannel != null) { |
| try { |
| mPostDataChannel.close(); |
| } catch (IOException e) { |
| // Ignore |
| } |
| } |
| |
| // 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.setFixedLengthStreamingMode(mUploadContentLength); |
| 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; |
| } |
| |
| |
| @Override |
| public String getHeader(String name) { |
| if (mConnection == null) { |
| throw new IllegalStateException("Response headers not available"); |
| } |
| Map<String, List<String>> headerFields = mConnection.getHeaderFields(); |
| if (headerFields != null) { |
| List<String> headerValues = headerFields.get(name); |
| if (headerValues != null) { |
| return TextUtils.join(", ", headerValues); |
| } |
| } |
| return null; |
| } |
| |
| private void validateNotStarted() { |
| if (mStarted) { |
| throw new IllegalStateException("Request already started"); |
| } |
| } |
| } |