xref: /aosp_15_r20/external/cronet/components/cronet/android/fake/java/org/chromium/net/test/FakeUrlRequest.java (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
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"
386                                                         + " header.")));
387                     });
388             return;
389         }
390         String pendingRedirectUrl =
391                 URI.create(mCurrentUrl)
392                         .resolve(mUrlResponseInfo.getAllHeaders().get("location").get(0))
393                         .toString();
394         mCurrentUrl = pendingRedirectUrl;
395         mUrlChain.add(mCurrentUrl);
396         transitionStates(State.REDIRECT_RECEIVED, State.AWAITING_FOLLOW_REDIRECT);
397         final UrlResponseInfo info = mUrlResponseInfo;
398         mExecutor.execute(
399                 () -> {
400                     executeCheckedRunnable(
401                             () -> {
402                                 mCallback.onRedirectReceived(
403                                         FakeUrlRequest.this, info, pendingRedirectUrl);
404                             });
405                 });
406     }
407 
408     @Override
read(ByteBuffer buffer)409     public void read(ByteBuffer buffer) {
410         // Entering {@link #State.READING} is somewhat redundant because the entire response is
411         // already acquired. We should still transition so that the fake {@link UrlRequest} follows
412         // the same state flow as a real request.
413         Preconditions.checkHasRemaining(buffer);
414         Preconditions.checkDirect(buffer);
415         synchronized (mLock) {
416             transitionStates(State.AWAITING_READ, State.READING);
417             final UrlResponseInfo info = mUrlResponseInfo;
418             if (mResponse.hasRemaining()) {
419                 transitionStates(State.READING, State.AWAITING_READ);
420                 fillBufferWithResponse(buffer);
421                 mExecutor.execute(
422                         () -> {
423                             executeCheckedRunnable(
424                                     () -> {
425                                         mCallback.onReadCompleted(
426                                                 FakeUrlRequest.this, info, buffer);
427                                     });
428                         });
429             } else {
430                 final RefCountDelegate inflightDoneCallbackCount = setTerminalState(State.COMPLETE);
431                 if (inflightDoneCallbackCount != null) {
432                     mUserExecutor.execute(
433                             () -> {
434                                 mCallback.onSucceeded(FakeUrlRequest.this, info);
435                                 inflightDoneCallbackCount.decrement();
436                             });
437                 }
438             }
439         }
440     }
441 
442     /**
443      * Puts as much of the remaining response as will fit into the {@link ByteBuffer} and removes
444      * that part of the string from the response left to send.
445      *
446      * @param buffer the {@link ByteBuffer} to put the response into
447      * @return the buffer with the response that we want to send back in it
448      */
449     @GuardedBy("mLock")
fillBufferWithResponse(ByteBuffer buffer)450     private void fillBufferWithResponse(ByteBuffer buffer) {
451         final int maxTransfer = Math.min(buffer.remaining(), mResponse.remaining());
452         ByteBuffer temp = mResponse.duplicate();
453         temp.limit(temp.position() + maxTransfer);
454         buffer.put(temp);
455         mResponse.position(mResponse.position() + maxTransfer);
456     }
457 
458     @Override
followRedirect()459     public void followRedirect() {
460         synchronized (mLock) {
461             transitionStates(State.AWAITING_FOLLOW_REDIRECT, State.STARTED);
462             if (mFakeDataSink != null) {
463                 mFakeDataSink = new FakeDataSink(mUploadExecutor, mExecutor, mUploadDataProvider);
464                 mFakeDataSink.start(/* firstTime= */ false);
465             } else {
466                 fakeConnect();
467             }
468         }
469     }
470 
471     @Override
cancel()472     public void cancel() {
473         synchronized (mLock) {
474             if (mState == State.NOT_STARTED || isDone()) {
475                 return;
476             }
477 
478             final UrlResponseInfo info = mUrlResponseInfo;
479             final RefCountDelegate inflightDoneCallbackCount = setTerminalState(State.CANCELLED);
480             if (inflightDoneCallbackCount != null) {
481                 mUserExecutor.execute(
482                         () -> {
483                             mCallback.onCanceled(FakeUrlRequest.this, info);
484                             inflightDoneCallbackCount.decrement();
485                         });
486             }
487         }
488     }
489 
490     @Override
getStatus(final StatusListener listener)491     public void getStatus(final StatusListener listener) {
492         synchronized (mLock) {
493             int extraStatus = mAdditionalStatusDetails;
494 
495             @StatusValues final int status;
496             switch (mState) {
497                 case State.ERROR:
498                 case State.COMPLETE:
499                 case State.CANCELLED:
500                 case State.NOT_STARTED:
501                     status = Status.INVALID;
502                     break;
503                 case State.STARTED:
504                     status = extraStatus;
505                     break;
506                 case State.REDIRECT_RECEIVED:
507                 case State.AWAITING_FOLLOW_REDIRECT:
508                 case State.AWAITING_READ:
509                     status = Status.IDLE;
510                     break;
511                 case State.READING:
512                     status = Status.READING_RESPONSE;
513                     break;
514                 default:
515                     throw new IllegalStateException("Switch is exhaustive: " + mState);
516             }
517             mUserExecutor.execute(
518                     new Runnable() {
519                         @Override
520                         public void run() {
521                             listener.onStatus(status);
522                         }
523                     });
524         }
525     }
526 
527     @Override
isDone()528     public boolean isDone() {
529         synchronized (mLock) {
530             return mState == State.COMPLETE || mState == State.ERROR || mState == State.CANCELLED;
531         }
532     }
533 
534     /**
535      * Swaps from the expected state to a new state. If the swap fails, and it's not
536      * due to an earlier error or cancellation, throws an exception.
537      */
538     @GuardedBy("mLock")
transitionStates(@tate int expected, @State int newState)539     private void transitionStates(@State int expected, @State int newState) {
540         if (mState == expected) {
541             mState = newState;
542         } else {
543             if (!(mState == State.CANCELLED || mState == State.ERROR)) {
544                 // TODO(crbug.com/40915368): Use Enums for state instead for better error messages.
545                 throw new IllegalStateException(
546                         "Invalid state transition - expected " + expected + " but was " + mState);
547             }
548         }
549     }
550 
551     /**
552      * Calls the callback's onFailed method if this request is not complete. Should be executed on
553      * the {@code mUserExecutor}, unless the error is a {@link InlineExecutionProhibitedException}
554      * produced by the {@code mUserExecutor}.
555      *
556      * @param e the {@link CronetException} that the request should pass to the callback.
557      *
558      */
tryToFailWithException(CronetException e)559     private void tryToFailWithException(CronetException e) {
560         synchronized (mLock) {
561             mCronetException = e;
562             final RefCountDelegate inflightDoneCallbackCount = setTerminalState(State.ERROR);
563             if (inflightDoneCallbackCount != null) {
564                 mCallback.onFailed(FakeUrlRequest.this, mUrlResponseInfo, e);
565                 inflightDoneCallbackCount.decrement();
566             }
567         }
568     }
569 
570     /**
571      * Execute a {@link CheckedRunnable} and call the {@link UrlRequest.Callback#onFailed} method
572      * if there is an exception and we can change to {@link State.ERROR}. Used to communicate with
573      * the {@link UrlRequest.Callback} methods using the executor provided by the constructor. This
574      * should be the last call in the critical section. If this is not the last call in a critical
575      * section, we risk modifying shared resources in a recursive call to another method
576      * guarded by the {@code mLock}. This is because in Java synchronized blocks are reentrant.
577      *
578      * @param checkedRunnable the runnable to execute
579      */
executeCheckedRunnable(JavaUrlRequestUtils.CheckedRunnable checkedRunnable)580     private void executeCheckedRunnable(JavaUrlRequestUtils.CheckedRunnable checkedRunnable) {
581         try {
582             mUserExecutor.execute(
583                     () -> {
584                         try {
585                             checkedRunnable.run();
586                         } catch (Exception e) {
587                             tryToFailWithException(
588                                     new CallbackExceptionImpl(
589                                             "Exception received from UrlRequest.Callback", e));
590                         }
591                     });
592         } catch (InlineExecutionProhibitedException e) {
593             // Don't try to fail using the {@code mUserExecutor} because it produced this error.
594             tryToFailWithException(
595                     new CronetExceptionImpl("Exception posting task to executor", e));
596         }
597     }
598 
599     /**
600      * Check the current state and if the request is started, but not complete, failed, or
601      * cancelled, change to the terminal state and call {@link FakeCronetEngine#onDestroyed}. This
602      * method ensures {@link FakeCronetEngine#onDestroyed} is only called once.
603      *
604      * @param terminalState the terminal state to set; one of {@link State.ERROR},
605      * {@link State.COMPLETE}, or {@link State.CANCELLED}
606      * @return a refcount to decrement after the terminal callback is called, or
607      * null if the terminal state wasn't set.
608      */
609     @GuardedBy("mLock")
setTerminalState(@tate int terminalState)610     private RefCountDelegate setTerminalState(@State int terminalState) {
611         switch (mState) {
612             case State.NOT_STARTED:
613                 throw new IllegalStateException("Can't enter terminal state before start");
614             case State.ERROR: // fallthrough
615             case State.COMPLETE: // fallthrough
616             case State.CANCELLED:
617                 return null; // Already in a terminal state
618             default:
619                 {
620                     mState = terminalState;
621                     final RefCountDelegate inflightDoneCallbackCount =
622                             new RefCountDelegate(mFakeCronetEngine::onRequestFinished);
623                     reportRequestFinished(inflightDoneCallbackCount);
624                     cleanup();
625                     return inflightDoneCallbackCount;
626                 }
627         }
628     }
629 
reportRequestFinished(RefCountDelegate inflightDoneCallbackCount)630     private void reportRequestFinished(RefCountDelegate inflightDoneCallbackCount) {
631         synchronized (mLock) {
632             mFakeCronetEngine.reportRequestFinished(
633                     new FakeRequestFinishedInfo(
634                             mCurrentUrl,
635                             mRequestAnnotations,
636                             getRequestFinishedReason(),
637                             mUrlResponseInfo,
638                             mCronetException),
639                     inflightDoneCallbackCount);
640         }
641     }
642 
643     @RequestFinishedInfoImpl.FinishedReason
644     @GuardedBy("mLock")
getRequestFinishedReason()645     private int getRequestFinishedReason() {
646         synchronized (mLock) {
647             switch (mState) {
648                 case State.COMPLETE:
649                     return RequestFinishedInfo.SUCCEEDED;
650                 case State.ERROR:
651                     return RequestFinishedInfo.FAILED;
652                 case State.CANCELLED:
653                     return RequestFinishedInfo.CANCELED;
654                 default:
655                     throw new IllegalStateException(
656                             "Request should be in terminal state before calling"
657                                 + " getRequestFinishedReason");
658             }
659         }
660     }
661 
662     @GuardedBy("mLock")
cleanup()663     private void cleanup() {
664         closeUploadDataProvider();
665         mFakeCronetEngine.onRequestDestroyed();
666     }
667 
668     /**
669      * Executed only once after the request has finished using the {@link UploadDataProvider}.
670      * Closes the {@link UploadDataProvider} if it exists and has not already been closed.
671      */
672     @GuardedBy("mLock")
closeUploadDataProvider()673     private void closeUploadDataProvider() {
674         if (mUploadDataProvider != null && !mUploadProviderClosed) {
675             try {
676                 mUploadExecutor.execute(
677                         uploadErrorSetting(
678                                 () -> {
679                                     synchronized (mLock) {
680                                         mUploadDataProvider.close();
681                                         mUploadProviderClosed = true;
682                                     }
683                                 }));
684             } catch (RejectedExecutionException e) {
685                 Log.e(TAG, "Exception when closing uploadDataProvider", e);
686             }
687         }
688     }
689 
690     /**
691      * Wraps a {@link CheckedRunnable} in a runnable that will attempt to fail the request if there
692      * is an exception.
693      *
694      * @param delegate the {@link CheckedRunnable} to try to run
695      * @return a {@link Runnable} that wraps the delegate runnable.
696      */
uploadErrorSetting(final CheckedRunnable delegate)697     private Runnable uploadErrorSetting(final CheckedRunnable delegate) {
698         return new Runnable() {
699             @Override
700             public void run() {
701                 try {
702                     delegate.run();
703                 } catch (Throwable t) {
704                     enterUploadErrorState(t);
705                 }
706             }
707         };
708     }
709 
710     /**
711      * Fails the request with an error. Called when uploading the request body using an
712      * {@link UploadDataProvider} fails.
713      *
714      * @param error the error that caused this request to fail which should be returned to the
715      *              {@link UrlRequest.Callback}
716      */
717     private void enterUploadErrorState(final Throwable error) {
718         synchronized (mLock) {
719             executeCheckedRunnable(
720                     () ->
721                             tryToFailWithException(
722                                     new CronetExceptionImpl(
723                                             "Exception received from UploadDataProvider", error)));
724         }
725     }
726 
727     /**
728      * Adapted from {@link JavaUrlRequest.OutputStreamDataSink}. Stores the received message in a
729      * {@link ByteArrayOutputStream} and transfers it to the {@code mRequestBody} when the response
730      * has been fully acquired.
731      */
732     @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
733     final class FakeDataSink extends JavaUploadDataSinkBase {
734         private final ByteArrayOutputStream mBodyStream = new ByteArrayOutputStream();
735         private final WritableByteChannel mBodyChannel = Channels.newChannel(mBodyStream);
736 
737         FakeDataSink(final Executor userExecutor, Executor executor, UploadDataProvider provider) {
738             super(userExecutor, executor, provider);
739         }
740 
741         @Override
742         public Runnable getErrorSettingRunnable(JavaUrlRequestUtils.CheckedRunnable runnable) {
743             return new Runnable() {
744                 @Override
745                 public void run() {
746                     try {
747                         runnable.run();
748                     } catch (Throwable t) {
749                         mUserExecutor.execute(
750                                 new Runnable() {
751                                     @Override
752                                     public void run() {
753                                         tryToFailWithException(
754                                                 new CronetExceptionImpl("System error", t));
755                                     }
756                                 });
757                     }
758                 }
759             };
760         }
761 
762         @Override
763         protected Runnable getUploadErrorSettingRunnable(
764                 JavaUrlRequestUtils.CheckedRunnable runnable) {
765             return uploadErrorSetting(runnable);
766         }
767 
768         @Override
769         protected void processUploadError(final Throwable error) {
770             enterUploadErrorState(error);
771         }
772 
773         @Override
774         protected int processSuccessfulRead(ByteBuffer buffer) throws IOException {
775             return mBodyChannel.write(buffer);
776         }
777 
778         /**
779          * Terminates the upload stage of the request. Writes the received bytes to the byte array:
780          * {@code mRequestBody}. Connects to the current URL for this request.
781          */
782         @Override
783         protected void finish() throws IOException {
784             synchronized (mLock) {
785                 mRequestBody = mBodyStream.toByteArray();
786                 fakeConnect();
787             }
788         }
789 
790         @Override
791         protected void initializeRead() throws IOException {
792             // Nothing to do before every read in this implementation.
793         }
794 
795         @Override
796         protected void initializeStart(long totalBytes) {
797             // Nothing to do to initialize the upload in this implementation.
798         }
799     }
800 
801     /**
802      * Verifies that the "content-type" header is present. Must be checked before an
803      * {@link UploadDataProvider} is premitted to be set.
804      *
805      * @return true if the "content-type" header is present in the request headers.
806      */
807     @GuardedBy("mLock")
808     private boolean checkHasContentTypeHeader() {
809         for (Map.Entry<String, String> entry : mAllHeadersList) {
810             if (entry.getKey().equalsIgnoreCase("content-type")) {
811                 return true;
812             }
813         }
814         return false;
815     }
816 
817     /**
818      * Gets a human readable description for a HTTP status code.
819      *
820      * @param code the code to retrieve the status for
821      * @return the HTTP status text as a string
822      */
823     private static String getDescriptionByCode(Integer code) {
824         return HTTP_STATUS_CODE_TO_TEXT.containsKey(code)
825                 ? HTTP_STATUS_CODE_TO_TEXT.get(code)
826                 : "Unassigned";
827     }
828 }
829