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