• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2019 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.net.test;
6 
7 import android.util.Log;
8 
9 import androidx.annotation.GuardedBy;
10 import androidx.annotation.VisibleForTesting;
11 
12 import org.chromium.net.CronetException;
13 import org.chromium.net.InlineExecutionProhibitedException;
14 import org.chromium.net.RequestFinishedInfo;
15 import org.chromium.net.UploadDataProvider;
16 import org.chromium.net.UrlResponseInfo;
17 import org.chromium.net.impl.CallbackExceptionImpl;
18 import org.chromium.net.impl.CronetExceptionImpl;
19 import org.chromium.net.impl.JavaUploadDataSinkBase;
20 import org.chromium.net.impl.JavaUrlRequestUtils;
21 import org.chromium.net.impl.JavaUrlRequestUtils.CheckedRunnable;
22 import org.chromium.net.impl.JavaUrlRequestUtils.DirectPreventingExecutor;
23 import org.chromium.net.impl.JavaUrlRequestUtils.State;
24 import org.chromium.net.impl.Preconditions;
25 import org.chromium.net.impl.RefCountDelegate;
26 import org.chromium.net.impl.RequestFinishedInfoImpl;
27 import org.chromium.net.impl.UrlRequestBase;
28 import org.chromium.net.impl.UrlResponseInfoImpl;
29 
30 import java.io.ByteArrayOutputStream;
31 import java.io.IOException;
32 import java.net.URI;
33 import java.nio.ByteBuffer;
34 import java.nio.channels.Channels;
35 import java.nio.channels.WritableByteChannel;
36 import java.util.AbstractMap;
37 import java.util.ArrayList;
38 import java.util.Collection;
39 import java.util.Collections;
40 import java.util.HashMap;
41 import java.util.List;
42 import java.util.Map;
43 import java.util.concurrent.Executor;
44 import java.util.concurrent.RejectedExecutionException;
45 
46 /**
47  * Fake UrlRequest that retrieves responses from the associated FakeCronetController. Used for
48  * testing Cronet usage on Android.
49  */
50 final class FakeUrlRequest extends UrlRequestBase {
51     // Used for logging errors.
52     private static final String TAG = FakeUrlRequest.class.getSimpleName();
53     // Callback used to report responses to the client.
54     private final Callback mCallback;
55     // The {@link Executor} provided by the user to be used for callbacks.
56     private final Executor mUserExecutor;
57     // The {@link Executor} provided by the engine used to break up callback loops.
58     private final Executor mExecutor;
59     // The Annotations provided by the engine during the creation of this request.
60     private final Collection<Object> mRequestAnnotations;
61     // The {@link FakeCronetController} that will provide responses for this request.
62     private final FakeCronetController mFakeCronetController;
63     // The fake {@link CronetEngine} that should be notified when this request starts and stops.
64     private final FakeCronetEngine mFakeCronetEngine;
65 
66     // Source of thread safety for this class.
67     @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
68     final Object mLock = new Object();
69 
70     // True if direct execution is allowed for this request.
71     private final boolean mAllowDirectExecutor;
72 
73     // The chain of URL's this request has received.
74     @GuardedBy("mLock")
75     private final List<String> mUrlChain = new ArrayList<>();
76 
77     // The list of HTTP headers used by this request to establish a connection.
78     @GuardedBy("mLock")
79     private final ArrayList<Map.Entry<String, String>> mAllHeadersList = new ArrayList<>();
80 
81     // The exception that is thrown by the request. This is the same exception as the one in
82     // onFailed
83     @GuardedBy("mLock")
84     private CronetException mCronetException;
85 
86     // The current URL this request is connecting to.
87     @GuardedBy("mLock")
88     private String mCurrentUrl;
89 
90     // The {@link FakeUrlResponse} for the current URL.
91     @GuardedBy("mLock")
92     private FakeUrlResponse mCurrentFakeResponse;
93 
94     // The body of the request from UploadDataProvider.
95     @GuardedBy("mLock")
96     private byte[] mRequestBody;
97 
98     // The {@link UploadDataProvider} to retrieve a request body from.
99     @GuardedBy("mLock")
100     private UploadDataProvider mUploadDataProvider;
101 
102     // The executor to call the {@link UploadDataProvider}'s callback methods with.
103     @GuardedBy("mLock")
104     private Executor mUploadExecutor;
105 
106     // The {@link UploadDataSink} for the {@link UploadDataProvider}.
107     @GuardedBy("mLock")
108     @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
109     FakeDataSink mFakeDataSink;
110 
111     // The {@link UrlResponseInfo} for the current request.
112     @GuardedBy("mLock")
113     private UrlResponseInfo mUrlResponseInfo;
114 
115     // The response from the current request that needs to be sent.
116     @GuardedBy("mLock")
117     private ByteBuffer mResponse;
118 
119     // The HTTP method used by this request to establish a connection.
120     @GuardedBy("mLock")
121     private String mHttpMethod;
122 
123     // True after the {@link UploadDataProvider} for this request has been closed.
124     @GuardedBy("mLock")
125     private boolean mUploadProviderClosed;
126 
127     @GuardedBy("mLock")
128     @State
129     private int mState = State.NOT_STARTED;
130 
131     /**
132      * Holds a subset of StatusValues - {@link State#STARTED} can represent
133      * {@link Status#SENDING_REQUEST} or {@link Status#WAITING_FOR_RESPONSE}. While the distinction
134      * isn't needed to implement the logic in this class, it is needed to implement
135      * {@link #getStatus(StatusListener)}.
136      */
137     @StatusValues private volatile int mAdditionalStatusDetails = Status.INVALID;
138 
139     /** Used to map from HTTP status codes to the corresponding human-readable text. */
140     private static final Map<Integer, String> HTTP_STATUS_CODE_TO_TEXT;
141 
142     static {
143         Map<Integer, String> httpCodeMap = new HashMap<>();
144         httpCodeMap.put(100, "Continue");
145         httpCodeMap.put(101, "Switching Protocols");
146         httpCodeMap.put(102, "Processing");
147         httpCodeMap.put(103, "Early Hints");
148         httpCodeMap.put(200, "OK");
149         httpCodeMap.put(201, "Created");
150         httpCodeMap.put(202, "Accepted");
151         httpCodeMap.put(203, "Non-Authoritative Information");
152         httpCodeMap.put(204, "No Content");
153         httpCodeMap.put(205, "Reset Content");
154         httpCodeMap.put(206, "Partial Content");
155         httpCodeMap.put(207, "Multi-Status");
156         httpCodeMap.put(208, "Already Reported");
157         httpCodeMap.put(226, "IM Used");
158         httpCodeMap.put(300, "Multiple Choices");
159         httpCodeMap.put(301, "Moved Permanently");
160         httpCodeMap.put(302, "Found");
161         httpCodeMap.put(303, "See Other");
162         httpCodeMap.put(304, "Not Modified");
163         httpCodeMap.put(305, "Use Proxy");
164         httpCodeMap.put(306, "Unused");
165         httpCodeMap.put(307, "Temporary Redirect");
166         httpCodeMap.put(308, "Permanent Redirect");
167         httpCodeMap.put(400, "Bad Request");
168         httpCodeMap.put(401, "Unauthorized");
169         httpCodeMap.put(402, "Payment Required");
170         httpCodeMap.put(403, "Forbidden");
171         httpCodeMap.put(404, "Not Found");
172         httpCodeMap.put(405, "Method Not Allowed");
173         httpCodeMap.put(406, "Not Acceptable");
174         httpCodeMap.put(407, "Proxy Authentication Required");
175         httpCodeMap.put(408, "Request Timeout");
176         httpCodeMap.put(409, "Conflict");
177         httpCodeMap.put(410, "Gone");
178         httpCodeMap.put(411, "Length Required");
179         httpCodeMap.put(412, "Precondition Failed");
180         httpCodeMap.put(413, "Payload Too Large");
181         httpCodeMap.put(414, "URI Too Long");
182         httpCodeMap.put(415, "Unsupported Media Type");
183         httpCodeMap.put(416, "Range Not Satisfiable");
184         httpCodeMap.put(417, "Expectation Failed");
185         httpCodeMap.put(421, "Misdirected Request");
186         httpCodeMap.put(422, "Unprocessable Entity");
187         httpCodeMap.put(423, "Locked");
188         httpCodeMap.put(424, "Failed Dependency");
189         httpCodeMap.put(425, "Too Early");
190         httpCodeMap.put(426, "Upgrade Required");
191         httpCodeMap.put(428, "Precondition Required");
192         httpCodeMap.put(429, "Too Many Requests");
193         httpCodeMap.put(431, "Request Header Fields Too Large");
194         httpCodeMap.put(451, "Unavailable For Legal Reasons");
195         httpCodeMap.put(500, "Internal Server Error");
196         httpCodeMap.put(501, "Not Implemented");
197         httpCodeMap.put(502, "Bad Gateway");
198         httpCodeMap.put(503, "Service Unavailable");
199         httpCodeMap.put(504, "Gateway Timeout");
200         httpCodeMap.put(505, "HTTP Version Not Supported");
201         httpCodeMap.put(506, "Variant Also Negotiates");
202         httpCodeMap.put(507, "Insufficient Storage");
203         httpCodeMap.put(508, "Loop Denied");
204         httpCodeMap.put(510, "Not Extended");
205         httpCodeMap.put(511, "Network Authentication Required");
206         HTTP_STATUS_CODE_TO_TEXT = Collections.unmodifiableMap(httpCodeMap);
207     }
208 
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)209     FakeUrlRequest(
210             Callback callback,
211             Executor userExecutor,
212             Executor executor,
213             String url,
214             boolean allowDirectExecutor,
215             boolean trafficStatsTagSet,
216             int trafficStatsTag,
217             final boolean trafficStatsUidSet,
218             final int trafficStatsUid,
219             FakeCronetController fakeCronetController,
220             FakeCronetEngine fakeCronetEngine,
221             Collection<Object> requestAnnotations) {
222         if (url == null) {
223             throw new NullPointerException("URL is required");
224         }
225         if (callback == null) {
226             throw new NullPointerException("Listener is required");
227         }
228         if (executor == null) {
229             throw new NullPointerException("Executor is required");
230         }
231         mCallback = callback;
232         mUserExecutor =
233                 allowDirectExecutor ? userExecutor : new DirectPreventingExecutor(userExecutor);
234         mExecutor = executor;
235         mCurrentUrl = url;
236         mFakeCronetController = fakeCronetController;
237         mFakeCronetEngine = fakeCronetEngine;
238         mAllowDirectExecutor = allowDirectExecutor;
239         mRequestAnnotations = requestAnnotations;
240     }
241 
242     @Override
setUploadDataProvider(UploadDataProvider uploadDataProvider, Executor executor)243     public void setUploadDataProvider(UploadDataProvider uploadDataProvider, Executor executor) {
244         if (uploadDataProvider == null) {
245             throw new NullPointerException("Invalid UploadDataProvider.");
246         }
247         synchronized (mLock) {
248             if (!checkHasContentTypeHeader()) {
249                 throw new IllegalArgumentException(
250                         "Requests with upload data must have a Content-Type.");
251             }
252             checkNotStarted();
253             if (mHttpMethod == null) {
254                 mHttpMethod = "POST";
255             }
256             mUploadExecutor =
257                     mAllowDirectExecutor ? executor : new DirectPreventingExecutor(executor);
258             mUploadDataProvider = uploadDataProvider;
259         }
260     }
261 
262     @Override
setHttpMethod(String method)263     public void setHttpMethod(String method) {
264         synchronized (mLock) {
265             checkNotStarted();
266             if (method == null) {
267                 throw new NullPointerException("Method is required.");
268             }
269             if ("OPTIONS".equalsIgnoreCase(method)
270                     || "GET".equalsIgnoreCase(method)
271                     || "HEAD".equalsIgnoreCase(method)
272                     || "POST".equalsIgnoreCase(method)
273                     || "PUT".equalsIgnoreCase(method)
274                     || "DELETE".equalsIgnoreCase(method)
275                     || "TRACE".equalsIgnoreCase(method)
276                     || "PATCH".equalsIgnoreCase(method)) {
277                 mHttpMethod = method;
278             } else {
279                 throw new IllegalArgumentException("Invalid http method: " + method);
280             }
281         }
282     }
283 
284     @Override
addHeader(String header, String value)285     public void addHeader(String header, String value) {
286         synchronized (mLock) {
287             checkNotStarted();
288             mAllHeadersList.add(new AbstractMap.SimpleEntry<String, String>(header, value));
289         }
290     }
291 
292     /** Verifies that the request is not already started and throws an exception if it is. */
293     @GuardedBy("mLock")
checkNotStarted()294     private void checkNotStarted() {
295         if (mState != State.NOT_STARTED) {
296             throw new IllegalStateException("Request is already started. State is: " + mState);
297         }
298     }
299 
300     @Override
start()301     public void start() {
302         synchronized (mLock) {
303             if (mFakeCronetEngine.startRequest()) {
304                 boolean transitionedState = false;
305                 try {
306                     transitionStates(State.NOT_STARTED, State.STARTED);
307                     mAdditionalStatusDetails = Status.CONNECTING;
308                     transitionedState = true;
309                 } finally {
310                     if (!transitionedState) {
311                         cleanup();
312                         mFakeCronetEngine.onRequestFinished();
313                     }
314                 }
315                 mUrlChain.add(mCurrentUrl);
316                 if (mUploadDataProvider != null) {
317                     mFakeDataSink =
318                             new FakeDataSink(mUploadExecutor, mExecutor, mUploadDataProvider);
319                     mFakeDataSink.start(/* firstTime= */ true);
320                 } else {
321                     fakeConnect();
322                 }
323             } else {
324                 throw new IllegalStateException("This request's CronetEngine is already shutdown.");
325             }
326         }
327     }
328 
329     /**
330      * Fakes a request to a server by retrieving a response to this {@link UrlRequest} from the
331      * {@link FakeCronetController}.
332      */
333     @GuardedBy("mLock")
fakeConnect()334     private void fakeConnect() {
335         mAdditionalStatusDetails = Status.WAITING_FOR_RESPONSE;
336         mCurrentFakeResponse =
337                 mFakeCronetController.getResponse(
338                         mCurrentUrl, mHttpMethod, mAllHeadersList, mRequestBody);
339         int responseCode = mCurrentFakeResponse.getHttpStatusCode();
340         mUrlResponseInfo =
341                 new UrlResponseInfoImpl(
342                         Collections.unmodifiableList(new ArrayList<>(mUrlChain)),
343                         responseCode,
344                         getDescriptionByCode(responseCode),
345                         mCurrentFakeResponse.getAllHeadersList(),
346                         mCurrentFakeResponse.getWasCached(),
347                         mCurrentFakeResponse.getNegotiatedProtocol(),
348                         mCurrentFakeResponse.getProxyServer(),
349                         mCurrentFakeResponse.getResponseBody().length);
350         mResponse = ByteBuffer.wrap(mCurrentFakeResponse.getResponseBody());
351         // Check for a redirect.
352         if (responseCode >= 300 && responseCode < 400) {
353             processRedirectResponse();
354         } else {
355             closeUploadDataProvider();
356             final UrlResponseInfo info = mUrlResponseInfo;
357             transitionStates(State.STARTED, State.AWAITING_READ);
358             executeCheckedRunnable(
359                     () -> {
360                         mCallback.onResponseStarted(FakeUrlRequest.this, info);
361                     });
362         }
363     }
364 
365     /**
366      * Retrieves the redirect location from the response headers and responds to the
367      * {@link UrlRequest.Callback#onRedirectReceived} method. Adds the redirect URL to the chain.
368      *
369      * @param url the URL that the {@link FakeUrlResponse} redirected this request to
370      */
371     @GuardedBy("mLock")
processRedirectResponse()372     private void processRedirectResponse() {
373         transitionStates(State.STARTED, State.REDIRECT_RECEIVED);
374         if (mUrlResponseInfo.getAllHeaders().get("location") == null) {
375             // Response did not have a location header, so this request must fail.
376             final String prevUrl = mCurrentUrl;
377             mUserExecutor.execute(
378                     () -> {
379                         tryToFailWithException(
380                                 new CronetExceptionImpl(
381                                         "Request failed due to bad redirect HTTP headers",
382                                         new IllegalStateException(
383                                                 "Response recieved from URL: "
384                                                         + prevUrl
385                                                         + " was a redirect, but lacked a location header.")));
386                     });
387             return;
388         }
389         String pendingRedirectUrl =
390                 URI.create(mCurrentUrl)
391                         .resolve(mUrlResponseInfo.getAllHeaders().get("location").get(0))
392                         .toString();
393         mCurrentUrl = pendingRedirectUrl;
394         mUrlChain.add(mCurrentUrl);
395         transitionStates(State.REDIRECT_RECEIVED, State.AWAITING_FOLLOW_REDIRECT);
396         final UrlResponseInfo info = mUrlResponseInfo;
397         mExecutor.execute(
398                 () -> {
399                     executeCheckedRunnable(
400                             () -> {
401                                 mCallback.onRedirectReceived(
402                                         FakeUrlRequest.this, info, pendingRedirectUrl);
403                             });
404                 });
405     }
406 
407     @Override
read(ByteBuffer buffer)408     public void read(ByteBuffer buffer) {
409         // Entering {@link #State.READING} is somewhat redundant because the entire response is
410         // already acquired. We should still transition so that the fake {@link UrlRequest} follows
411         // the same state flow as a real request.
412         Preconditions.checkHasRemaining(buffer);
413         Preconditions.checkDirect(buffer);
414         synchronized (mLock) {
415             transitionStates(State.AWAITING_READ, State.READING);
416             final UrlResponseInfo info = mUrlResponseInfo;
417             if (mResponse.hasRemaining()) {
418                 transitionStates(State.READING, State.AWAITING_READ);
419                 fillBufferWithResponse(buffer);
420                 mExecutor.execute(
421                         () -> {
422                             executeCheckedRunnable(
423                                     () -> {
424                                         mCallback.onReadCompleted(
425                                                 FakeUrlRequest.this, info, buffer);
426                                     });
427                         });
428             } else {
429                 final RefCountDelegate inflightDoneCallbackCount = setTerminalState(State.COMPLETE);
430                 if (inflightDoneCallbackCount != null) {
431                     mUserExecutor.execute(
432                             () -> {
433                                 mCallback.onSucceeded(FakeUrlRequest.this, info);
434                                 inflightDoneCallbackCount.decrement();
435                             });
436                 }
437             }
438         }
439     }
440 
441     /**
442      * Puts as much of the remaining response as will fit into the {@link ByteBuffer} and removes
443      * that part of the string from the response left to send.
444      *
445      * @param buffer the {@link ByteBuffer} to put the response into
446      * @return the buffer with the response that we want to send back in it
447      */
448     @GuardedBy("mLock")
fillBufferWithResponse(ByteBuffer buffer)449     private void fillBufferWithResponse(ByteBuffer buffer) {
450         final int maxTransfer = Math.min(buffer.remaining(), mResponse.remaining());
451         ByteBuffer temp = mResponse.duplicate();
452         temp.limit(temp.position() + maxTransfer);
453         buffer.put(temp);
454         mResponse.position(mResponse.position() + maxTransfer);
455     }
456 
457     @Override
followRedirect()458     public void followRedirect() {
459         synchronized (mLock) {
460             transitionStates(State.AWAITING_FOLLOW_REDIRECT, State.STARTED);
461             if (mFakeDataSink != null) {
462                 mFakeDataSink = new FakeDataSink(mUploadExecutor, mExecutor, mUploadDataProvider);
463                 mFakeDataSink.start(/* firstTime= */ false);
464             } else {
465                 fakeConnect();
466             }
467         }
468     }
469 
470     @Override
cancel()471     public void cancel() {
472         synchronized (mLock) {
473             if (mState == State.NOT_STARTED || isDone()) {
474                 return;
475             }
476 
477             final UrlResponseInfo info = mUrlResponseInfo;
478             final RefCountDelegate inflightDoneCallbackCount = setTerminalState(State.CANCELLED);
479             if (inflightDoneCallbackCount != null) {
480                 mUserExecutor.execute(
481                         () -> {
482                             mCallback.onCanceled(FakeUrlRequest.this, info);
483                             inflightDoneCallbackCount.decrement();
484                         });
485             }
486         }
487     }
488 
489     @Override
getStatus(final StatusListener listener)490     public void getStatus(final StatusListener listener) {
491         synchronized (mLock) {
492             int extraStatus = mAdditionalStatusDetails;
493 
494             @StatusValues final int status;
495             switch (mState) {
496                 case State.ERROR:
497                 case State.COMPLETE:
498                 case State.CANCELLED:
499                 case State.NOT_STARTED:
500                     status = Status.INVALID;
501                     break;
502                 case State.STARTED:
503                     status = extraStatus;
504                     break;
505                 case State.REDIRECT_RECEIVED:
506                 case State.AWAITING_FOLLOW_REDIRECT:
507                 case State.AWAITING_READ:
508                     status = Status.IDLE;
509                     break;
510                 case State.READING:
511                     status = Status.READING_RESPONSE;
512                     break;
513                 default:
514                     throw new IllegalStateException("Switch is exhaustive: " + mState);
515             }
516             mUserExecutor.execute(
517                     new Runnable() {
518                         @Override
519                         public void run() {
520                             listener.onStatus(status);
521                         }
522                     });
523         }
524     }
525 
526     @Override
isDone()527     public boolean isDone() {
528         synchronized (mLock) {
529             return mState == State.COMPLETE || mState == State.ERROR || mState == State.CANCELLED;
530         }
531     }
532 
533     /**
534      * Swaps from the expected state to a new state. If the swap fails, and it's not
535      * due to an earlier error or cancellation, throws an exception.
536      */
537     @GuardedBy("mLock")
transitionStates(@tate int expected, @State int newState)538     private void transitionStates(@State int expected, @State int newState) {
539         if (mState == expected) {
540             mState = newState;
541         } else {
542             if (!(mState == State.CANCELLED || mState == State.ERROR)) {
543                 // TODO(crbug/1450573): Use Enums for state instead for better error messages.
544                 throw new IllegalStateException(
545                         "Invalid state transition - expected " + expected + " but was " + mState);
546             }
547         }
548     }
549 
550     /**
551      * Calls the callback's onFailed method if this request is not complete. Should be executed on
552      * the {@code mUserExecutor}, unless the error is a {@link InlineExecutionProhibitedException}
553      * produced by the {@code mUserExecutor}.
554      *
555      * @param e the {@link CronetException} that the request should pass to the callback.
556      *
557      */
tryToFailWithException(CronetException e)558     private void tryToFailWithException(CronetException e) {
559         synchronized (mLock) {
560             mCronetException = e;
561             final RefCountDelegate inflightDoneCallbackCount = setTerminalState(State.ERROR);
562             if (inflightDoneCallbackCount != null) {
563                 mCallback.onFailed(FakeUrlRequest.this, mUrlResponseInfo, e);
564                 inflightDoneCallbackCount.decrement();
565             }
566         }
567     }
568 
569     /**
570      * Execute a {@link CheckedRunnable} and call the {@link UrlRequest.Callback#onFailed} method
571      * if there is an exception and we can change to {@link State.ERROR}. Used to communicate with
572      * the {@link UrlRequest.Callback} methods using the executor provided by the constructor. This
573      * should be the last call in the critical section. If this is not the last call in a critical
574      * section, we risk modifying shared resources in a recursive call to another method
575      * guarded by the {@code mLock}. This is because in Java synchronized blocks are reentrant.
576      *
577      * @param checkedRunnable the runnable to execute
578      */
executeCheckedRunnable(JavaUrlRequestUtils.CheckedRunnable checkedRunnable)579     private void executeCheckedRunnable(JavaUrlRequestUtils.CheckedRunnable checkedRunnable) {
580         try {
581             mUserExecutor.execute(
582                     () -> {
583                         try {
584                             checkedRunnable.run();
585                         } catch (Exception e) {
586                             tryToFailWithException(
587                                     new CallbackExceptionImpl(
588                                             "Exception received from UrlRequest.Callback", e));
589                         }
590                     });
591         } catch (InlineExecutionProhibitedException e) {
592             // Don't try to fail using the {@code mUserExecutor} because it produced this error.
593             tryToFailWithException(
594                     new CronetExceptionImpl("Exception posting task to executor", e));
595         }
596     }
597 
598     /**
599      * Check the current state and if the request is started, but not complete, failed, or
600      * cancelled, change to the terminal state and call {@link FakeCronetEngine#onDestroyed}. This
601      * method ensures {@link FakeCronetEngine#onDestroyed} is only called once.
602      *
603      * @param terminalState the terminal state to set; one of {@link State.ERROR},
604      * {@link State.COMPLETE}, or {@link State.CANCELLED}
605      * @return a refcount to decrement after the terminal callback is called, or
606      * null if the terminal state wasn't set.
607      */
608     @GuardedBy("mLock")
setTerminalState(@tate int terminalState)609     private RefCountDelegate setTerminalState(@State int terminalState) {
610         switch (mState) {
611             case State.NOT_STARTED:
612                 throw new IllegalStateException("Can't enter terminal state before start");
613             case State.ERROR: // fallthrough
614             case State.COMPLETE: // fallthrough
615             case State.CANCELLED:
616                 return null; // Already in a terminal state
617             default:
618                 {
619                     mState = terminalState;
620                     final RefCountDelegate inflightDoneCallbackCount =
621                             new RefCountDelegate(mFakeCronetEngine::onRequestFinished);
622                     reportRequestFinished(inflightDoneCallbackCount);
623                     cleanup();
624                     return inflightDoneCallbackCount;
625                 }
626         }
627     }
628 
reportRequestFinished(RefCountDelegate inflightDoneCallbackCount)629     private void reportRequestFinished(RefCountDelegate inflightDoneCallbackCount) {
630         synchronized (mLock) {
631             mFakeCronetEngine.reportRequestFinished(
632                     new FakeRequestFinishedInfo(
633                             mCurrentUrl,
634                             mRequestAnnotations,
635                             getRequestFinishedReason(),
636                             mUrlResponseInfo,
637                             mCronetException),
638                     inflightDoneCallbackCount);
639         }
640     }
641 
642     @RequestFinishedInfoImpl.FinishedReason
643     @GuardedBy("mLock")
getRequestFinishedReason()644     private int getRequestFinishedReason() {
645         synchronized (mLock) {
646             switch (mState) {
647                 case State.COMPLETE:
648                     return RequestFinishedInfo.SUCCEEDED;
649                 case State.ERROR:
650                     return RequestFinishedInfo.FAILED;
651                 case State.CANCELLED:
652                     return RequestFinishedInfo.CANCELED;
653                 default:
654                     throw new IllegalStateException(
655                             "Request should be in terminal state before calling getRequestFinishedReason");
656             }
657         }
658     }
659 
660     @GuardedBy("mLock")
cleanup()661     private void cleanup() {
662         closeUploadDataProvider();
663         mFakeCronetEngine.onRequestDestroyed();
664     }
665 
666     /**
667      * Executed only once after the request has finished using the {@link UploadDataProvider}.
668      * Closes the {@link UploadDataProvider} if it exists and has not already been closed.
669      */
670     @GuardedBy("mLock")
closeUploadDataProvider()671     private void closeUploadDataProvider() {
672         if (mUploadDataProvider != null && !mUploadProviderClosed) {
673             try {
674                 mUploadExecutor.execute(
675                         uploadErrorSetting(
676                                 () -> {
677                                     synchronized (mLock) {
678                                         mUploadDataProvider.close();
679                                         mUploadProviderClosed = true;
680                                     }
681                                 }));
682             } catch (RejectedExecutionException e) {
683                 Log.e(TAG, "Exception when closing uploadDataProvider", e);
684             }
685         }
686     }
687 
688     /**
689      * Wraps a {@link CheckedRunnable} in a runnable that will attempt to fail the request if there
690      * is an exception.
691      *
692      * @param delegate the {@link CheckedRunnable} to try to run
693      * @return a {@link Runnable} that wraps the delegate runnable.
694      */
uploadErrorSetting(final CheckedRunnable delegate)695     private Runnable uploadErrorSetting(final CheckedRunnable delegate) {
696         return new Runnable() {
697             @Override
698             public void run() {
699                 try {
700                     delegate.run();
701                 } catch (Throwable t) {
702                     enterUploadErrorState(t);
703                 }
704             }
705         };
706     }
707 
708     /**
709      * Fails the request with an error. Called when uploading the request body using an
710      * {@link UploadDataProvider} fails.
711      *
712      * @param error the error that caused this request to fail which should be returned to the
713      *              {@link UrlRequest.Callback}
714      */
715     private void enterUploadErrorState(final Throwable error) {
716         synchronized (mLock) {
717             executeCheckedRunnable(
718                     () ->
719                             tryToFailWithException(
720                                     new CronetExceptionImpl(
721                                             "Exception received from UploadDataProvider", error)));
722         }
723     }
724 
725     /**
726      * Adapted from {@link JavaUrlRequest.OutputStreamDataSink}. Stores the received message in a
727      * {@link ByteArrayOutputStream} and transfers it to the {@code mRequestBody} when the response
728      * has been fully acquired.
729      */
730     @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
731     final class FakeDataSink extends JavaUploadDataSinkBase {
732         private final ByteArrayOutputStream mBodyStream = new ByteArrayOutputStream();
733         private final WritableByteChannel mBodyChannel = Channels.newChannel(mBodyStream);
734 
735         FakeDataSink(final Executor userExecutor, Executor executor, UploadDataProvider provider) {
736             super(userExecutor, executor, provider);
737         }
738 
739         @Override
740         public Runnable getErrorSettingRunnable(JavaUrlRequestUtils.CheckedRunnable runnable) {
741             return new Runnable() {
742                 @Override
743                 public void run() {
744                     try {
745                         runnable.run();
746                     } catch (Throwable t) {
747                         mUserExecutor.execute(
748                                 new Runnable() {
749                                     @Override
750                                     public void run() {
751                                         tryToFailWithException(
752                                                 new CronetExceptionImpl("System error", t));
753                                     }
754                                 });
755                     }
756                 }
757             };
758         }
759 
760         @Override
761         protected Runnable getUploadErrorSettingRunnable(
762                 JavaUrlRequestUtils.CheckedRunnable runnable) {
763             return uploadErrorSetting(runnable);
764         }
765 
766         @Override
767         protected void processUploadError(final Throwable error) {
768             enterUploadErrorState(error);
769         }
770 
771         @Override
772         protected int processSuccessfulRead(ByteBuffer buffer) throws IOException {
773             return mBodyChannel.write(buffer);
774         }
775 
776         /**
777          * Terminates the upload stage of the request. Writes the received bytes to the byte array:
778          * {@code mRequestBody}. Connects to the current URL for this request.
779          */
780         @Override
781         protected void finish() throws IOException {
782             synchronized (mLock) {
783                 mRequestBody = mBodyStream.toByteArray();
784                 fakeConnect();
785             }
786         }
787 
788         @Override
789         protected void initializeRead() throws IOException {
790             // Nothing to do before every read in this implementation.
791         }
792 
793         @Override
794         protected void initializeStart(long totalBytes) {
795             // Nothing to do to initialize the upload in this implementation.
796         }
797     }
798 
799     /**
800      * Verifies that the "content-type" header is present. Must be checked before an
801      * {@link UploadDataProvider} is premitted to be set.
802      *
803      * @return true if the "content-type" header is present in the request headers.
804      */
805     @GuardedBy("mLock")
806     private boolean checkHasContentTypeHeader() {
807         for (Map.Entry<String, String> entry : mAllHeadersList) {
808             if (entry.getKey().equalsIgnoreCase("content-type")) {
809                 return true;
810             }
811         }
812         return false;
813     }
814 
815     /**
816      * Gets a human readable description for a HTTP status code.
817      *
818      * @param code the code to retrieve the status for
819      * @return the HTTP status text as a string
820      */
821     private static String getDescriptionByCode(Integer code) {
822         return HTTP_STATUS_CODE_TO_TEXT.containsKey(code)
823                 ? HTTP_STATUS_CODE_TO_TEXT.get(code)
824                 : "Unassigned";
825     }
826 }
827