| // Copyright 2019 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.test; |
| |
| import android.util.Log; |
| |
| import androidx.annotation.GuardedBy; |
| import androidx.annotation.VisibleForTesting; |
| |
| import org.chromium.net.CronetException; |
| import org.chromium.net.InlineExecutionProhibitedException; |
| import org.chromium.net.RequestFinishedInfo; |
| import org.chromium.net.UploadDataProvider; |
| import org.chromium.net.UrlResponseInfo; |
| import org.chromium.net.impl.CallbackExceptionImpl; |
| import org.chromium.net.impl.CronetExceptionImpl; |
| import org.chromium.net.impl.JavaUploadDataSinkBase; |
| import org.chromium.net.impl.JavaUrlRequestUtils; |
| import org.chromium.net.impl.JavaUrlRequestUtils.CheckedRunnable; |
| import org.chromium.net.impl.JavaUrlRequestUtils.DirectPreventingExecutor; |
| import org.chromium.net.impl.JavaUrlRequestUtils.State; |
| import org.chromium.net.impl.Preconditions; |
| import org.chromium.net.impl.RefCountDelegate; |
| import org.chromium.net.impl.RequestFinishedInfoImpl; |
| import org.chromium.net.impl.UrlRequestBase; |
| import org.chromium.net.impl.UrlResponseInfoImpl; |
| |
| import java.io.ByteArrayOutputStream; |
| import java.io.IOException; |
| import java.net.URI; |
| import java.nio.ByteBuffer; |
| import java.nio.channels.Channels; |
| import java.nio.channels.WritableByteChannel; |
| import java.util.AbstractMap; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.concurrent.Executor; |
| import java.util.concurrent.RejectedExecutionException; |
| |
| /** |
| * Fake UrlRequest that retrieves responses from the associated FakeCronetController. Used for |
| * testing Cronet usage on Android. |
| */ |
| final class FakeUrlRequest extends UrlRequestBase { |
| // Used for logging errors. |
| private static final String TAG = FakeUrlRequest.class.getSimpleName(); |
| // Callback used to report responses to the client. |
| private final Callback mCallback; |
| // The {@link Executor} provided by the user to be used for callbacks. |
| private final Executor mUserExecutor; |
| // The {@link Executor} provided by the engine used to break up callback loops. |
| private final Executor mExecutor; |
| // The Annotations provided by the engine during the creation of this request. |
| private final Collection<Object> mRequestAnnotations; |
| // The {@link FakeCronetController} that will provide responses for this request. |
| private final FakeCronetController mFakeCronetController; |
| // The fake {@link CronetEngine} that should be notified when this request starts and stops. |
| private final FakeCronetEngine mFakeCronetEngine; |
| |
| // Source of thread safety for this class. |
| @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) |
| final Object mLock = new Object(); |
| |
| // True if direct execution is allowed for this request. |
| private final boolean mAllowDirectExecutor; |
| |
| // The chain of URL's this request has received. |
| @GuardedBy("mLock") |
| private final List<String> mUrlChain = new ArrayList<>(); |
| |
| // The list of HTTP headers used by this request to establish a connection. |
| @GuardedBy("mLock") |
| private final ArrayList<Map.Entry<String, String>> mAllHeadersList = new ArrayList<>(); |
| |
| // The exception that is thrown by the request. This is the same exception as the one in |
| // onFailed |
| @GuardedBy("mLock") |
| private CronetException mCronetException; |
| |
| // The current URL this request is connecting to. |
| @GuardedBy("mLock") |
| private String mCurrentUrl; |
| |
| // The {@link FakeUrlResponse} for the current URL. |
| @GuardedBy("mLock") |
| private FakeUrlResponse mCurrentFakeResponse; |
| |
| // The body of the request from UploadDataProvider. |
| @GuardedBy("mLock") |
| private byte[] mRequestBody; |
| |
| // The {@link UploadDataProvider} to retrieve a request body from. |
| @GuardedBy("mLock") |
| private UploadDataProvider mUploadDataProvider; |
| |
| // The executor to call the {@link UploadDataProvider}'s callback methods with. |
| @GuardedBy("mLock") |
| private Executor mUploadExecutor; |
| |
| // The {@link UploadDataSink} for the {@link UploadDataProvider}. |
| @GuardedBy("mLock") |
| @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) |
| FakeDataSink mFakeDataSink; |
| |
| // The {@link UrlResponseInfo} for the current request. |
| @GuardedBy("mLock") |
| private UrlResponseInfo mUrlResponseInfo; |
| |
| // The response from the current request that needs to be sent. |
| @GuardedBy("mLock") |
| private ByteBuffer mResponse; |
| |
| // The HTTP method used by this request to establish a connection. |
| @GuardedBy("mLock") |
| private String mHttpMethod; |
| |
| // True after the {@link UploadDataProvider} for this request has been closed. |
| @GuardedBy("mLock") |
| private boolean mUploadProviderClosed; |
| |
| @GuardedBy("mLock") |
| @State |
| private int mState = State.NOT_STARTED; |
| |
| /** |
| * Holds a subset of StatusValues - {@link State#STARTED} can represent |
| * {@link Status#SENDING_REQUEST} or {@link Status#WAITING_FOR_RESPONSE}. While the distinction |
| * isn't needed to implement the logic in this class, it is needed to implement |
| * {@link #getStatus(StatusListener)}. |
| */ |
| @StatusValues private volatile int mAdditionalStatusDetails = Status.INVALID; |
| |
| /** Used to map from HTTP status codes to the corresponding human-readable text. */ |
| private static final Map<Integer, String> HTTP_STATUS_CODE_TO_TEXT; |
| |
| static { |
| Map<Integer, String> httpCodeMap = new HashMap<>(); |
| httpCodeMap.put(100, "Continue"); |
| httpCodeMap.put(101, "Switching Protocols"); |
| httpCodeMap.put(102, "Processing"); |
| httpCodeMap.put(103, "Early Hints"); |
| httpCodeMap.put(200, "OK"); |
| httpCodeMap.put(201, "Created"); |
| httpCodeMap.put(202, "Accepted"); |
| httpCodeMap.put(203, "Non-Authoritative Information"); |
| httpCodeMap.put(204, "No Content"); |
| httpCodeMap.put(205, "Reset Content"); |
| httpCodeMap.put(206, "Partial Content"); |
| httpCodeMap.put(207, "Multi-Status"); |
| httpCodeMap.put(208, "Already Reported"); |
| httpCodeMap.put(226, "IM Used"); |
| httpCodeMap.put(300, "Multiple Choices"); |
| httpCodeMap.put(301, "Moved Permanently"); |
| httpCodeMap.put(302, "Found"); |
| httpCodeMap.put(303, "See Other"); |
| httpCodeMap.put(304, "Not Modified"); |
| httpCodeMap.put(305, "Use Proxy"); |
| httpCodeMap.put(306, "Unused"); |
| httpCodeMap.put(307, "Temporary Redirect"); |
| httpCodeMap.put(308, "Permanent Redirect"); |
| httpCodeMap.put(400, "Bad Request"); |
| httpCodeMap.put(401, "Unauthorized"); |
| httpCodeMap.put(402, "Payment Required"); |
| httpCodeMap.put(403, "Forbidden"); |
| httpCodeMap.put(404, "Not Found"); |
| httpCodeMap.put(405, "Method Not Allowed"); |
| httpCodeMap.put(406, "Not Acceptable"); |
| httpCodeMap.put(407, "Proxy Authentication Required"); |
| httpCodeMap.put(408, "Request Timeout"); |
| httpCodeMap.put(409, "Conflict"); |
| httpCodeMap.put(410, "Gone"); |
| httpCodeMap.put(411, "Length Required"); |
| httpCodeMap.put(412, "Precondition Failed"); |
| httpCodeMap.put(413, "Payload Too Large"); |
| httpCodeMap.put(414, "URI Too Long"); |
| httpCodeMap.put(415, "Unsupported Media Type"); |
| httpCodeMap.put(416, "Range Not Satisfiable"); |
| httpCodeMap.put(417, "Expectation Failed"); |
| httpCodeMap.put(421, "Misdirected Request"); |
| httpCodeMap.put(422, "Unprocessable Entity"); |
| httpCodeMap.put(423, "Locked"); |
| httpCodeMap.put(424, "Failed Dependency"); |
| httpCodeMap.put(425, "Too Early"); |
| httpCodeMap.put(426, "Upgrade Required"); |
| httpCodeMap.put(428, "Precondition Required"); |
| httpCodeMap.put(429, "Too Many Requests"); |
| httpCodeMap.put(431, "Request Header Fields Too Large"); |
| httpCodeMap.put(451, "Unavailable For Legal Reasons"); |
| httpCodeMap.put(500, "Internal Server Error"); |
| httpCodeMap.put(501, "Not Implemented"); |
| httpCodeMap.put(502, "Bad Gateway"); |
| httpCodeMap.put(503, "Service Unavailable"); |
| httpCodeMap.put(504, "Gateway Timeout"); |
| httpCodeMap.put(505, "HTTP Version Not Supported"); |
| httpCodeMap.put(506, "Variant Also Negotiates"); |
| httpCodeMap.put(507, "Insufficient Storage"); |
| httpCodeMap.put(508, "Loop Denied"); |
| httpCodeMap.put(510, "Not Extended"); |
| httpCodeMap.put(511, "Network Authentication Required"); |
| HTTP_STATUS_CODE_TO_TEXT = Collections.unmodifiableMap(httpCodeMap); |
| } |
| |
| FakeUrlRequest( |
| Callback callback, |
| Executor userExecutor, |
| Executor executor, |
| String url, |
| boolean allowDirectExecutor, |
| boolean trafficStatsTagSet, |
| int trafficStatsTag, |
| final boolean trafficStatsUidSet, |
| final int trafficStatsUid, |
| FakeCronetController fakeCronetController, |
| FakeCronetEngine fakeCronetEngine, |
| Collection<Object> requestAnnotations) { |
| if (url == null) { |
| throw new NullPointerException("URL is required"); |
| } |
| if (callback == null) { |
| throw new NullPointerException("Listener is required"); |
| } |
| if (executor == null) { |
| throw new NullPointerException("Executor is required"); |
| } |
| mCallback = callback; |
| mUserExecutor = |
| allowDirectExecutor ? userExecutor : new DirectPreventingExecutor(userExecutor); |
| mExecutor = executor; |
| mCurrentUrl = url; |
| mFakeCronetController = fakeCronetController; |
| mFakeCronetEngine = fakeCronetEngine; |
| mAllowDirectExecutor = allowDirectExecutor; |
| mRequestAnnotations = requestAnnotations; |
| } |
| |
| @Override |
| public void setUploadDataProvider(UploadDataProvider uploadDataProvider, Executor executor) { |
| if (uploadDataProvider == null) { |
| throw new NullPointerException("Invalid UploadDataProvider."); |
| } |
| synchronized (mLock) { |
| if (!checkHasContentTypeHeader()) { |
| throw new IllegalArgumentException( |
| "Requests with upload data must have a Content-Type."); |
| } |
| checkNotStarted(); |
| if (mHttpMethod == null) { |
| mHttpMethod = "POST"; |
| } |
| mUploadExecutor = |
| mAllowDirectExecutor ? executor : new DirectPreventingExecutor(executor); |
| mUploadDataProvider = uploadDataProvider; |
| } |
| } |
| |
| @Override |
| public void setHttpMethod(String method) { |
| synchronized (mLock) { |
| checkNotStarted(); |
| if (method == null) { |
| throw new NullPointerException("Method is required."); |
| } |
| if ("OPTIONS".equalsIgnoreCase(method) |
| || "GET".equalsIgnoreCase(method) |
| || "HEAD".equalsIgnoreCase(method) |
| || "POST".equalsIgnoreCase(method) |
| || "PUT".equalsIgnoreCase(method) |
| || "DELETE".equalsIgnoreCase(method) |
| || "TRACE".equalsIgnoreCase(method) |
| || "PATCH".equalsIgnoreCase(method)) { |
| mHttpMethod = method; |
| } else { |
| throw new IllegalArgumentException("Invalid http method: " + method); |
| } |
| } |
| } |
| |
| @Override |
| public void addHeader(String header, String value) { |
| synchronized (mLock) { |
| checkNotStarted(); |
| mAllHeadersList.add(new AbstractMap.SimpleEntry<String, String>(header, value)); |
| } |
| } |
| |
| /** Verifies that the request is not already started and throws an exception if it is. */ |
| @GuardedBy("mLock") |
| private void checkNotStarted() { |
| if (mState != State.NOT_STARTED) { |
| throw new IllegalStateException("Request is already started. State is: " + mState); |
| } |
| } |
| |
| @Override |
| public void start() { |
| synchronized (mLock) { |
| if (mFakeCronetEngine.startRequest()) { |
| boolean transitionedState = false; |
| try { |
| transitionStates(State.NOT_STARTED, State.STARTED); |
| mAdditionalStatusDetails = Status.CONNECTING; |
| transitionedState = true; |
| } finally { |
| if (!transitionedState) { |
| cleanup(); |
| mFakeCronetEngine.onRequestFinished(); |
| } |
| } |
| mUrlChain.add(mCurrentUrl); |
| if (mUploadDataProvider != null) { |
| mFakeDataSink = |
| new FakeDataSink(mUploadExecutor, mExecutor, mUploadDataProvider); |
| mFakeDataSink.start(/* firstTime= */ true); |
| } else { |
| fakeConnect(); |
| } |
| } else { |
| throw new IllegalStateException("This request's CronetEngine is already shutdown."); |
| } |
| } |
| } |
| |
| /** |
| * Fakes a request to a server by retrieving a response to this {@link UrlRequest} from the |
| * {@link FakeCronetController}. |
| */ |
| @GuardedBy("mLock") |
| private void fakeConnect() { |
| mAdditionalStatusDetails = Status.WAITING_FOR_RESPONSE; |
| mCurrentFakeResponse = |
| mFakeCronetController.getResponse( |
| mCurrentUrl, mHttpMethod, mAllHeadersList, mRequestBody); |
| int responseCode = mCurrentFakeResponse.getHttpStatusCode(); |
| mUrlResponseInfo = |
| new UrlResponseInfoImpl( |
| Collections.unmodifiableList(new ArrayList<>(mUrlChain)), |
| responseCode, |
| getDescriptionByCode(responseCode), |
| mCurrentFakeResponse.getAllHeadersList(), |
| mCurrentFakeResponse.getWasCached(), |
| mCurrentFakeResponse.getNegotiatedProtocol(), |
| mCurrentFakeResponse.getProxyServer(), |
| mCurrentFakeResponse.getResponseBody().length); |
| mResponse = ByteBuffer.wrap(mCurrentFakeResponse.getResponseBody()); |
| // Check for a redirect. |
| if (responseCode >= 300 && responseCode < 400) { |
| processRedirectResponse(); |
| } else { |
| closeUploadDataProvider(); |
| final UrlResponseInfo info = mUrlResponseInfo; |
| transitionStates(State.STARTED, State.AWAITING_READ); |
| executeCheckedRunnable( |
| () -> { |
| mCallback.onResponseStarted(FakeUrlRequest.this, info); |
| }); |
| } |
| } |
| |
| /** |
| * Retrieves the redirect location from the response headers and responds to the |
| * {@link UrlRequest.Callback#onRedirectReceived} method. Adds the redirect URL to the chain. |
| * |
| * @param url the URL that the {@link FakeUrlResponse} redirected this request to |
| */ |
| @GuardedBy("mLock") |
| private void processRedirectResponse() { |
| transitionStates(State.STARTED, State.REDIRECT_RECEIVED); |
| if (mUrlResponseInfo.getAllHeaders().get("location") == null) { |
| // Response did not have a location header, so this request must fail. |
| final String prevUrl = mCurrentUrl; |
| mUserExecutor.execute( |
| () -> { |
| tryToFailWithException( |
| new CronetExceptionImpl( |
| "Request failed due to bad redirect HTTP headers", |
| new IllegalStateException( |
| "Response recieved from URL: " |
| + prevUrl |
| + " was a redirect, but lacked a location header."))); |
| }); |
| return; |
| } |
| String pendingRedirectUrl = |
| URI.create(mCurrentUrl) |
| .resolve(mUrlResponseInfo.getAllHeaders().get("location").get(0)) |
| .toString(); |
| mCurrentUrl = pendingRedirectUrl; |
| mUrlChain.add(mCurrentUrl); |
| transitionStates(State.REDIRECT_RECEIVED, State.AWAITING_FOLLOW_REDIRECT); |
| final UrlResponseInfo info = mUrlResponseInfo; |
| mExecutor.execute( |
| () -> { |
| executeCheckedRunnable( |
| () -> { |
| mCallback.onRedirectReceived( |
| FakeUrlRequest.this, info, pendingRedirectUrl); |
| }); |
| }); |
| } |
| |
| @Override |
| public void read(ByteBuffer buffer) { |
| // Entering {@link #State.READING} is somewhat redundant because the entire response is |
| // already acquired. We should still transition so that the fake {@link UrlRequest} follows |
| // the same state flow as a real request. |
| Preconditions.checkHasRemaining(buffer); |
| Preconditions.checkDirect(buffer); |
| synchronized (mLock) { |
| transitionStates(State.AWAITING_READ, State.READING); |
| final UrlResponseInfo info = mUrlResponseInfo; |
| if (mResponse.hasRemaining()) { |
| transitionStates(State.READING, State.AWAITING_READ); |
| fillBufferWithResponse(buffer); |
| mExecutor.execute( |
| () -> { |
| executeCheckedRunnable( |
| () -> { |
| mCallback.onReadCompleted( |
| FakeUrlRequest.this, info, buffer); |
| }); |
| }); |
| } else { |
| final RefCountDelegate inflightDoneCallbackCount = setTerminalState(State.COMPLETE); |
| if (inflightDoneCallbackCount != null) { |
| mUserExecutor.execute( |
| () -> { |
| mCallback.onSucceeded(FakeUrlRequest.this, info); |
| inflightDoneCallbackCount.decrement(); |
| }); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Puts as much of the remaining response as will fit into the {@link ByteBuffer} and removes |
| * that part of the string from the response left to send. |
| * |
| * @param buffer the {@link ByteBuffer} to put the response into |
| * @return the buffer with the response that we want to send back in it |
| */ |
| @GuardedBy("mLock") |
| private void fillBufferWithResponse(ByteBuffer buffer) { |
| final int maxTransfer = Math.min(buffer.remaining(), mResponse.remaining()); |
| ByteBuffer temp = mResponse.duplicate(); |
| temp.limit(temp.position() + maxTransfer); |
| buffer.put(temp); |
| mResponse.position(mResponse.position() + maxTransfer); |
| } |
| |
| @Override |
| public void followRedirect() { |
| synchronized (mLock) { |
| transitionStates(State.AWAITING_FOLLOW_REDIRECT, State.STARTED); |
| if (mFakeDataSink != null) { |
| mFakeDataSink = new FakeDataSink(mUploadExecutor, mExecutor, mUploadDataProvider); |
| mFakeDataSink.start(/* firstTime= */ false); |
| } else { |
| fakeConnect(); |
| } |
| } |
| } |
| |
| @Override |
| public void cancel() { |
| synchronized (mLock) { |
| if (mState == State.NOT_STARTED || isDone()) { |
| return; |
| } |
| |
| final UrlResponseInfo info = mUrlResponseInfo; |
| final RefCountDelegate inflightDoneCallbackCount = setTerminalState(State.CANCELLED); |
| if (inflightDoneCallbackCount != null) { |
| mUserExecutor.execute( |
| () -> { |
| mCallback.onCanceled(FakeUrlRequest.this, info); |
| inflightDoneCallbackCount.decrement(); |
| }); |
| } |
| } |
| } |
| |
| @Override |
| public void getStatus(final StatusListener listener) { |
| synchronized (mLock) { |
| int extraStatus = mAdditionalStatusDetails; |
| |
| @StatusValues final int status; |
| switch (mState) { |
| case State.ERROR: |
| case State.COMPLETE: |
| case State.CANCELLED: |
| case State.NOT_STARTED: |
| status = Status.INVALID; |
| break; |
| case State.STARTED: |
| status = extraStatus; |
| break; |
| case State.REDIRECT_RECEIVED: |
| case State.AWAITING_FOLLOW_REDIRECT: |
| case State.AWAITING_READ: |
| status = Status.IDLE; |
| break; |
| case State.READING: |
| status = Status.READING_RESPONSE; |
| break; |
| default: |
| throw new IllegalStateException("Switch is exhaustive: " + mState); |
| } |
| mUserExecutor.execute( |
| new Runnable() { |
| @Override |
| public void run() { |
| listener.onStatus(status); |
| } |
| }); |
| } |
| } |
| |
| @Override |
| public boolean isDone() { |
| synchronized (mLock) { |
| return mState == State.COMPLETE || mState == State.ERROR || mState == State.CANCELLED; |
| } |
| } |
| |
| /** |
| * Swaps from the expected state to a new state. If the swap fails, and it's not |
| * due to an earlier error or cancellation, throws an exception. |
| */ |
| @GuardedBy("mLock") |
| private void transitionStates(@State int expected, @State int newState) { |
| if (mState == expected) { |
| mState = newState; |
| } else { |
| if (!(mState == State.CANCELLED || mState == State.ERROR)) { |
| // TODO(crbug/1450573): Use Enums for state instead for better error messages. |
| throw new IllegalStateException( |
| "Invalid state transition - expected " + expected + " but was " + mState); |
| } |
| } |
| } |
| |
| /** |
| * Calls the callback's onFailed method if this request is not complete. Should be executed on |
| * the {@code mUserExecutor}, unless the error is a {@link InlineExecutionProhibitedException} |
| * produced by the {@code mUserExecutor}. |
| * |
| * @param e the {@link CronetException} that the request should pass to the callback. |
| * |
| */ |
| private void tryToFailWithException(CronetException e) { |
| synchronized (mLock) { |
| mCronetException = e; |
| final RefCountDelegate inflightDoneCallbackCount = setTerminalState(State.ERROR); |
| if (inflightDoneCallbackCount != null) { |
| mCallback.onFailed(FakeUrlRequest.this, mUrlResponseInfo, e); |
| inflightDoneCallbackCount.decrement(); |
| } |
| } |
| } |
| |
| /** |
| * Execute a {@link CheckedRunnable} and call the {@link UrlRequest.Callback#onFailed} method |
| * if there is an exception and we can change to {@link State.ERROR}. Used to communicate with |
| * the {@link UrlRequest.Callback} methods using the executor provided by the constructor. This |
| * should be the last call in the critical section. If this is not the last call in a critical |
| * section, we risk modifying shared resources in a recursive call to another method |
| * guarded by the {@code mLock}. This is because in Java synchronized blocks are reentrant. |
| * |
| * @param checkedRunnable the runnable to execute |
| */ |
| private void executeCheckedRunnable(JavaUrlRequestUtils.CheckedRunnable checkedRunnable) { |
| try { |
| mUserExecutor.execute( |
| () -> { |
| try { |
| checkedRunnable.run(); |
| } catch (Exception e) { |
| tryToFailWithException( |
| new CallbackExceptionImpl( |
| "Exception received from UrlRequest.Callback", e)); |
| } |
| }); |
| } catch (InlineExecutionProhibitedException e) { |
| // Don't try to fail using the {@code mUserExecutor} because it produced this error. |
| tryToFailWithException( |
| new CronetExceptionImpl("Exception posting task to executor", e)); |
| } |
| } |
| |
| /** |
| * Check the current state and if the request is started, but not complete, failed, or |
| * cancelled, change to the terminal state and call {@link FakeCronetEngine#onDestroyed}. This |
| * method ensures {@link FakeCronetEngine#onDestroyed} is only called once. |
| * |
| * @param terminalState the terminal state to set; one of {@link State.ERROR}, |
| * {@link State.COMPLETE}, or {@link State.CANCELLED} |
| * @return a refcount to decrement after the terminal callback is called, or |
| * null if the terminal state wasn't set. |
| */ |
| @GuardedBy("mLock") |
| private RefCountDelegate setTerminalState(@State int terminalState) { |
| switch (mState) { |
| case State.NOT_STARTED: |
| throw new IllegalStateException("Can't enter terminal state before start"); |
| case State.ERROR: // fallthrough |
| case State.COMPLETE: // fallthrough |
| case State.CANCELLED: |
| return null; // Already in a terminal state |
| default: |
| { |
| mState = terminalState; |
| final RefCountDelegate inflightDoneCallbackCount = |
| new RefCountDelegate(mFakeCronetEngine::onRequestFinished); |
| reportRequestFinished(inflightDoneCallbackCount); |
| cleanup(); |
| return inflightDoneCallbackCount; |
| } |
| } |
| } |
| |
| private void reportRequestFinished(RefCountDelegate inflightDoneCallbackCount) { |
| synchronized (mLock) { |
| mFakeCronetEngine.reportRequestFinished( |
| new FakeRequestFinishedInfo( |
| mCurrentUrl, |
| mRequestAnnotations, |
| getRequestFinishedReason(), |
| mUrlResponseInfo, |
| mCronetException), |
| inflightDoneCallbackCount); |
| } |
| } |
| |
| @RequestFinishedInfoImpl.FinishedReason |
| @GuardedBy("mLock") |
| private int getRequestFinishedReason() { |
| synchronized (mLock) { |
| switch (mState) { |
| case State.COMPLETE: |
| return RequestFinishedInfo.SUCCEEDED; |
| case State.ERROR: |
| return RequestFinishedInfo.FAILED; |
| case State.CANCELLED: |
| return RequestFinishedInfo.CANCELED; |
| default: |
| throw new IllegalStateException( |
| "Request should be in terminal state before calling getRequestFinishedReason"); |
| } |
| } |
| } |
| |
| @GuardedBy("mLock") |
| private void cleanup() { |
| closeUploadDataProvider(); |
| mFakeCronetEngine.onRequestDestroyed(); |
| } |
| |
| /** |
| * Executed only once after the request has finished using the {@link UploadDataProvider}. |
| * Closes the {@link UploadDataProvider} if it exists and has not already been closed. |
| */ |
| @GuardedBy("mLock") |
| private void closeUploadDataProvider() { |
| if (mUploadDataProvider != null && !mUploadProviderClosed) { |
| try { |
| mUploadExecutor.execute( |
| uploadErrorSetting( |
| () -> { |
| synchronized (mLock) { |
| mUploadDataProvider.close(); |
| mUploadProviderClosed = true; |
| } |
| })); |
| } catch (RejectedExecutionException e) { |
| Log.e(TAG, "Exception when closing uploadDataProvider", e); |
| } |
| } |
| } |
| |
| /** |
| * Wraps a {@link CheckedRunnable} in a runnable that will attempt to fail the request if there |
| * is an exception. |
| * |
| * @param delegate the {@link CheckedRunnable} to try to run |
| * @return a {@link Runnable} that wraps the delegate runnable. |
| */ |
| private Runnable uploadErrorSetting(final CheckedRunnable delegate) { |
| return new Runnable() { |
| @Override |
| public void run() { |
| try { |
| delegate.run(); |
| } catch (Throwable t) { |
| enterUploadErrorState(t); |
| } |
| } |
| }; |
| } |
| |
| /** |
| * Fails the request with an error. Called when uploading the request body using an |
| * {@link UploadDataProvider} fails. |
| * |
| * @param error the error that caused this request to fail which should be returned to the |
| * {@link UrlRequest.Callback} |
| */ |
| private void enterUploadErrorState(final Throwable error) { |
| synchronized (mLock) { |
| executeCheckedRunnable( |
| () -> |
| tryToFailWithException( |
| new CronetExceptionImpl( |
| "Exception received from UploadDataProvider", error))); |
| } |
| } |
| |
| /** |
| * Adapted from {@link JavaUrlRequest.OutputStreamDataSink}. Stores the received message in a |
| * {@link ByteArrayOutputStream} and transfers it to the {@code mRequestBody} when the response |
| * has been fully acquired. |
| */ |
| @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) |
| final class FakeDataSink extends JavaUploadDataSinkBase { |
| private final ByteArrayOutputStream mBodyStream = new ByteArrayOutputStream(); |
| private final WritableByteChannel mBodyChannel = Channels.newChannel(mBodyStream); |
| |
| FakeDataSink(final Executor userExecutor, Executor executor, UploadDataProvider provider) { |
| super(userExecutor, executor, provider); |
| } |
| |
| @Override |
| public Runnable getErrorSettingRunnable(JavaUrlRequestUtils.CheckedRunnable runnable) { |
| return new Runnable() { |
| @Override |
| public void run() { |
| try { |
| runnable.run(); |
| } catch (Throwable t) { |
| mUserExecutor.execute( |
| new Runnable() { |
| @Override |
| public void run() { |
| tryToFailWithException( |
| new CronetExceptionImpl("System error", t)); |
| } |
| }); |
| } |
| } |
| }; |
| } |
| |
| @Override |
| protected Runnable getUploadErrorSettingRunnable( |
| JavaUrlRequestUtils.CheckedRunnable runnable) { |
| return uploadErrorSetting(runnable); |
| } |
| |
| @Override |
| protected void processUploadError(final Throwable error) { |
| enterUploadErrorState(error); |
| } |
| |
| @Override |
| protected int processSuccessfulRead(ByteBuffer buffer) throws IOException { |
| return mBodyChannel.write(buffer); |
| } |
| |
| /** |
| * Terminates the upload stage of the request. Writes the received bytes to the byte array: |
| * {@code mRequestBody}. Connects to the current URL for this request. |
| */ |
| @Override |
| protected void finish() throws IOException { |
| synchronized (mLock) { |
| mRequestBody = mBodyStream.toByteArray(); |
| fakeConnect(); |
| } |
| } |
| |
| @Override |
| protected void initializeRead() throws IOException { |
| // Nothing to do before every read in this implementation. |
| } |
| |
| @Override |
| protected void initializeStart(long totalBytes) { |
| // Nothing to do to initialize the upload in this implementation. |
| } |
| } |
| |
| /** |
| * Verifies that the "content-type" header is present. Must be checked before an |
| * {@link UploadDataProvider} is premitted to be set. |
| * |
| * @return true if the "content-type" header is present in the request headers. |
| */ |
| @GuardedBy("mLock") |
| private boolean checkHasContentTypeHeader() { |
| for (Map.Entry<String, String> entry : mAllHeadersList) { |
| if (entry.getKey().equalsIgnoreCase("content-type")) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Gets a human readable description for a HTTP status code. |
| * |
| * @param code the code to retrieve the status for |
| * @return the HTTP status text as a string |
| */ |
| private static String getDescriptionByCode(Integer code) { |
| return HTTP_STATUS_CODE_TO_TEXT.containsKey(code) |
| ? HTTP_STATUS_CODE_TO_TEXT.get(code) |
| : "Unassigned"; |
| } |
| } |