Chromium Code Reviews| Index: components/cronet/android/java/src/org/chromium/net/CronetUrlRequest.java |
| diff --git a/components/cronet/android/java/src/org/chromium/net/CronetUrlRequest.java b/components/cronet/android/java/src/org/chromium/net/CronetUrlRequest.java |
| index 3a7776dc8520cbabc219fde0c6515e7d1499e808..16a41a67cc1c90dcf29ea63b08d345d0b68254a2 100644 |
| --- a/components/cronet/android/java/src/org/chromium/net/CronetUrlRequest.java |
| +++ b/components/cronet/android/java/src/org/chromium/net/CronetUrlRequest.java |
| @@ -4,38 +4,243 @@ |
| package org.chromium.net; |
| +import android.util.Log; |
| + |
| +import org.chromium.base.CalledByNative; |
| +import org.chromium.base.JNINamespace; |
| + |
| +import java.nio.ByteBuffer; |
| +import java.util.AbstractMap; |
| +import java.util.ArrayList; |
| +import java.util.HashMap; |
| +import java.util.List; |
| +import java.util.Map; |
| +import java.util.concurrent.Executor; |
| + |
| /** |
| * UrlRequest using Chromium HTTP stack implementation. |
| */ |
| -public class CronetUrlRequest implements UrlRequest { |
| +@JNINamespace("cronet") |
| +final class CronetUrlRequest implements UrlRequest { |
| + /** Native adapter object, owned by UrlRequest. */ |
| + private long mUrlRequestAdapter; |
| + private boolean mStarted = false; |
| + private boolean mCanceled = false; |
| + /** Synchronize access to mUrlRequestAdapter, mStarted and mCanceled. */ |
| + private final Object mUrlRequestAdapterLock = new Object(); |
| + private final CronetUrlRequestContext mRequestContext; |
| + private final List<String> mUrlChain = new ArrayList<String>(); |
| + private final int mPriority; |
| + private final UrlRequestListener mListener; |
| + private final Executor mExecutor; |
| + private final String mInitialUrl; |
| + private String mMethod; |
| + private final HeadersList mRequestHeaders = new HeadersList(); |
| + private NativeResponseInfo mResponseInfo; |
| + private OnDataReceivedRunnable mOnDataReceivedTask; |
| + |
| + static final class HeaderEntry extends |
| + AbstractMap.SimpleEntry<String, String> { |
| + public HeaderEntry(String name, String value) { |
| + super(name, value); |
| + } |
| + } |
| + |
| + static final class HeadersList extends ArrayList<HeaderEntry> { |
| + } |
| + |
| + final class OnDataReceivedRunnable implements Runnable { |
| + ByteBuffer mByteBuffer; |
| + public void run() { |
| + if (isCanceled()) { |
| + return; |
| + } |
| + try { |
| + mListener.onDataReceived(CronetUrlRequest.this, |
| + mResponseInfo, mByteBuffer); |
| + mByteBuffer = null; |
| + synchronized (mUrlRequestAdapterLock) { |
| + if (mUrlRequestAdapter == 0) { |
| + return; |
| + } |
| + nativeReceiveData(mUrlRequestAdapter); |
| + } |
| + } catch (Exception e) { |
| + onCalledByNativeException(e); |
| + } |
| + } |
| + } |
| + |
| + static final class NativeResponseInfo implements ResponseInfo { |
| + private final String[] mResponseInfoUrlChain; |
| + private final int mHttpStatusCode; |
| + private final HeadersMap mAllHeaders = new HeadersMap(); |
| + private final boolean mWasCached; |
| + private final String mNegotiatedProtocol; |
| + |
| + NativeResponseInfo(String[] urlChain, int httpStatusCode, |
| + boolean wasCached, String negotiatedProtocol) { |
| + mResponseInfoUrlChain = urlChain; |
| + mHttpStatusCode = httpStatusCode; |
| + mWasCached = wasCached; |
| + mNegotiatedProtocol = negotiatedProtocol; |
| + } |
| + |
| + @Override |
| + public String getUrl() { |
| + return mResponseInfoUrlChain[mResponseInfoUrlChain.length - 1]; |
| + } |
| + |
| + @Override |
| + public String[] getUrlChain() { |
| + return mResponseInfoUrlChain; |
| + } |
| + |
| + @Override |
| + public int getHttpStatusCode() { |
| + return mHttpStatusCode; |
| + } |
| + |
| + @Override |
| + public Map<String, List<String>> getAllHeaders() { |
| + return mAllHeaders; |
| + } |
| + |
| + @Override |
| + public boolean wasCached() { |
| + return mWasCached; |
| + } |
| + |
| + @Override |
| + public String getNegotiatedProtocol() { |
| + return mNegotiatedProtocol; |
| + } |
| + }; |
| + |
| + static final class NativeExtendedResponseInfo implements |
| + ExtendedResponseInfo { |
| + private final ResponseInfo mResponseInfo; |
| + private final long mTotalReceivedBytes; |
| + |
| + NativeExtendedResponseInfo(ResponseInfo responseInfo, |
| + long totalReceivedBytes) { |
| + mResponseInfo = responseInfo; |
| + mTotalReceivedBytes = totalReceivedBytes; |
| + } |
| + |
| + @Override |
| + public ResponseInfo getResponseInfo() { |
| + return mResponseInfo; |
| + } |
| + |
| + @Override |
| + public long getTotalReceivedBytes() { |
| + return mTotalReceivedBytes; |
| + } |
| + }; |
| + |
| + CronetUrlRequest(CronetUrlRequestContext requestContext, |
| + long urlRequestContextAdapter, |
| + String url, |
| + int priority, |
| + UrlRequestListener listener, |
| + Executor executor) { |
| + if (requestContext == null) { |
| + throw new NullPointerException("Context is required"); |
| + } |
| + if (url == null) { |
| + throw new NullPointerException("URL is required"); |
| + } |
| + if (listener == null) { |
| + throw new NullPointerException("Listener is required"); |
| + } |
| + if (executor == null) { |
| + throw new NullPointerException("Executor is required"); |
| + } |
| + |
| + mRequestContext = requestContext; |
| + mInitialUrl = url; |
| + mUrlChain.add(url); |
| + mPriority = convertRequestPriority(priority); |
| + mListener = listener; |
| + mExecutor = executor; |
| + } |
| + |
| @Override |
| public void setHttpMethod(String method) { |
| - |
| + checkNotStarted(); |
| + if (method == null) { |
| + throw new NullPointerException("Method is required."); |
| + } |
| + mMethod = method; |
| } |
| @Override |
| public void addHeader(String header, String value) { |
| - |
| + checkNotStarted(); |
| + if (header == null) { |
| + throw new NullPointerException("Invalid header name."); |
| + } |
| + if (value == null) { |
| + throw new NullPointerException("Invalid header value."); |
| + } |
| + mRequestHeaders.add(new HeaderEntry(header, value)); |
| } |
| @Override |
| - public void start(UrlRequestListener listener) { |
| - |
| + public void start() { |
| + synchronized (mUrlRequestAdapterLock) { |
| + if (mUrlRequestAdapter != 0) { |
| + throw new IllegalStateException("Request is already started."); |
| + } |
| + mUrlRequestAdapter = nativeCreateRequestAdapter( |
| + mRequestContext.getUrlRequestContextAdapter(), |
| + mInitialUrl, |
| + mPriority); |
| + if (mMethod != null) { |
| + if (!nativeSetHttpMethod(mUrlRequestAdapter, mMethod)) { |
| + nativeDestroyRequestAdapter(mUrlRequestAdapter); |
| + mUrlRequestAdapter = 0; |
| + throw new IllegalArgumentException("Invalid http method " |
| + + mMethod); |
| + } |
| + } |
| + for (HeaderEntry header : mRequestHeaders) { |
| + if (!nativeAddHeader(mUrlRequestAdapter, header.getKey(), |
| + header.getValue())) { |
| + nativeDestroyRequestAdapter(mUrlRequestAdapter); |
| + mUrlRequestAdapter = 0; |
| + throw new IllegalArgumentException("Invalid header " |
| + + header.getKey() + "=" + header.getValue()); |
| + } |
| + } |
| + mStarted = true; |
| + nativeStart(mUrlRequestAdapter); |
| + mRequestContext.onRequestStarted(this); |
| + } |
| } |
| @Override |
| public void cancel() { |
| - |
| + synchronized (mUrlRequestAdapterLock) { |
| + if (mCanceled) { |
| + return; |
| + } |
| + mCanceled = true; |
| + destroyRequestAdapter(); |
| + } |
| } |
| @Override |
| public boolean isCanceled() { |
| - return false; |
| + synchronized (mUrlRequestAdapterLock) { |
| + return mCanceled; |
| + } |
| } |
| @Override |
| public void pause() { |
| - |
| + throw new UnsupportedOperationException("Not implemented yet"); |
| } |
| @Override |
| @@ -45,6 +250,306 @@ public class CronetUrlRequest implements UrlRequest { |
| @Override |
| public void resume() { |
| + throw new UnsupportedOperationException("Not implemented yet"); |
| + } |
| + |
| + /** |
| + * Post task to application Executor or Looper. Used for Listener callbacks |
| + * and other tasks that should not be executed on network thread. |
| + */ |
| + private void postAppTask(Runnable task) { |
| + mExecutor.execute(task); |
| + } |
| + |
| + private static int convertRequestPriority(int priority) { |
| + switch (priority) { |
| + case REQUEST_PRIORITY_IDLE: |
| + return ChromiumUrlRequestPriority.IDLE; |
| + case REQUEST_PRIORITY_LOWEST: |
| + return ChromiumUrlRequestPriority.LOWEST; |
| + case REQUEST_PRIORITY_LOW: |
| + return ChromiumUrlRequestPriority.LOW; |
| + case REQUEST_PRIORITY_MEDIUM: |
| + return ChromiumUrlRequestPriority.MEDIUM; |
| + case REQUEST_PRIORITY_HIGHEST: |
| + return ChromiumUrlRequestPriority.HIGHEST; |
| + default: |
| + return ChromiumUrlRequestPriority.MEDIUM; |
| + } |
| + } |
| + |
| + private NativeResponseInfo prepareResponseInfo(int httpStatusCode) { |
| + long urlRequestAdapter; |
| + synchronized (mUrlRequestAdapterLock) { |
| + if (mUrlRequestAdapter == 0) { |
| + return null; |
| + } |
| + urlRequestAdapter = mUrlRequestAdapter; |
| + } |
| + NativeResponseInfo responseInfo = new NativeResponseInfo( |
| + mUrlChain.toArray(new String[mUrlChain.size()]), |
| + httpStatusCode, |
| + nativeGetWasCached(urlRequestAdapter), |
| + nativeGetNegotiatedProtocol(urlRequestAdapter)); |
| + nativePopulateResponseHeaders(urlRequestAdapter, |
| + responseInfo.mAllHeaders); |
| + return responseInfo; |
| + } |
| + |
| + private void checkNotStarted() { |
| + synchronized (mUrlRequestAdapterLock) { |
| + if (mStarted) { |
| + throw new IllegalStateException("Request is already started."); |
| + } |
| + } |
| + } |
| + |
| + private void destroyRequestAdapter() { |
| + synchronized (mUrlRequestAdapterLock) { |
| + if (mUrlRequestAdapter == 0) { |
| + return; |
| + } |
| + nativeDestroyRequestAdapter(mUrlRequestAdapter); |
| + mRequestContext.onRequestDestroyed(this); |
| + mUrlRequestAdapter = 0; |
| + } |
| + } |
| + |
| + private void appendHeaderToMap(HeadersMap headersMap, |
| + String name, String value) { |
| + if (!headersMap.containsKey(name)) { |
| + headersMap.put(name, new ArrayList<String>()); |
| + } |
| + headersMap.get(name).add(value); |
| + } |
| + |
| + /** |
| + * If @CalledByNative method throws an exception, request gets canceled |
| + * and exception could be retrieved from request using getException(). |
| + */ |
| + private void onCalledByNativeException(Exception e) { |
| + UrlRequestException requestError = new UrlRequestException( |
| + "CalledByNative method has thrown an exception", e); |
| + Log.e(CronetUrlRequestContext.LOG_TAG, |
| + "Exception in CalledByNative method", e); |
| + try { |
| + cancel(); |
| + mListener.onError(this, mResponseInfo, requestError); |
| + } catch (Exception cancelException) { |
| + Log.e(CronetUrlRequestContext.LOG_TAG, |
| + "Exception trying to cancel request", cancelException); |
| + } |
| + } |
| + |
| + //////////////////////////////////////////////// |
| + // Private methods called by the native code. |
| + //////////////////////////////////////////////// |
| + |
| + /** |
| + * Called before following redirects. The redirect will automatically be |
| + * followed, unless the request is paused or canceled during this |
| + * callback. If the redirect response has a body, it will be ignored. |
| + * This will only be called between start and onResponseStarted. |
| + * |
| + * @param newLocation Location where request is redirected. |
| + * @param httpStatusCode from redirect response |
| + */ |
| + @SuppressWarnings("unused") |
| + @CalledByNative |
| + private void onRedirect(final String newLocation, int httpStatusCode) { |
| + final NativeResponseInfo responseInfo = |
| + prepareResponseInfo(httpStatusCode); |
| + Runnable task = new Runnable() { |
| + public void run() { |
| + if (isCanceled()) { |
| + return; |
| + } |
| + try { |
| + mListener.onRedirect(CronetUrlRequest.this, responseInfo, |
| + newLocation); |
| + synchronized (mUrlRequestAdapterLock) { |
| + if (mUrlRequestAdapter == 0) { |
| + return; |
| + } |
| + // It is Ok to access mUrlChain not on the network |
| + // thread as request is waiting to follow redirect. |
| + mUrlChain.add(newLocation); |
| + nativeFollowDeferredRedirect(mUrlRequestAdapter); |
| + } |
| + } catch (Exception e) { |
| + onCalledByNativeException(e); |
| + } |
| + } |
| + }; |
| + postAppTask(task); |
| + } |
| + |
| + /** |
| + * Called when the final set of headers, after all redirects, |
| + * is received. Can only be called once for each request. |
| + */ |
| + @SuppressWarnings("unused") |
| + @CalledByNative |
| + private void onResponseStarted(int httpStatusCode) { |
| + mResponseInfo = prepareResponseInfo(httpStatusCode); |
| + Runnable task = new Runnable() { |
| + public void run() { |
| + if (isCanceled()) { |
| + return; |
| + } |
| + try { |
| + mListener.onResponseStarted(CronetUrlRequest.this, |
| + mResponseInfo); |
| + synchronized (mUrlRequestAdapterLock) { |
| + if (mUrlRequestAdapter == 0) { |
| + return; |
| + } |
| + nativeReceiveData(mUrlRequestAdapter); |
| + } |
| + } catch (Exception e) { |
| + onCalledByNativeException(e); |
| + } |
| + } |
| + }; |
| + postAppTask(task); |
| + } |
| + |
| + /** |
| + * Called whenever data is received. The ByteBuffer remains |
| + * valid only until listener callback. Or if the callback |
| + * pauses the request, it remains valid until the request is resumed. |
| + * Cancelling the request also invalidates the buffer. |
| + * |
| + * @param byteBuffer Received data. |
| + */ |
| + @SuppressWarnings("unused") |
| + @CalledByNative |
| + private void onDataReceived(final ByteBuffer byteBuffer) { |
| + if (mOnDataReceivedTask == null) { |
| + mOnDataReceivedTask = new OnDataReceivedRunnable(); |
| + } |
| + mOnDataReceivedTask.mByteBuffer = byteBuffer; |
| + postAppTask(mOnDataReceivedTask); |
| + } |
| + |
| + /** |
| + * Called when request is complete, no callbacks will be called afterwards. |
| + */ |
| + @SuppressWarnings("unused") |
| + @CalledByNative |
| + private void onComplete() { |
| + long totalReceivedBytes; |
| + synchronized (mUrlRequestAdapterLock) { |
| + if (mUrlRequestAdapter == 0) { |
| + return; |
| + } |
| + totalReceivedBytes = |
| + nativeGetTotalReceivedBytes(mUrlRequestAdapter); |
| + } |
| + |
| + final NativeExtendedResponseInfo extendedResponseInfo = |
| + new NativeExtendedResponseInfo(mResponseInfo, |
| + totalReceivedBytes); |
| + Runnable task = new Runnable() { |
| + public void run() { |
| + if (isCanceled()) { |
| + return; |
| + } |
| + try { |
| + mListener.onComplete(CronetUrlRequest.this, |
| + extendedResponseInfo); |
| + } catch (Exception e) { |
| + Log.e(CronetUrlRequestContext.LOG_TAG, |
| + "Exception in onComplete method", e); |
| + } |
| + destroyRequestAdapter(); |
|
mmenke
2014/10/30 21:47:56
Suggestion: Destroy the adapter first. Then onCo
mef
2014/10/30 22:32:55
Done.
|
| + } |
| + }; |
| + postAppTask(task); |
| + } |
| + |
| + /** |
| + * Called when error has occured, no callbacks will be called afterwards. |
| + * |
| + * @param nativeError native net error code. |
| + * @param errorString textual representation of the error code. |
| + */ |
| + @SuppressWarnings("unused") |
| + @CalledByNative |
| + private void onError(final int nativeError, final String errorString) { |
| + Runnable task = new Runnable() { |
| + public void run() { |
| + if (isCanceled()) { |
| + return; |
| + } |
| + try { |
| + UrlRequestException requestError = new UrlRequestException( |
| + "Exception in CronetUrlRequest: " + errorString, |
| + nativeError); |
| + mListener.onError(CronetUrlRequest.this, |
| + mResponseInfo, |
| + requestError); |
| + destroyRequestAdapter(); |
|
mmenke
2014/10/30 21:47:56
Suggestion: Destroy the adapter first. Then onCo
mef
2014/10/30 22:32:55
Done.
|
| + } catch (Exception e) { |
| + onCalledByNativeException(e); |
|
mmenke
2014/10/30 21:47:56
If onError throws an exception, does it really mak
mef
2014/10/30 22:32:55
Done.
|
| + } |
| + } |
| + }; |
| + postAppTask(task); |
| + } |
| + |
| + /** |
| + * Appends header |name| with value |value| to |headersMap|. |
| + */ |
| + @SuppressWarnings("unused") |
| + @CalledByNative |
| + private void onAppendResponseHeader(HeadersMap headersMap, |
| + String name, String value) { |
| + try { |
| + appendHeaderToMap(headersMap, name, value); |
| + } catch (final Exception e) { |
| + onCalledByNativeException(e); |
| + Runnable task = new Runnable() { |
| + public void run() { |
| + if (isCanceled()) { |
| + return; |
| + } |
| + onCalledByNativeException(e); |
| + } |
| + }; |
| + postAppTask(task); |
| + } |
| + } |
| + |
| + // Native methods are implemented in cronet_url_request.cc. |
| + |
| + private native long nativeCreateRequestAdapter( |
| + long urlRequestContextAdapter, String url, int priority); |
| + |
| + private native boolean nativeAddHeader(long urlRequestAdapter, String name, |
| + String value); |
| + |
| + private native boolean nativeSetHttpMethod(long urlRequestAdapter, |
| + String method); |
| + |
| + private native void nativeStart(long urlRequestAdapter); |
| + |
| + private native void nativeDestroyRequestAdapter(long urlRequestAdapter); |
| + |
| + private native void nativeFollowDeferredRedirect(long urlRequestAdapter); |
| + |
| + private native void nativeReceiveData(long urlRequestAdapter); |
| + |
| + private native void nativePopulateResponseHeaders(long urlRequestAdapter, |
| + HeadersMap headers); |
| + |
| + private native String nativeGetNegotiatedProtocol(long urlRequestAdapter); |
| + |
| + private native boolean nativeGetWasCached(long urlRequestAdapter); |
| + |
| + private native long nativeGetTotalReceivedBytes(long urlRequestAdapter); |
| + // Explicit class to work around JNI-generator generics confusion. |
| + private static class HeadersMap extends HashMap<String, List<String>> { |
| } |
| } |