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

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

Issue 586143002: Initial implementation of Cronet Async API. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Address comments, moar scoped pointers. Created 6 years, 2 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: 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..190388b93afc77cc2e7d0cc89c3318d2ec115241 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();
mmenke 2014/11/03 17:13:16 BUG: Calling cancel on one thread while another t
mef 2014/11/03 21:23:37 Done.
+ }
}
@Override
public boolean isCanceled() {
- return false;
+ synchronized (mUrlRequestAdapterLock) {
+ return mCanceled;
+ }
}
@Override
public void pause() {
-
+ throw new UnsupportedOperationException("Not implemented yet");
}
@Override
@@ -45,6 +250,307 @@ 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 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.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.
+ ////////////////////////////////////////////////
+
+ /**
+ * 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);
+ }
+ }
+ };
+ 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 = 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);
+ }
+ }
+ };
+ 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 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;
+ }
+ 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;
+ }
+ 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 {
+ appendHeaderToMap(headersMap, name, value);
+ } catch (final Exception e) {
+ onCalledByNativeException(e);
+ Runnable task = new Runnable() {
+ public void run() {
+ if (isCanceled()) {
+ return;
+ }
+ onCalledByNativeException(e);
+ }
+ };
+ postTaskToExecutor(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>> {
}
}

Powered by Google App Engine
This is Rietveld 408576698