| // Copyright 2014 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package org.chromium.net.impl; |
| |
| import static java.lang.Math.max; |
| import static org.chromium.net.UrlRequest.Builder.REQUEST_PRIORITY_IDLE; |
| import static org.chromium.net.UrlRequest.Builder.REQUEST_PRIORITY_LOWEST; |
| import static org.chromium.net.UrlRequest.Builder.REQUEST_PRIORITY_LOW; |
| import static org.chromium.net.UrlRequest.Builder.REQUEST_PRIORITY_MEDIUM; |
| import static org.chromium.net.UrlRequest.Builder.REQUEST_PRIORITY_HIGHEST; |
| |
| import android.os.Build; |
| |
| import androidx.annotation.RequiresApi; |
| import androidx.annotation.VisibleForTesting; |
| |
| import org.jni_zero.CalledByNative; |
| import org.jni_zero.JNINamespace; |
| import org.jni_zero.NativeClassQualifiedName; |
| import org.jni_zero.NativeMethods; |
| |
| import org.chromium.base.Log; |
| import org.chromium.net.CallbackException; |
| import org.chromium.net.CronetException; |
| import org.chromium.net.Idempotency; |
| import org.chromium.net.InlineExecutionProhibitedException; |
| import org.chromium.net.NetworkException; |
| import org.chromium.net.RequestFinishedInfo; |
| import org.chromium.net.RequestPriority; |
| import org.chromium.net.UploadDataProvider; |
| import org.chromium.net.UrlRequest; |
| import org.chromium.net.UrlResponseInfo.HeaderBlock; |
| import org.chromium.net.impl.CronetLogger.CronetTrafficInfo; |
| |
| import java.nio.ByteBuffer; |
| import java.time.Duration; |
| import java.util.AbstractMap; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.concurrent.Executor; |
| import java.util.concurrent.RejectedExecutionException; |
| |
| import javax.annotation.concurrent.GuardedBy; |
| |
| /** |
| * UrlRequest using Chromium HTTP stack implementation. Could be accessed from |
| * any thread on Executor. Cancel can be called 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. |
| */ |
| @JNINamespace("cronet") |
| @VisibleForTesting |
| public final class CronetUrlRequest extends UrlRequestBase { |
| private final boolean mAllowDirectExecutor; |
| |
| /* Native adapter object, owned by UrlRequest. */ |
| @GuardedBy("mUrlRequestAdapterLock") |
| private long mUrlRequestAdapter; |
| |
| @GuardedBy("mUrlRequestAdapterLock") |
| private boolean mStarted; |
| |
| @GuardedBy("mUrlRequestAdapterLock") |
| private boolean mWaitingOnRedirect; |
| |
| @GuardedBy("mUrlRequestAdapterLock") |
| private boolean mWaitingOnRead; |
| |
| /* |
| * Synchronize access to mUrlRequestAdapter, mStarted, mWaitingOnRedirect, |
| * and mWaitingOnRead. |
| */ |
| private final Object mUrlRequestAdapterLock = new Object(); |
| private final CronetUrlRequestContext mRequestContext; |
| private final Executor mExecutor; |
| |
| /* |
| * URL chain contains the URL currently being requested, and |
| * all URLs previously requested. New URLs are added before |
| * mCallback.onRedirectReceived is called. |
| */ |
| private final List<String> mUrlChain = new ArrayList<String>(); |
| |
| private final VersionSafeCallbacks.UrlRequestCallback mCallback; |
| private final String mInitialUrl; |
| private final int mPriority; |
| private final int mIdempotency; |
| private String mInitialMethod; |
| private final HeadersList mRequestHeaders = new HeadersList(); |
| private final Collection<Object> mRequestAnnotations; |
| private final boolean mDisableCache; |
| private final boolean mDisableConnectionMigration; |
| private final boolean mTrafficStatsTagSet; |
| private final int mTrafficStatsTag; |
| private final boolean mTrafficStatsUidSet; |
| private final int mTrafficStatsUid; |
| private final VersionSafeCallbacks.RequestFinishedInfoListener mRequestFinishedListener; |
| private final long mNetworkHandle; |
| private final int mCronetEngineId; |
| private final CronetLogger mLogger; |
| |
| private CronetUploadDataStream mUploadDataStream; |
| |
| private UrlResponseInfoImpl mResponseInfo; |
| |
| // These three should only be updated once with mUrlRequestAdapterLock held. They are read on |
| // UrlRequest.Callback's and RequestFinishedInfo.Listener's executors after the last update. |
| @RequestFinishedInfoImpl.FinishedReason private int mFinishedReason; |
| private CronetException mException; |
| private CronetMetrics mMetrics; |
| private boolean mQuicConnectionMigrationAttempted; |
| private boolean mQuicConnectionMigrationSuccessful; |
| |
| /* |
| * Listener callback is repeatedly invoked when each read is completed, so it |
| * is cached as a member variable. |
| */ |
| private OnReadCompletedRunnable mOnReadCompletedTask; |
| |
| @GuardedBy("mUrlRequestAdapterLock") |
| private Runnable mOnDestroyedCallbackForTesting; |
| |
| @VisibleForTesting |
| public static final class HeadersList extends ArrayList<Map.Entry<String, String>> {} |
| |
| private final class OnReadCompletedRunnable implements Runnable { |
| // Buffer passed back from current invocation of onReadCompleted. |
| ByteBuffer mByteBuffer; |
| |
| @Override |
| public void run() { |
| checkCallingThread(); |
| // Null out mByteBuffer, to pass buffer ownership to callback or release if done. |
| ByteBuffer buffer = mByteBuffer; |
| mByteBuffer = null; |
| |
| try { |
| synchronized (mUrlRequestAdapterLock) { |
| if (isDoneLocked()) { |
| return; |
| } |
| mWaitingOnRead = true; |
| } |
| mCallback.onReadCompleted(CronetUrlRequest.this, mResponseInfo, buffer); |
| } catch (Exception e) { |
| onCallbackException(e); |
| } |
| } |
| } |
| |
| CronetUrlRequest( |
| CronetUrlRequestContext requestContext, |
| String url, |
| int priority, |
| UrlRequest.Callback callback, |
| Executor executor, |
| Collection<Object> requestAnnotations, |
| boolean disableCache, |
| boolean disableConnectionMigration, |
| boolean allowDirectExecutor, |
| boolean trafficStatsTagSet, |
| int trafficStatsTag, |
| boolean trafficStatsUidSet, |
| int trafficStatsUid, |
| RequestFinishedInfo.Listener requestFinishedListener, |
| int idempotency, |
| long networkHandle) { |
| Objects.requireNonNull(url, "URL is required"); |
| Objects.requireNonNull(callback, "Listener is required"); |
| Objects.requireNonNull(executor, "Executor is required"); |
| |
| mAllowDirectExecutor = allowDirectExecutor; |
| mRequestContext = requestContext; |
| mCronetEngineId = requestContext.getCronetEngineId(); |
| mLogger = requestContext.getCronetLogger(); |
| mInitialUrl = url; |
| mUrlChain.add(url); |
| mPriority = convertRequestPriority(priority); |
| mCallback = new VersionSafeCallbacks.UrlRequestCallback(callback); |
| mExecutor = executor; |
| mRequestAnnotations = requestAnnotations; |
| mDisableCache = disableCache; |
| mDisableConnectionMigration = disableConnectionMigration; |
| mTrafficStatsTagSet = trafficStatsTagSet; |
| mTrafficStatsTag = trafficStatsTag; |
| mTrafficStatsUidSet = trafficStatsUidSet; |
| mTrafficStatsUid = trafficStatsUid; |
| mRequestFinishedListener = |
| requestFinishedListener != null |
| ? new VersionSafeCallbacks.RequestFinishedInfoListener( |
| requestFinishedListener) |
| : null; |
| mIdempotency = convertIdempotency(idempotency); |
| mNetworkHandle = networkHandle; |
| } |
| |
| @Override |
| public void setHttpMethod(String method) { |
| checkNotStarted(); |
| Objects.requireNonNull(method, "Method is required."); |
| mInitialMethod = method; |
| } |
| |
| @Override |
| public void addHeader(String header, String value) { |
| checkNotStarted(); |
| Objects.requireNonNull(header, "Invalid header name."); |
| Objects.requireNonNull(value, "Invalid header value."); |
| mRequestHeaders.add(new AbstractMap.SimpleImmutableEntry<String, String>(header, value)); |
| } |
| |
| @Override |
| public void setUploadDataProvider(UploadDataProvider uploadDataProvider, Executor executor) { |
| Objects.requireNonNull(uploadDataProvider, "Invalid UploadDataProvider."); |
| if (mInitialMethod == null) { |
| mInitialMethod = "POST"; |
| } |
| mUploadDataStream = new CronetUploadDataStream(uploadDataProvider, executor, this); |
| } |
| |
| @Override |
| public String getHttpMethod() { |
| return mInitialMethod; |
| } |
| |
| @Override |
| public boolean isDirectExecutorAllowed() { |
| return mAllowDirectExecutor; |
| } |
| |
| @Override |
| public boolean isCacheDisabled() { |
| return mDisableCache; |
| } |
| |
| @Override |
| public boolean hasTrafficStatsTag() { |
| return mTrafficStatsTagSet; |
| } |
| |
| @Override |
| public int getTrafficStatsTag() { |
| if (!hasTrafficStatsTag()) { |
| throw new IllegalStateException("TrafficStatsTag is not set"); |
| } |
| return mTrafficStatsTag; |
| } |
| |
| @Override |
| public boolean hasTrafficStatsUid() { |
| return mTrafficStatsUidSet; |
| } |
| |
| @Override |
| public int getTrafficStatsUid() { |
| if (!hasTrafficStatsUid()) { |
| throw new IllegalStateException("TrafficStatsUid is not set"); |
| } |
| return mTrafficStatsUid; |
| } |
| @Override |
| public int getPriority() { |
| switch (mPriority) { |
| case RequestPriority.IDLE: |
| return REQUEST_PRIORITY_IDLE; |
| case RequestPriority.LOWEST: |
| return REQUEST_PRIORITY_LOWEST; |
| case RequestPriority.LOW: |
| return REQUEST_PRIORITY_LOW; |
| case RequestPriority.MEDIUM: |
| return REQUEST_PRIORITY_MEDIUM; |
| case RequestPriority.HIGHEST: |
| return REQUEST_PRIORITY_HIGHEST; |
| default: |
| throw new IllegalStateException("Invalid request priority: " + mPriority); |
| } |
| } |
| |
| @Override |
| public HeaderBlock getHeaders() { |
| return new UrlResponseInfoImpl.HeaderBlockImpl(mRequestHeaders); |
| } |
| |
| @Override |
| public void start() { |
| synchronized (mUrlRequestAdapterLock) { |
| checkNotStarted(); |
| |
| try { |
| mUrlRequestAdapter = |
| CronetUrlRequestJni.get() |
| .createRequestAdapter( |
| CronetUrlRequest.this, |
| mRequestContext.getUrlRequestContextAdapter(), |
| mInitialUrl, |
| mPriority, |
| mDisableCache, |
| mDisableConnectionMigration, |
| mTrafficStatsTagSet, |
| mTrafficStatsTag, |
| mTrafficStatsUidSet, |
| mTrafficStatsUid, |
| mIdempotency, |
| mNetworkHandle); |
| mRequestContext.onRequestStarted(); |
| if (mInitialMethod != null) { |
| if (!CronetUrlRequestJni.get() |
| .setHttpMethod( |
| mUrlRequestAdapter, CronetUrlRequest.this, mInitialMethod)) { |
| throw new IllegalArgumentException("Invalid http method " + mInitialMethod); |
| } |
| } |
| |
| boolean hasContentType = false; |
| for (Map.Entry<String, String> header : mRequestHeaders) { |
| if (header.getKey().equalsIgnoreCase("Content-Type") |
| && !header.getValue().isEmpty()) { |
| hasContentType = true; |
| } |
| if (!CronetUrlRequestJni.get() |
| .addRequestHeader( |
| mUrlRequestAdapter, |
| CronetUrlRequest.this, |
| header.getKey(), |
| header.getValue())) { |
| throw new IllegalArgumentException( |
| "Invalid header with headername: " + header.getKey()); |
| } |
| } |
| if (mUploadDataStream != null) { |
| if (!hasContentType) { |
| throw new IllegalArgumentException( |
| "Requests with upload data must have a Content-Type."); |
| } |
| mStarted = true; |
| mUploadDataStream.postTaskToExecutor( |
| new Runnable() { |
| @Override |
| public void run() { |
| mUploadDataStream.initializeWithRequest(); |
| synchronized (mUrlRequestAdapterLock) { |
| if (isDoneLocked()) { |
| return; |
| } |
| mUploadDataStream.attachNativeAdapterToRequest( |
| mUrlRequestAdapter); |
| startInternalLocked(); |
| } |
| } |
| }); |
| return; |
| } |
| } catch (RuntimeException e) { |
| // If there's an exception, cleanup and then throw the exception to the caller. |
| // start() is synchronized so we do not acquire mUrlRequestAdapterLock here. |
| destroyRequestAdapterLocked(RequestFinishedInfo.FAILED); |
| mRequestContext.onRequestFinished(); |
| throw e; |
| } |
| mStarted = true; |
| startInternalLocked(); |
| } |
| } |
| |
| /* |
| * Starts fully configured request. Could execute on UploadDataProvider executor. |
| * Caller is expected to ensure that request isn't canceled and mUrlRequestAdapter is valid. |
| */ |
| @GuardedBy("mUrlRequestAdapterLock") |
| private void startInternalLocked() { |
| CronetUrlRequestJni.get().start(mUrlRequestAdapter, CronetUrlRequest.this); |
| } |
| |
| @Override |
| public void followRedirect() { |
| synchronized (mUrlRequestAdapterLock) { |
| if (!mWaitingOnRedirect) { |
| throw new IllegalStateException("No redirect to follow."); |
| } |
| mWaitingOnRedirect = false; |
| |
| if (isDoneLocked()) { |
| return; |
| } |
| |
| CronetUrlRequestJni.get() |
| .followDeferredRedirect(mUrlRequestAdapter, CronetUrlRequest.this); |
| } |
| } |
| |
| @Override |
| public void read(ByteBuffer buffer) { |
| Preconditions.checkHasRemaining(buffer); |
| Preconditions.checkDirect(buffer); |
| synchronized (mUrlRequestAdapterLock) { |
| if (!mWaitingOnRead) { |
| throw new IllegalStateException("Unexpected read attempt."); |
| } |
| mWaitingOnRead = false; |
| |
| if (isDoneLocked()) { |
| return; |
| } |
| |
| if (!CronetUrlRequestJni.get() |
| .readData( |
| mUrlRequestAdapter, |
| CronetUrlRequest.this, |
| buffer, |
| buffer.position(), |
| buffer.limit())) { |
| // Still waiting on read. This is just to have consistent |
| // behavior with the other error cases. |
| mWaitingOnRead = true; |
| throw new IllegalArgumentException("Unable to call native read"); |
| } |
| } |
| } |
| |
| @Override |
| public void cancel() { |
| synchronized (mUrlRequestAdapterLock) { |
| if (isDoneLocked() || !mStarted) { |
| return; |
| } |
| destroyRequestAdapterLocked(RequestFinishedInfo.CANCELED); |
| } |
| } |
| |
| @Override |
| public boolean isDone() { |
| synchronized (mUrlRequestAdapterLock) { |
| return isDoneLocked(); |
| } |
| } |
| |
| @GuardedBy("mUrlRequestAdapterLock") |
| private boolean isDoneLocked() { |
| return mStarted && mUrlRequestAdapter == 0; |
| } |
| |
| @Override |
| public void getStatus(UrlRequest.StatusListener unsafeListener) { |
| final VersionSafeCallbacks.UrlRequestStatusListener listener = |
| new VersionSafeCallbacks.UrlRequestStatusListener(unsafeListener); |
| synchronized (mUrlRequestAdapterLock) { |
| if (mUrlRequestAdapter != 0) { |
| CronetUrlRequestJni.get() |
| .getStatus(mUrlRequestAdapter, CronetUrlRequest.this, listener); |
| return; |
| } |
| } |
| Runnable task = |
| new Runnable() { |
| @Override |
| public void run() { |
| listener.onStatus(UrlRequest.Status.INVALID); |
| } |
| }; |
| postTaskToExecutor(task); |
| } |
| |
| public void setOnDestroyedCallbackForTesting(Runnable onDestroyedCallbackForTesting) { |
| synchronized (mUrlRequestAdapterLock) { |
| mOnDestroyedCallbackForTesting = onDestroyedCallbackForTesting; |
| } |
| } |
| |
| public void setOnDestroyedUploadCallbackForTesting( |
| Runnable onDestroyedUploadCallbackForTesting) { |
| mUploadDataStream.setOnDestroyedCallbackForTesting(onDestroyedUploadCallbackForTesting); |
| } |
| |
| public long getUrlRequestAdapterForTesting() { |
| synchronized (mUrlRequestAdapterLock) { |
| return mUrlRequestAdapter; |
| } |
| } |
| |
| /** |
| * Posts task to application Executor. Used for Listener callbacks |
| * and other tasks that should not be executed on network thread. |
| */ |
| private void postTaskToExecutor(Runnable task) { |
| try { |
| mExecutor.execute(task); |
| } catch (RejectedExecutionException failException) { |
| Log.e( |
| CronetUrlRequestContext.LOG_TAG, |
| "Exception posting task to executor", |
| failException); |
| // If posting a task throws an exception, then we fail the request. This exception could |
| // be permanent (executor shutdown), transient (AbortPolicy, or CallerRunsPolicy with |
| // direct execution not permitted), or caused by the runnables we submit if |
| // mUserExecutor is a direct executor and direct execution is not permitted. In the |
| // latter two cases, there is at least have a chance to inform the embedder of the |
| // request's failure, since failWithException does not enforce that onFailed() is not |
| // executed inline. |
| failWithException( |
| new CronetExceptionImpl("Exception posting task to executor", failException)); |
| } |
| } |
| |
| private static int convertRequestPriority(int priority) { |
| switch (priority) { |
| case Builder.REQUEST_PRIORITY_IDLE: |
| return RequestPriority.IDLE; |
| case Builder.REQUEST_PRIORITY_LOWEST: |
| return RequestPriority.LOWEST; |
| case Builder.REQUEST_PRIORITY_LOW: |
| return RequestPriority.LOW; |
| case Builder.REQUEST_PRIORITY_MEDIUM: |
| return RequestPriority.MEDIUM; |
| case Builder.REQUEST_PRIORITY_HIGHEST: |
| return RequestPriority.HIGHEST; |
| default: |
| return RequestPriority.MEDIUM; |
| } |
| } |
| |
| private static int convertIdempotency(int idempotency) { |
| switch (idempotency) { |
| case Builder.DEFAULT_IDEMPOTENCY: |
| return Idempotency.DEFAULT_IDEMPOTENCY; |
| case Builder.IDEMPOTENT: |
| return Idempotency.IDEMPOTENT; |
| case Builder.NOT_IDEMPOTENT: |
| return Idempotency.NOT_IDEMPOTENT; |
| default: |
| return Idempotency.DEFAULT_IDEMPOTENCY; |
| } |
| } |
| |
| /** |
| * Estimates the byte size of the headers in their on-wire format. |
| * We are not really interested in their specific size but something which is close enough. |
| */ |
| @VisibleForTesting |
| public static long estimateHeadersSizeInBytes(Map<String, List<String>> headers) { |
| if (headers == null) return 0; |
| |
| long responseHeaderSizeInBytes = 0; |
| for (Map.Entry<String, List<String>> entry : headers.entrySet()) { |
| String key = entry.getKey(); |
| if (key != null) responseHeaderSizeInBytes += key.length(); |
| if (entry.getValue() == null) continue; |
| |
| for (String content : entry.getValue()) { |
| responseHeaderSizeInBytes += content.length(); |
| } |
| } |
| return responseHeaderSizeInBytes; |
| } |
| |
| /** |
| * Estimates the byte size of the headers in their on-wire format. |
| * We are not really interested in their specific size but something which is close enough. |
| */ |
| @VisibleForTesting |
| public static long estimateHeadersSizeInBytes(HeadersList headers) { |
| if (headers == null) return 0; |
| long responseHeaderSizeInBytes = 0; |
| for (Map.Entry<String, String> entry : headers) { |
| String key = entry.getKey(); |
| if (key != null) responseHeaderSizeInBytes += key.length(); |
| String value = entry.getValue(); |
| if (value != null) responseHeaderSizeInBytes += entry.getValue().length(); |
| } |
| return responseHeaderSizeInBytes; |
| } |
| |
| private UrlResponseInfoImpl prepareResponseInfoOnNetworkThread( |
| int httpStatusCode, |
| String httpStatusText, |
| String[] headers, |
| boolean wasCached, |
| String negotiatedProtocol, |
| String proxyServer, |
| long receivedByteCount) { |
| HeadersList headersList = new HeadersList(); |
| for (int i = 0; i < headers.length; i += 2) { |
| headersList.add( |
| new AbstractMap.SimpleImmutableEntry<String, String>( |
| headers[i], headers[i + 1])); |
| } |
| return new UrlResponseInfoImpl( |
| new ArrayList<String>(mUrlChain), |
| httpStatusCode, |
| httpStatusText, |
| headersList, |
| wasCached, |
| negotiatedProtocol, |
| proxyServer, |
| receivedByteCount); |
| } |
| |
| private void checkNotStarted() { |
| synchronized (mUrlRequestAdapterLock) { |
| if (mStarted || isDoneLocked()) { |
| throw new IllegalStateException("Request is already started."); |
| } |
| } |
| } |
| |
| /** |
| * Helper method to set final status of CronetUrlRequest and clean up the |
| * native request adapter. |
| */ |
| @GuardedBy("mUrlRequestAdapterLock") |
| private void destroyRequestAdapterLocked( |
| @RequestFinishedInfoImpl.FinishedReason int finishedReason) { |
| assert mException == null || finishedReason == RequestFinishedInfo.FAILED; |
| mFinishedReason = finishedReason; |
| if (mUrlRequestAdapter == 0) { |
| return; |
| } |
| mRequestContext.onRequestDestroyed(); |
| // Posts a task to destroy the native adapter. |
| CronetUrlRequestJni.get() |
| .destroy( |
| mUrlRequestAdapter, |
| CronetUrlRequest.this, |
| finishedReason == RequestFinishedInfo.CANCELED); |
| mUrlRequestAdapter = 0; |
| } |
| |
| /** |
| * If callback method throws an exception, request gets canceled |
| * and exception is reported via onFailed listener callback. |
| * Only called on the Executor. |
| */ |
| private void onCallbackException(Exception e) { |
| CallbackException requestError = |
| new CallbackExceptionImpl("Exception received from UrlRequest.Callback", e); |
| Log.e(CronetUrlRequestContext.LOG_TAG, "Exception in CalledByNative method", e); |
| failWithException(requestError); |
| } |
| |
| /** Called when UploadDataProvider encounters an error. */ |
| void onUploadException(Throwable e) { |
| CallbackException uploadError = |
| new CallbackExceptionImpl("Exception received from UploadDataProvider", e); |
| Log.e(CronetUrlRequestContext.LOG_TAG, "Exception in upload method", e); |
| failWithException(uploadError); |
| } |
| |
| /** Fails the request with an exception on any thread. */ |
| private void failWithException(final CronetException exception) { |
| synchronized (mUrlRequestAdapterLock) { |
| if (isDoneLocked()) { |
| return; |
| } |
| assert mException == null; |
| mException = exception; |
| destroyRequestAdapterLocked(RequestFinishedInfo.FAILED); |
| } |
| // onFailed will be invoked from onNativeAdapterDestroyed() to ensure metrics collection. |
| } |
| |
| //////////////////////////////////////////////// |
| // Private methods called by the native code. |
| // Always called on network thread. |
| //////////////////////////////////////////////// |
| |
| /** |
| * Called before following redirects. The redirect will only be followed if |
| * {@link #followRedirect()} is called. 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 |
| * @param receivedByteCount count of bytes received for redirect response |
| * @param headers an array of response headers with keys at the even indices |
| * followed by the corresponding values at the odd indices. |
| */ |
| @SuppressWarnings("unused") |
| @CalledByNative |
| private void onRedirectReceived( |
| final String newLocation, |
| int httpStatusCode, |
| String httpStatusText, |
| String[] headers, |
| boolean wasCached, |
| String negotiatedProtocol, |
| String proxyServer, |
| long receivedByteCount) { |
| final UrlResponseInfoImpl responseInfo = |
| prepareResponseInfoOnNetworkThread( |
| httpStatusCode, |
| httpStatusText, |
| headers, |
| wasCached, |
| negotiatedProtocol, |
| proxyServer, |
| receivedByteCount); |
| |
| // Have to do this after creating responseInfo. |
| mUrlChain.add(newLocation); |
| |
| Runnable task = |
| new Runnable() { |
| @Override |
| public void run() { |
| checkCallingThread(); |
| synchronized (mUrlRequestAdapterLock) { |
| if (isDoneLocked()) { |
| return; |
| } |
| mWaitingOnRedirect = true; |
| } |
| |
| try { |
| mCallback.onRedirectReceived( |
| CronetUrlRequest.this, responseInfo, newLocation); |
| } catch (Exception e) { |
| onCallbackException(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, |
| String httpStatusText, |
| String[] headers, |
| boolean wasCached, |
| String negotiatedProtocol, |
| String proxyServer, |
| long receivedByteCount) { |
| mResponseInfo = |
| prepareResponseInfoOnNetworkThread( |
| httpStatusCode, |
| httpStatusText, |
| headers, |
| wasCached, |
| negotiatedProtocol, |
| proxyServer, |
| receivedByteCount); |
| Runnable task = |
| new Runnable() { |
| @Override |
| public void run() { |
| checkCallingThread(); |
| synchronized (mUrlRequestAdapterLock) { |
| if (isDoneLocked()) { |
| return; |
| } |
| mWaitingOnRead = true; |
| } |
| |
| try { |
| mCallback.onResponseStarted(CronetUrlRequest.this, mResponseInfo); |
| } catch (Exception e) { |
| onCallbackException(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 ByteBuffer containing received data, starting at |
| * initialPosition. Guaranteed to have at least one read byte. Its |
| * limit has not yet been updated to reflect the bytes read. |
| * @param bytesRead Number of bytes read. |
| * @param initialPosition Original position of byteBuffer when passed to |
| * read(). Used as a minimal check that the buffer hasn't been |
| * modified while reading from the network. |
| * @param initialLimit Original limit of byteBuffer when passed to |
| * read(). Used as a minimal check that the buffer hasn't been |
| * modified while reading from the network. |
| * @param receivedByteCount number of bytes received. |
| */ |
| @SuppressWarnings("unused") |
| @CalledByNative |
| private void onReadCompleted( |
| final ByteBuffer byteBuffer, |
| int bytesRead, |
| int initialPosition, |
| int initialLimit, |
| long receivedByteCount) { |
| mResponseInfo.setReceivedByteCount(receivedByteCount); |
| if (byteBuffer.position() != initialPosition || byteBuffer.limit() != initialLimit) { |
| failWithException( |
| new CronetExceptionImpl("ByteBuffer modified externally during read", null)); |
| return; |
| } |
| if (mOnReadCompletedTask == null) { |
| mOnReadCompletedTask = new OnReadCompletedRunnable(); |
| } |
| byteBuffer.position(initialPosition + bytesRead); |
| mOnReadCompletedTask.mByteBuffer = byteBuffer; |
| postTaskToExecutor(mOnReadCompletedTask); |
| } |
| |
| /** |
| * Called when request is completed successfully, no callbacks will be |
| * called afterwards. |
| * |
| * @param receivedByteCount number of bytes received. |
| */ |
| @SuppressWarnings("unused") |
| @CalledByNative |
| private void onSucceeded(long receivedByteCount) { |
| mResponseInfo.setReceivedByteCount(receivedByteCount); |
| Runnable task = |
| new Runnable() { |
| @Override |
| public void run() { |
| synchronized (mUrlRequestAdapterLock) { |
| if (isDoneLocked()) { |
| return; |
| } |
| // Destroy adapter first, so request context could be shut |
| // down from the listener. |
| destroyRequestAdapterLocked(RequestFinishedInfo.SUCCEEDED); |
| } |
| try { |
| mCallback.onSucceeded(CronetUrlRequest.this, mResponseInfo); |
| } catch (Exception e) { |
| Log.e( |
| CronetUrlRequestContext.LOG_TAG, |
| "Exception in onSucceeded method", |
| e); |
| } |
| maybeReportMetrics(); |
| } |
| }; |
| postTaskToExecutor(task); |
| } |
| |
| /** |
| * Called when error has occurred, no callbacks will be called afterwards. |
| * |
| * @param errorCode Error code represented by {@code UrlRequestError} that should be mapped |
| * to one of {@link NetworkException#ERROR_HOSTNAME_NOT_RESOLVED |
| * NetworkException.ERROR_*}. |
| * @param nativeError native net error code. |
| * @param errorString textual representation of the error code. |
| * @param receivedByteCount number of bytes received. |
| */ |
| @SuppressWarnings("unused") |
| @CalledByNative |
| private void onError( |
| int errorCode, |
| int nativeError, |
| int nativeQuicError, |
| String errorString, |
| long receivedByteCount) { |
| if (mResponseInfo != null) { |
| mResponseInfo.setReceivedByteCount(receivedByteCount); |
| } |
| if (errorCode == NetworkException.ERROR_QUIC_PROTOCOL_FAILED |
| || errorCode == NetworkException.ERROR_NETWORK_CHANGED) { |
| failWithException( |
| new QuicExceptionImpl( |
| "Exception in CronetUrlRequest: " + errorString, |
| errorCode, |
| nativeError, |
| nativeQuicError)); |
| } else { |
| int javaError = mapUrlRequestErrorToApiErrorCode(errorCode); |
| failWithException( |
| new NetworkExceptionImpl( |
| "Exception in CronetUrlRequest: " + errorString, |
| javaError, |
| nativeError)); |
| } |
| } |
| |
| /** Called when request is canceled, no callbacks will be called afterwards. */ |
| @SuppressWarnings("unused") |
| @CalledByNative |
| private void onCanceled() { |
| Runnable task = |
| new Runnable() { |
| @Override |
| public void run() { |
| try { |
| mCallback.onCanceled(CronetUrlRequest.this, mResponseInfo); |
| } catch (Exception e) { |
| Log.e( |
| CronetUrlRequestContext.LOG_TAG, |
| "Exception in onCanceled method", |
| e); |
| } |
| maybeReportMetrics(); |
| } |
| }; |
| postTaskToExecutor(task); |
| } |
| |
| /** |
| * Called by the native code when request status is fetched from the |
| * native stack. |
| */ |
| @SuppressWarnings("unused") |
| @CalledByNative |
| private void onStatus( |
| final VersionSafeCallbacks.UrlRequestStatusListener listener, final int loadState) { |
| Runnable task = |
| new Runnable() { |
| @Override |
| public void run() { |
| listener.onStatus(convertLoadState(loadState)); |
| } |
| }; |
| postTaskToExecutor(task); |
| } |
| |
| /** |
| * Called by the native code on the network thread to report metrics. Happens before |
| * onSucceeded, onError and onCanceled. |
| */ |
| @SuppressWarnings("unused") |
| @CalledByNative |
| private void onMetricsCollected( |
| long requestStartMs, |
| long dnsStartMs, |
| long dnsEndMs, |
| long connectStartMs, |
| long connectEndMs, |
| long sslStartMs, |
| long sslEndMs, |
| long sendingStartMs, |
| long sendingEndMs, |
| long pushStartMs, |
| long pushEndMs, |
| long responseStartMs, |
| long requestEndMs, |
| boolean socketReused, |
| long sentByteCount, |
| long receivedByteCount, |
| boolean quicConnectionMigrationAttempted, |
| boolean quicConnectionMigrationSuccessful) { |
| synchronized (mUrlRequestAdapterLock) { |
| if (mMetrics != null) { |
| throw new IllegalStateException("Metrics collection should only happen once."); |
| } |
| mMetrics = |
| new CronetMetrics( |
| requestStartMs, |
| dnsStartMs, |
| dnsEndMs, |
| connectStartMs, |
| connectEndMs, |
| sslStartMs, |
| sslEndMs, |
| sendingStartMs, |
| sendingEndMs, |
| pushStartMs, |
| pushEndMs, |
| responseStartMs, |
| requestEndMs, |
| socketReused, |
| sentByteCount, |
| receivedByteCount); |
| mQuicConnectionMigrationAttempted = quicConnectionMigrationAttempted; |
| mQuicConnectionMigrationSuccessful = quicConnectionMigrationSuccessful; |
| } |
| // Metrics are reported to RequestFinishedListener when the final UrlRequest.Callback has |
| // been invoked. |
| } |
| |
| /** Called when the native adapter is destroyed. */ |
| @SuppressWarnings("unused") |
| @CalledByNative |
| private void onNativeAdapterDestroyed() { |
| synchronized (mUrlRequestAdapterLock) { |
| if (mOnDestroyedCallbackForTesting != null) { |
| mOnDestroyedCallbackForTesting.run(); |
| } |
| // mException is set when an error is encountered (in native code via onError or in |
| // Java code). If mException is not null, notify the mCallback and report metrics. |
| if (mException == null) { |
| return; |
| } |
| } |
| Runnable task = |
| new Runnable() { |
| @Override |
| public void run() { |
| try { |
| mCallback.onFailed(CronetUrlRequest.this, mResponseInfo, mException); |
| } catch (Exception e) { |
| Log.e( |
| CronetUrlRequestContext.LOG_TAG, |
| "Exception in onFailed method", |
| e); |
| } |
| maybeReportMetrics(); |
| } |
| }; |
| try { |
| mExecutor.execute(task); |
| } catch (RejectedExecutionException e) { |
| Log.e(CronetUrlRequestContext.LOG_TAG, "Exception posting task to executor", e); |
| } |
| } |
| |
| /** Enforces prohibition of direct execution. */ |
| void checkCallingThread() { |
| if (!mAllowDirectExecutor && mRequestContext.isNetworkThread(Thread.currentThread())) { |
| throw new InlineExecutionProhibitedException(); |
| } |
| } |
| |
| private int mapUrlRequestErrorToApiErrorCode(int errorCode) { |
| switch (errorCode) { |
| case UrlRequestError.HOSTNAME_NOT_RESOLVED: |
| return NetworkException.ERROR_HOSTNAME_NOT_RESOLVED; |
| case UrlRequestError.INTERNET_DISCONNECTED: |
| return NetworkException.ERROR_INTERNET_DISCONNECTED; |
| case UrlRequestError.NETWORK_CHANGED: |
| return NetworkException.ERROR_NETWORK_CHANGED; |
| case UrlRequestError.TIMED_OUT: |
| return NetworkException.ERROR_TIMED_OUT; |
| case UrlRequestError.CONNECTION_CLOSED: |
| return NetworkException.ERROR_CONNECTION_CLOSED; |
| case UrlRequestError.CONNECTION_TIMED_OUT: |
| return NetworkException.ERROR_CONNECTION_TIMED_OUT; |
| case UrlRequestError.CONNECTION_REFUSED: |
| return NetworkException.ERROR_CONNECTION_REFUSED; |
| case UrlRequestError.CONNECTION_RESET: |
| return NetworkException.ERROR_CONNECTION_RESET; |
| case UrlRequestError.ADDRESS_UNREACHABLE: |
| return NetworkException.ERROR_ADDRESS_UNREACHABLE; |
| case UrlRequestError.QUIC_PROTOCOL_FAILED: |
| return NetworkException.ERROR_QUIC_PROTOCOL_FAILED; |
| case UrlRequestError.OTHER: |
| return NetworkException.ERROR_OTHER; |
| default: |
| Log.e(CronetUrlRequestContext.LOG_TAG, "Unknown error code: " + errorCode); |
| return errorCode; |
| } |
| } |
| |
| /** |
| * Builds the {@link CronetTrafficInfo} associated to this request internal state. |
| * This helper methods makes strong assumptions about the state of the request. For this reason |
| * it should only be called within {@link CronetUrlRequest#maybeReportMetrics} where these |
| * assumptions are guaranteed to be true. |
| * @return the {@link CronetTrafficInfo} associated to this request internal state |
| */ |
| @RequiresApi(Build.VERSION_CODES.O) |
| private CronetTrafficInfo buildCronetTrafficInfo() { |
| assert mMetrics != null; |
| assert mRequestHeaders != null; |
| |
| // Most of the CronetTrafficInfo fields have similar names/semantics. To avoid bugs due to |
| // typos everything is final, this means that things have to initialized through an if/else. |
| final Map<String, List<String>> responseHeaders; |
| final String negotiatedProtocol; |
| final int httpStatusCode; |
| final boolean wasCached; |
| if (mResponseInfo != null) { |
| responseHeaders = mResponseInfo.getAllHeaders(); |
| negotiatedProtocol = mResponseInfo.getNegotiatedProtocol(); |
| httpStatusCode = mResponseInfo.getHttpStatusCode(); |
| wasCached = mResponseInfo.wasCached(); |
| } else { |
| responseHeaders = Collections.emptyMap(); |
| negotiatedProtocol = ""; |
| httpStatusCode = 0; |
| wasCached = false; |
| } |
| |
| // TODO(stefanoduo): A better approach might be keeping track of the total length of an |
| // upload and use that value as the request body size instead. |
| final long requestTotalSizeInBytes = mMetrics.getSentByteCount(); |
| final long requestHeaderSizeInBytes; |
| final long requestBodySizeInBytes; |
| // Cached responses might still need to be revalidated over the network before being served |
| // (from UrlResponseInfo#wasCached documentation). |
| if (wasCached && requestTotalSizeInBytes == 0) { |
| // Served from cache without the need to revalidate. |
| requestHeaderSizeInBytes = 0; |
| requestBodySizeInBytes = 0; |
| } else { |
| // Served from cache with the need to revalidate or served from the network directly. |
| requestHeaderSizeInBytes = estimateHeadersSizeInBytes(mRequestHeaders); |
| requestBodySizeInBytes = max(0, requestTotalSizeInBytes - requestHeaderSizeInBytes); |
| } |
| |
| final long responseTotalSizeInBytes = mMetrics.getReceivedByteCount(); |
| final long responseBodySizeInBytes; |
| final long responseHeaderSizeInBytes; |
| // Cached responses might still need to be revalidated over the network before being served |
| // (from UrlResponseInfo#wasCached documentation). |
| if (wasCached && responseTotalSizeInBytes == 0) { |
| // Served from cache without the need to revalidate. |
| responseBodySizeInBytes = 0; |
| responseHeaderSizeInBytes = 0; |
| } else { |
| // Served from cache with the need to revalidate or served from the network directly. |
| responseHeaderSizeInBytes = estimateHeadersSizeInBytes(responseHeaders); |
| responseBodySizeInBytes = max(0, responseTotalSizeInBytes - responseHeaderSizeInBytes); |
| } |
| |
| final Duration headersLatency; |
| if (mMetrics.getRequestStart() != null && mMetrics.getResponseStart() != null) { |
| headersLatency = |
| Duration.ofMillis( |
| mMetrics.getResponseStart().getTime() |
| - mMetrics.getRequestStart().getTime()); |
| } else { |
| headersLatency = Duration.ofSeconds(0); |
| } |
| |
| final Duration totalLatency; |
| if (mMetrics.getRequestStart() != null && mMetrics.getRequestEnd() != null) { |
| totalLatency = |
| Duration.ofMillis( |
| mMetrics.getRequestEnd().getTime() |
| - mMetrics.getRequestStart().getTime()); |
| } else { |
| totalLatency = Duration.ofSeconds(0); |
| } |
| |
| return new CronetTrafficInfo( |
| requestHeaderSizeInBytes, |
| requestBodySizeInBytes, |
| responseHeaderSizeInBytes, |
| responseBodySizeInBytes, |
| httpStatusCode, |
| headersLatency, |
| totalLatency, |
| negotiatedProtocol, |
| mQuicConnectionMigrationAttempted, |
| mQuicConnectionMigrationSuccessful); |
| } |
| |
| // Maybe report metrics. This method should only be called on Callback's executor thread and |
| // after Callback's onSucceeded, onFailed and onCanceled. |
| private void maybeReportMetrics() { |
| final RefCountDelegate inflightCallbackCount = |
| new RefCountDelegate(() -> mRequestContext.onRequestFinished()); |
| try { |
| if (mMetrics == null) return; |
| |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { |
| try { |
| mLogger.logCronetTrafficInfo(mCronetEngineId, buildCronetTrafficInfo()); |
| } catch (RuntimeException e) { |
| // Handle any issue gracefully, we should never crash due failures while |
| // logging. |
| Log.e( |
| CronetUrlRequestContext.LOG_TAG, |
| "Error while trying to log CronetTrafficInfo: ", |
| e); |
| } |
| } |
| |
| final RequestFinishedInfo requestInfo = |
| new RequestFinishedInfoImpl( |
| mInitialUrl, |
| mRequestAnnotations, |
| mMetrics, |
| mFinishedReason, |
| mResponseInfo, |
| mException); |
| mRequestContext.reportRequestFinished(requestInfo, inflightCallbackCount); |
| if (mRequestFinishedListener != null) { |
| inflightCallbackCount.increment(); |
| try { |
| mRequestFinishedListener |
| .getExecutor() |
| .execute( |
| new Runnable() { |
| @Override |
| public void run() { |
| try { |
| mRequestFinishedListener.onRequestFinished( |
| requestInfo); |
| } catch (Exception e) { |
| Log.e( |
| CronetUrlRequestContext.LOG_TAG, |
| "Exception thrown from request" |
| + " finishedlistener", |
| e); |
| } finally { |
| inflightCallbackCount.decrement(); |
| } |
| } |
| }); |
| } catch (RejectedExecutionException failException) { |
| Log.e( |
| CronetUrlRequestContext.LOG_TAG, |
| "Exception posting task to executor", |
| failException); |
| inflightCallbackCount.decrement(); |
| } |
| } |
| } finally { |
| inflightCallbackCount.decrement(); |
| } |
| } |
| |
| // Native methods are implemented in cronet_url_request_adapter.cc. |
| @NativeMethods |
| interface Natives { |
| long createRequestAdapter( |
| CronetUrlRequest caller, |
| long urlRequestContextAdapter, |
| String url, |
| int priority, |
| boolean disableCache, |
| boolean disableConnectionMigration, |
| boolean trafficStatsTagSet, |
| int trafficStatsTag, |
| boolean trafficStatsUidSet, |
| int trafficStatsUid, |
| int idempotency, |
| long networkHandle); |
| |
| @NativeClassQualifiedName("CronetURLRequestAdapter") |
| boolean setHttpMethod(long nativePtr, CronetUrlRequest caller, String method); |
| |
| @NativeClassQualifiedName("CronetURLRequestAdapter") |
| boolean addRequestHeader( |
| long nativePtr, CronetUrlRequest caller, String name, String value); |
| |
| @NativeClassQualifiedName("CronetURLRequestAdapter") |
| void start(long nativePtr, CronetUrlRequest caller); |
| |
| @NativeClassQualifiedName("CronetURLRequestAdapter") |
| void followDeferredRedirect(long nativePtr, CronetUrlRequest caller); |
| |
| @NativeClassQualifiedName("CronetURLRequestAdapter") |
| boolean readData( |
| long nativePtr, |
| CronetUrlRequest caller, |
| ByteBuffer byteBuffer, |
| int position, |
| int capacity); |
| |
| @NativeClassQualifiedName("CronetURLRequestAdapter") |
| void destroy(long nativePtr, CronetUrlRequest caller, boolean sendOnCanceled); |
| |
| @NativeClassQualifiedName("CronetURLRequestAdapter") |
| void getStatus( |
| long nativePtr, |
| CronetUrlRequest caller, |
| VersionSafeCallbacks.UrlRequestStatusListener listener); |
| } |
| } |