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..bd928ab58e3d068ed189ded61ba4353f23d3b4a7 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,285 @@ |
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. |
+ * UrlRequest using Chromium HTTP stack implementation. Could be accessed from |
+ * any thread on Executor. Cancel can be done from any thread. |
+ * All @CallByNative methods are called on native network thread |
+ * and post tasks with listener calls onto Executor. Upon return from listener |
+ * callback native request adapter is called on executive thread and posts |
+ * native tasks to native network thread. Because Cancel could be called from |
+ * any thread it is protected by mUrlRequestAdapterLock. |
*/ |
-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; |
+ private boolean mInOnDataReceived = false; |
+ |
+ /* |
+ * Synchronize access to mUrlRequestAdapter, mStarted, mCanceled and |
+ * mDestroyAfterReading. |
+ */ |
+ private final Object mUrlRequestAdapterLock = new Object(); |
+ private final CronetUrlRequestContext mRequestContext; |
+ private final Executor mExecutor; |
+ |
+ /* |
+ * Url chain contans the URL currently being requested, and |
+ * all URLs previously requested. New URLs are only added after it is |
+ * decided a redirect will be followed. |
+ */ |
+ private final List<String> mUrlChain = new ArrayList<String>(); |
+ |
+ private final UrlRequestListener mListener; |
+ private final String mInitialUrl; |
+ private final int mPriority; |
+ private String mInitialMethod; |
+ private final HeadersList mRequestHeaders = new HeadersList(); |
+ |
+ private NativeResponseInfo mResponseInfo; |
+ |
+ /* |
+ * Listener callback is repeatedly called when data is received, so it is |
+ * cached as member variable. |
+ */ |
+ 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 { |
+ synchronized (mUrlRequestAdapterLock) { |
+ if (mUrlRequestAdapter == 0) { |
+ return; |
+ } |
+ // mByteBuffer is direct buffer backed by native memory, |
+ // and passed to listener, so the request adapter cannot |
+ // be destroyed while listener has access to it. |
+ // Set |mInOnDataReceived| flag during the call to listener |
+ // and destroy adapter immediately after if request was |
+ // cancelled during the call from listener or other thread. |
+ mInOnDataReceived = true; |
+ } |
+ mListener.onDataReceived(CronetUrlRequest.this, |
+ mResponseInfo, mByteBuffer); |
+ mByteBuffer = null; |
+ synchronized (mUrlRequestAdapterLock) { |
+ mInOnDataReceived = false; |
+ if (isCanceled()) { |
+ destroyRequestAdapter(); |
+ return; |
+ } |
+ nativeReceiveData(mUrlRequestAdapter); |
+ } |
+ } catch (Exception e) { |
+ synchronized (mUrlRequestAdapterLock) { |
+ mInOnDataReceived = false; |
+ if (isCanceled()) { |
+ destroyRequestAdapter(); |
+ } |
+ } |
+ onListenerException(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 (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."); |
+ } |
+ mInitialMethod = 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) { |
+ checkNotStarted(); |
+ mUrlRequestAdapter = nativeCreateRequestAdapter( |
+ mRequestContext.getUrlRequestContextAdapter(), |
+ mInitialUrl, |
+ mPriority); |
+ mRequestContext.onRequestStarted(this); |
+ if (mInitialMethod != null) { |
+ if (!nativeSetHttpMethod(mUrlRequestAdapter, mInitialMethod)) { |
+ destroyRequestAdapter(); |
+ throw new IllegalArgumentException("Invalid http method " |
+ + mInitialMethod); |
+ } |
+ } |
+ for (HeaderEntry header : mRequestHeaders) { |
+ if (!nativeAddHeader(mUrlRequestAdapter, header.getKey(), |
+ header.getValue())) { |
+ destroyRequestAdapter(); |
+ throw new IllegalArgumentException("Invalid header " |
+ + header.getKey() + "=" + header.getValue()); |
+ } |
+ } |
+ mStarted = true; |
+ nativeStart(mUrlRequestAdapter); |
+ } |
} |
@Override |
public void cancel() { |
- |
+ synchronized (mUrlRequestAdapterLock) { |
+ if (mCanceled || !mStarted) { |
+ return; |
+ } |
+ mCanceled = true; |
+ // During call into listener OnDataReceived adapter cannot be |
+ // destroyed as it owns the byte buffer. |
+ if (!mInOnDataReceived) { |
+ destroyRequestAdapter(); |
+ } |
+ } |
} |
@Override |
public boolean isCanceled() { |
- return false; |
+ synchronized (mUrlRequestAdapterLock) { |
+ return mCanceled; |
+ } |
} |
@Override |
public void pause() { |
- |
+ throw new UnsupportedOperationException("Not implemented yet"); |
} |
@Override |
@@ -45,6 +292,312 @@ public class CronetUrlRequest implements UrlRequest { |
@Override |
public void resume() { |
+ throw new UnsupportedOperationException("Not implemented yet"); |
+ } |
+ |
+ /** |
+ * Post task to application Executor. Used for Listener callbacks |
+ * and other tasks that should not be executed on network thread. |
+ */ |
+ private void postTaskToExecutor(Runnable task) { |
+ mExecutor.execute(task); |
+ } |
+ |
+ private static int convertRequestPriority(int priority) { |
+ switch (priority) { |
+ case REQUEST_PRIORITY_IDLE: |
+ return RequestPriority.IDLE; |
+ case REQUEST_PRIORITY_LOWEST: |
+ return RequestPriority.LOWEST; |
+ case REQUEST_PRIORITY_LOW: |
+ return RequestPriority.LOW; |
+ case REQUEST_PRIORITY_MEDIUM: |
+ return RequestPriority.MEDIUM; |
+ case REQUEST_PRIORITY_HIGHEST: |
+ return RequestPriority.HIGHEST; |
+ default: |
+ return RequestPriority.MEDIUM; |
+ } |
+ } |
+ |
+ private NativeResponseInfo prepareResponseInfoOnNetworkThread( |
+ int httpStatusCode) { |
+ long urlRequestAdapter; |
+ synchronized (mUrlRequestAdapterLock) { |
+ if (mUrlRequestAdapter == 0) { |
+ return null; |
+ } |
+ // This method is running on network thread, so even if |
+ // mUrlRequestAdapter is set to 0 from another thread the actual |
+ // deletion of the adapter is posted to network thread, so it is |
+ // safe to preserve and use urlRequestAdapter outside the lock. |
+ 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 || isCanceled()) { |
+ throw new IllegalStateException("Request is already started."); |
+ } |
+ } |
+ } |
+ |
+ private void destroyRequestAdapter() { |
+ synchronized (mUrlRequestAdapterLock) { |
+ if (mUrlRequestAdapter == 0) { |
+ return; |
+ } |
+ nativeDestroyRequestAdapter(mUrlRequestAdapter); |
+ mRequestContext.onRequestDestroyed(this); |
+ mUrlRequestAdapter = 0; |
+ } |
+ } |
+ |
+ /** |
+ * If listener method throws an exception, request gets canceled |
+ * and exception is reported via onFailed listener callback. |
+ * Only called on the Executor. |
+ */ |
+ private void onListenerException(Exception e) { |
+ UrlRequestException requestError = new UrlRequestException( |
+ "CalledByNative method has thrown an exception", e); |
+ Log.e(CronetUrlRequestContext.LOG_TAG, |
+ "Exception in CalledByNative method", e); |
+ // Do not call into listener if request is canceled. |
+ if (isCanceled()) { |
+ return; |
+ } |
+ try { |
+ cancel(); |
+ mListener.onFailed(this, mResponseInfo, requestError); |
+ } catch (Exception cancelException) { |
+ Log.e(CronetUrlRequestContext.LOG_TAG, |
+ "Exception trying to cancel request", cancelException); |
+ } |
+ } |
+ |
+ //////////////////////////////////////////////// |
+ // Private methods called by the native code. |
+ // Always called on network thread. |
+ //////////////////////////////////////////////// |
+ |
+ /** |
+ * 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 = |
+ prepareResponseInfoOnNetworkThread(httpStatusCode); |
+ Runnable task = new Runnable() { |
+ public void run() { |
+ if (isCanceled()) { |
+ return; |
+ } |
+ try { |
+ mListener.onRedirect(CronetUrlRequest.this, responseInfo, |
+ newLocation); |
+ synchronized (mUrlRequestAdapterLock) { |
+ if (isCanceled()) { |
+ return; |
+ } |
+ // It is Ok to access mUrlChain not on the network |
+ // thread as the request is waiting to follow redirect. |
+ mUrlChain.add(newLocation); |
+ nativeFollowDeferredRedirect(mUrlRequestAdapter); |
+ } |
+ } catch (Exception e) { |
+ onListenerException(e); |
+ } |
+ } |
+ }; |
+ postTaskToExecutor(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 = prepareResponseInfoOnNetworkThread(httpStatusCode); |
+ Runnable task = new Runnable() { |
+ public void run() { |
+ if (isCanceled()) { |
+ return; |
+ } |
+ try { |
+ mListener.onResponseStarted(CronetUrlRequest.this, |
+ mResponseInfo); |
+ synchronized (mUrlRequestAdapterLock) { |
+ if (isCanceled()) { |
+ return; |
+ } |
+ nativeReceiveData(mUrlRequestAdapter); |
+ } |
+ } catch (Exception e) { |
+ onListenerException(e); |
+ } |
+ } |
+ }; |
+ postTaskToExecutor(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; |
+ postTaskToExecutor(mOnDataReceivedTask); |
+ } |
+ |
+ /** |
+ * Called when request is completed successfully, no callbacks will be |
+ * called afterwards. |
+ */ |
+ @SuppressWarnings("unused") |
+ @CalledByNative |
+ private void onSucceeded() { |
+ 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() { |
+ synchronized (mUrlRequestAdapterLock) { |
+ if (isCanceled()) { |
+ return; |
+ } |
+ // Destroy adapter first, so request context could be shut |
+ // down from the listener. |
+ destroyRequestAdapter(); |
+ } |
+ try { |
+ mListener.onSucceeded(CronetUrlRequest.this, |
+ extendedResponseInfo); |
+ } catch (Exception e) { |
+ Log.e(CronetUrlRequestContext.LOG_TAG, |
+ "Exception in onComplete method", e); |
+ } |
+ } |
+ }; |
+ postTaskToExecutor(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; |
+ } |
+ // Destroy adapter first, so request context could be shut down |
+ // from the listener. |
+ destroyRequestAdapter(); |
+ try { |
+ UrlRequestException requestError = new UrlRequestException( |
+ "Exception in CronetUrlRequest: " + errorString, |
+ nativeError); |
+ mListener.onFailed(CronetUrlRequest.this, |
+ mResponseInfo, |
+ requestError); |
+ } catch (Exception e) { |
+ Log.e(CronetUrlRequestContext.LOG_TAG, |
+ "Exception in onError method", e); |
+ } |
+ } |
+ }; |
+ postTaskToExecutor(task); |
+ } |
+ |
+ /** |
+ * Appends header |name| with value |value| to |headersMap|. |
+ */ |
+ @SuppressWarnings("unused") |
+ @CalledByNative |
+ private void onAppendResponseHeader(HeadersMap headersMap, |
+ String name, String value) { |
+ try { |
+ if (!headersMap.containsKey(name)) { |
+ headersMap.put(name, new ArrayList<String>()); |
+ } |
+ headersMap.get(name).add(value); |
+ } catch (final Exception e) { |
+ Log.e(CronetUrlRequestContext.LOG_TAG, |
+ "Exception in onAppendResponseHeader method", e); |
+ } |
+ } |
+ |
+ // 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>> { |
} |
} |