1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 package org.chromium.net;
7 import android.util.Log;
9 import org.chromium.base.CalledByNative;
10 import org.chromium.base.JNINamespace;
12 import java.nio.ByteBuffer;
13 import java.util.AbstractMap;
14 import java.util.ArrayList;
15 import java.util.HashMap;
16 import java.util.List;
18 import java.util.concurrent.Executor;
21 * UrlRequest using Chromium HTTP stack implementation. Could be accessed from
22 * any thread on Executor. Cancel can be done from any thread.
23 * All @CallByNative methods are called on native network thread
24 * and post tasks with listener calls onto Executor. Upon return from listener
25 * callback native request adapter is called on executive thread and posts
26 * native tasks to native network thread. Because Cancel could be called from
27 * any thread it is protected by mUrlRequestAdapterLock.
29 @JNINamespace("cronet")
30 final class CronetUrlRequest implements UrlRequest {
31 /* Native adapter object, owned by UrlRequest. */
32 private long mUrlRequestAdapter;
33 private boolean mStarted = false;
34 private boolean mCanceled = false;
35 private boolean mInOnDataReceived = false;
38 * Synchronize access to mUrlRequestAdapter, mStarted, mCanceled and
39 * mDestroyAfterReading.
41 private final Object mUrlRequestAdapterLock = new Object();
42 private final CronetUrlRequestContext mRequestContext;
43 private final Executor mExecutor;
46 * Url chain contans the URL currently being requested, and
47 * all URLs previously requested. New URLs are only added after it is
48 * decided a redirect will be followed.
50 private final List<String> mUrlChain = new ArrayList<String>();
52 private final UrlRequestListener mListener;
53 private final String mInitialUrl;
54 private final int mPriority;
55 private String mInitialMethod;
56 private final HeadersList mRequestHeaders = new HeadersList();
58 private NativeResponseInfo mResponseInfo;
61 * Listener callback is repeatedly called when data is received, so it is
62 * cached as member variable.
64 private OnDataReceivedRunnable mOnDataReceivedTask;
66 static final class HeaderEntry extends
67 AbstractMap.SimpleEntry<String, String> {
68 public HeaderEntry(String name, String value) {
73 static final class HeadersList extends ArrayList<HeaderEntry> {
76 final class OnDataReceivedRunnable implements Runnable {
77 ByteBuffer mByteBuffer;
84 synchronized (mUrlRequestAdapterLock) {
85 if (mUrlRequestAdapter == 0) {
88 // mByteBuffer is direct buffer backed by native memory,
89 // and passed to listener, so the request adapter cannot
90 // be destroyed while listener has access to it.
91 // Set |mInOnDataReceived| flag during the call to listener
92 // and destroy adapter immediately after if request was
93 // cancelled during the call from listener or other thread.
94 mInOnDataReceived = true;
96 mListener.onDataReceived(CronetUrlRequest.this,
97 mResponseInfo, mByteBuffer);
99 synchronized (mUrlRequestAdapterLock) {
100 mInOnDataReceived = false;
102 destroyRequestAdapter();
105 nativeReceiveData(mUrlRequestAdapter);
107 } catch (Exception e) {
108 synchronized (mUrlRequestAdapterLock) {
109 mInOnDataReceived = false;
111 destroyRequestAdapter();
114 onListenerException(e);
119 static final class NativeResponseInfo implements ResponseInfo {
120 private final String[] mResponseInfoUrlChain;
121 private final int mHttpStatusCode;
122 private final HeadersMap mAllHeaders = new HeadersMap();
123 private final boolean mWasCached;
124 private final String mNegotiatedProtocol;
126 NativeResponseInfo(String[] urlChain, int httpStatusCode,
127 boolean wasCached, String negotiatedProtocol) {
128 mResponseInfoUrlChain = urlChain;
129 mHttpStatusCode = httpStatusCode;
130 mWasCached = wasCached;
131 mNegotiatedProtocol = negotiatedProtocol;
135 public String getUrl() {
136 return mResponseInfoUrlChain[mResponseInfoUrlChain.length - 1];
140 public String[] getUrlChain() {
141 return mResponseInfoUrlChain;
145 public int getHttpStatusCode() {
146 return mHttpStatusCode;
150 public Map<String, List<String>> getAllHeaders() {
155 public boolean wasCached() {
160 public String getNegotiatedProtocol() {
161 return mNegotiatedProtocol;
165 static final class NativeExtendedResponseInfo implements
166 ExtendedResponseInfo {
167 private final ResponseInfo mResponseInfo;
168 private final long mTotalReceivedBytes;
170 NativeExtendedResponseInfo(ResponseInfo responseInfo,
171 long totalReceivedBytes) {
172 mResponseInfo = responseInfo;
173 mTotalReceivedBytes = totalReceivedBytes;
177 public ResponseInfo getResponseInfo() {
178 return mResponseInfo;
182 public long getTotalReceivedBytes() {
183 return mTotalReceivedBytes;
187 CronetUrlRequest(CronetUrlRequestContext requestContext,
188 long urlRequestContextAdapter,
191 UrlRequestListener listener,
194 throw new NullPointerException("URL is required");
196 if (listener == null) {
197 throw new NullPointerException("Listener is required");
199 if (executor == null) {
200 throw new NullPointerException("Executor is required");
203 mRequestContext = requestContext;
206 mPriority = convertRequestPriority(priority);
207 mListener = listener;
208 mExecutor = executor;
212 public void setHttpMethod(String method) {
214 if (method == null) {
215 throw new NullPointerException("Method is required.");
217 mInitialMethod = method;
221 public void addHeader(String header, String value) {
223 if (header == null) {
224 throw new NullPointerException("Invalid header name.");
227 throw new NullPointerException("Invalid header value.");
229 mRequestHeaders.add(new HeaderEntry(header, value));
233 public void start() {
234 synchronized (mUrlRequestAdapterLock) {
236 mUrlRequestAdapter = nativeCreateRequestAdapter(
237 mRequestContext.getUrlRequestContextAdapter(),
240 mRequestContext.onRequestStarted(this);
241 if (mInitialMethod != null) {
242 if (!nativeSetHttpMethod(mUrlRequestAdapter, mInitialMethod)) {
243 destroyRequestAdapter();
244 throw new IllegalArgumentException("Invalid http method "
248 for (HeaderEntry header : mRequestHeaders) {
249 if (!nativeAddHeader(mUrlRequestAdapter, header.getKey(),
250 header.getValue())) {
251 destroyRequestAdapter();
252 throw new IllegalArgumentException("Invalid header "
253 + header.getKey() + "=" + header.getValue());
257 nativeStart(mUrlRequestAdapter);
262 public void cancel() {
263 synchronized (mUrlRequestAdapterLock) {
264 if (mCanceled || !mStarted) {
268 // During call into listener OnDataReceived adapter cannot be
269 // destroyed as it owns the byte buffer.
270 if (!mInOnDataReceived) {
271 destroyRequestAdapter();
277 public boolean isCanceled() {
278 synchronized (mUrlRequestAdapterLock) {
284 public void pause() {
285 throw new UnsupportedOperationException("Not implemented yet");
289 public boolean isPaused() {
294 public void resume() {
295 throw new UnsupportedOperationException("Not implemented yet");
299 * Post task to application Executor. Used for Listener callbacks
300 * and other tasks that should not be executed on network thread.
302 private void postTaskToExecutor(Runnable task) {
303 mExecutor.execute(task);
306 private static int convertRequestPriority(int priority) {
308 case REQUEST_PRIORITY_IDLE:
309 return RequestPriority.IDLE;
310 case REQUEST_PRIORITY_LOWEST:
311 return RequestPriority.LOWEST;
312 case REQUEST_PRIORITY_LOW:
313 return RequestPriority.LOW;
314 case REQUEST_PRIORITY_MEDIUM:
315 return RequestPriority.MEDIUM;
316 case REQUEST_PRIORITY_HIGHEST:
317 return RequestPriority.HIGHEST;
319 return RequestPriority.MEDIUM;
323 private NativeResponseInfo prepareResponseInfoOnNetworkThread(
324 int httpStatusCode) {
325 long urlRequestAdapter;
326 synchronized (mUrlRequestAdapterLock) {
327 if (mUrlRequestAdapter == 0) {
330 // This method is running on network thread, so even if
331 // mUrlRequestAdapter is set to 0 from another thread the actual
332 // deletion of the adapter is posted to network thread, so it is
333 // safe to preserve and use urlRequestAdapter outside the lock.
334 urlRequestAdapter = mUrlRequestAdapter;
336 NativeResponseInfo responseInfo = new NativeResponseInfo(
337 mUrlChain.toArray(new String[mUrlChain.size()]),
339 nativeGetWasCached(urlRequestAdapter),
340 nativeGetNegotiatedProtocol(urlRequestAdapter));
341 nativePopulateResponseHeaders(urlRequestAdapter,
342 responseInfo.mAllHeaders);
346 private void checkNotStarted() {
347 synchronized (mUrlRequestAdapterLock) {
348 if (mStarted || isCanceled()) {
349 throw new IllegalStateException("Request is already started.");
354 private void destroyRequestAdapter() {
355 synchronized (mUrlRequestAdapterLock) {
356 if (mUrlRequestAdapter == 0) {
359 nativeDestroyRequestAdapter(mUrlRequestAdapter);
360 mRequestContext.onRequestDestroyed(this);
361 mUrlRequestAdapter = 0;
366 * If listener method throws an exception, request gets canceled
367 * and exception is reported via onFailed listener callback.
368 * Only called on the Executor.
370 private void onListenerException(Exception e) {
371 UrlRequestException requestError = new UrlRequestException(
372 "CalledByNative method has thrown an exception", e);
373 Log.e(CronetUrlRequestContext.LOG_TAG,
374 "Exception in CalledByNative method", e);
375 // Do not call into listener if request is canceled.
381 mListener.onFailed(this, mResponseInfo, requestError);
382 } catch (Exception cancelException) {
383 Log.e(CronetUrlRequestContext.LOG_TAG,
384 "Exception trying to cancel request", cancelException);
388 ////////////////////////////////////////////////
389 // Private methods called by the native code.
390 // Always called on network thread.
391 ////////////////////////////////////////////////
394 * Called before following redirects. The redirect will automatically be
395 * followed, unless the request is paused or canceled during this
396 * callback. If the redirect response has a body, it will be ignored.
397 * This will only be called between start and onResponseStarted.
399 * @param newLocation Location where request is redirected.
400 * @param httpStatusCode from redirect response
402 @SuppressWarnings("unused")
404 private void onRedirect(final String newLocation, int httpStatusCode) {
405 final NativeResponseInfo responseInfo =
406 prepareResponseInfoOnNetworkThread(httpStatusCode);
407 Runnable task = new Runnable() {
413 mListener.onRedirect(CronetUrlRequest.this, responseInfo,
415 synchronized (mUrlRequestAdapterLock) {
419 // It is Ok to access mUrlChain not on the network
420 // thread as the request is waiting to follow redirect.
421 mUrlChain.add(newLocation);
422 nativeFollowDeferredRedirect(mUrlRequestAdapter);
424 } catch (Exception e) {
425 onListenerException(e);
429 postTaskToExecutor(task);
433 * Called when the final set of headers, after all redirects,
434 * is received. Can only be called once for each request.
436 @SuppressWarnings("unused")
438 private void onResponseStarted(int httpStatusCode) {
439 mResponseInfo = prepareResponseInfoOnNetworkThread(httpStatusCode);
440 Runnable task = new Runnable() {
446 mListener.onResponseStarted(CronetUrlRequest.this,
448 synchronized (mUrlRequestAdapterLock) {
452 nativeReceiveData(mUrlRequestAdapter);
454 } catch (Exception e) {
455 onListenerException(e);
459 postTaskToExecutor(task);
463 * Called whenever data is received. The ByteBuffer remains
464 * valid only until listener callback. Or if the callback
465 * pauses the request, it remains valid until the request is resumed.
466 * Cancelling the request also invalidates the buffer.
468 * @param byteBuffer Received data.
470 @SuppressWarnings("unused")
472 private void onDataReceived(final ByteBuffer byteBuffer) {
473 if (mOnDataReceivedTask == null) {
474 mOnDataReceivedTask = new OnDataReceivedRunnable();
476 mOnDataReceivedTask.mByteBuffer = byteBuffer;
477 postTaskToExecutor(mOnDataReceivedTask);
481 * Called when request is completed successfully, no callbacks will be
484 @SuppressWarnings("unused")
486 private void onSucceeded() {
487 long totalReceivedBytes;
488 synchronized (mUrlRequestAdapterLock) {
489 if (mUrlRequestAdapter == 0) {
493 nativeGetTotalReceivedBytes(mUrlRequestAdapter);
496 final NativeExtendedResponseInfo extendedResponseInfo =
497 new NativeExtendedResponseInfo(mResponseInfo,
499 Runnable task = new Runnable() {
501 synchronized (mUrlRequestAdapterLock) {
505 // Destroy adapter first, so request context could be shut
506 // down from the listener.
507 destroyRequestAdapter();
510 mListener.onSucceeded(CronetUrlRequest.this,
511 extendedResponseInfo);
512 } catch (Exception e) {
513 Log.e(CronetUrlRequestContext.LOG_TAG,
514 "Exception in onComplete method", e);
518 postTaskToExecutor(task);
522 * Called when error has occured, no callbacks will be called afterwards.
524 * @param nativeError native net error code.
525 * @param errorString textual representation of the error code.
527 @SuppressWarnings("unused")
529 private void onError(final int nativeError, final String errorString) {
530 Runnable task = new Runnable() {
535 // Destroy adapter first, so request context could be shut down
536 // from the listener.
537 destroyRequestAdapter();
539 UrlRequestException requestError = new UrlRequestException(
540 "Exception in CronetUrlRequest: " + errorString,
542 mListener.onFailed(CronetUrlRequest.this,
545 } catch (Exception e) {
546 Log.e(CronetUrlRequestContext.LOG_TAG,
547 "Exception in onError method", e);
551 postTaskToExecutor(task);
555 * Appends header |name| with value |value| to |headersMap|.
557 @SuppressWarnings("unused")
559 private void onAppendResponseHeader(HeadersMap headersMap,
560 String name, String value) {
562 if (!headersMap.containsKey(name)) {
563 headersMap.put(name, new ArrayList<String>());
565 headersMap.get(name).add(value);
566 } catch (final Exception e) {
567 Log.e(CronetUrlRequestContext.LOG_TAG,
568 "Exception in onAppendResponseHeader method", e);
572 // Native methods are implemented in cronet_url_request.cc.
574 private native long nativeCreateRequestAdapter(
575 long urlRequestContextAdapter, String url, int priority);
577 private native boolean nativeAddHeader(long urlRequestAdapter, String name,
580 private native boolean nativeSetHttpMethod(long urlRequestAdapter,
583 private native void nativeStart(long urlRequestAdapter);
585 private native void nativeDestroyRequestAdapter(long urlRequestAdapter);
587 private native void nativeFollowDeferredRedirect(long urlRequestAdapter);
589 private native void nativeReceiveData(long urlRequestAdapter);
591 private native void nativePopulateResponseHeaders(long urlRequestAdapter,
594 private native String nativeGetNegotiatedProtocol(long urlRequestAdapter);
596 private native boolean nativeGetWasCached(long urlRequestAdapter);
598 private native long nativeGetTotalReceivedBytes(long urlRequestAdapter);
600 // Explicit class to work around JNI-generator generics confusion.
601 private static class HeadersMap extends HashMap<String, List<String>> {