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