OLD | NEW |
(Empty) | |
| 1 // Copyright 2015 The Chromium Authors. All rights reserved. |
| 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; |
| 6 |
| 7 import android.annotation.TargetApi; |
| 8 import android.net.TrafficStats; |
| 9 import android.os.Build; |
| 10 import android.util.Log; |
| 11 |
| 12 import java.io.Closeable; |
| 13 import java.io.IOException; |
| 14 import java.net.HttpURLConnection; |
| 15 import java.net.URI; |
| 16 import java.net.URL; |
| 17 import java.nio.ByteBuffer; |
| 18 import java.nio.channels.Channels; |
| 19 import java.nio.channels.ReadableByteChannel; |
| 20 import java.nio.channels.WritableByteChannel; |
| 21 import java.util.AbstractMap.SimpleEntry; |
| 22 import java.util.ArrayList; |
| 23 import java.util.Collections; |
| 24 import java.util.List; |
| 25 import java.util.Map; |
| 26 import java.util.TreeMap; |
| 27 import java.util.concurrent.Executor; |
| 28 import java.util.concurrent.RejectedExecutionException; |
| 29 import java.util.concurrent.atomic.AtomicReference; |
| 30 |
| 31 /** |
| 32 * Pure java UrlRequest, backed by {@link HttpURLConnection}. |
| 33 */ |
| 34 @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) // TrafficStats only avai
lable on ICS |
| 35 final class JavaUrlRequest implements UrlRequest { |
| 36 private static final String X_ANDROID_SELECTED_TRANSPORT = "X-Android-Select
ed-Transport"; |
| 37 private static final String TAG = "JavaUrlConnection"; |
| 38 private static final int DEFAULT_UPLOAD_BUFFER_SIZE = 8192; |
| 39 private static final int DEFAULT_CHUNK_LENGTH = DEFAULT_UPLOAD_BUFFER_SIZE; |
| 40 private static final String USER_AGENT = "User-Agent"; |
| 41 private final AsyncUrlRequestCallback mCallbackAsync; |
| 42 private final Executor mExecutor; |
| 43 private final String mUserAgent; |
| 44 private final Map<String, String> mRequestHeaders = |
| 45 new TreeMap<>(String.CASE_INSENSITIVE_ORDER); |
| 46 private final List<String> mUrlChain = new ArrayList<>(); |
| 47 /** |
| 48 * This is the source of thread safety in this class - no other synchronizat
ion is performed. |
| 49 * By compare-and-swapping from one state to another, we guarantee that oper
ations aren't |
| 50 * running concurrently. Only the winner of a CAS proceeds. |
| 51 * |
| 52 * <p>A caller can lose a CAS for three reasons - user error (two calls to r
ead() without |
| 53 * waiting for the read to succeed), runtime error (network code or user cod
e throws an |
| 54 * exception), or cancellation. |
| 55 */ |
| 56 private final AtomicReference<State> mState = new AtomicReference<>(State.NO
T_STARTED); |
| 57 |
| 58 /** |
| 59 * Traffic stats tag to associate this requests' data use with. It's capture
d when the request |
| 60 * is created, so that applications doing work on behalf of another app can
correctly attribute |
| 61 * that data use. |
| 62 */ |
| 63 private final int mTrafficStatsTag; |
| 64 |
| 65 /* These don't change with redirects */ |
| 66 private String mInitialMethod; |
| 67 private UploadDataProvider mUploadDataProvider; |
| 68 private Executor mUploadExecutor; |
| 69 /** |
| 70 * Holds a subset of StatusValues - {@link State#STARTED} can represent |
| 71 * {@link Status#SENDING_REQUEST} or {@link Status#WAITING_FOR_RESPONSE}. Wh
ile the distinction |
| 72 * isn't needed to implement the logic in this class, it is needed to implem
ent |
| 73 * {@link #getStatus(StatusListener)}. |
| 74 * |
| 75 * <p>Concurrency notes - this value is not atomically updated with mState,
so there is some |
| 76 * risk that we'd get an inconsistent snapshot of both - however, it also ha
ppens that this |
| 77 * value is only used with the STARTED state, so it's inconsequential. |
| 78 */ |
| 79 @Status.StatusValues private volatile int mAdditionalStatusDetails = Status.
INVALID; |
| 80 |
| 81 /* These change with redirects. */ |
| 82 private String mCurrentUrl; |
| 83 private ReadableByteChannel mResponseChannel; |
| 84 private UrlResponseInfo mUrlResponseInfo; |
| 85 private String mPendingRedirectUrl; |
| 86 /** |
| 87 * The happens-before edges created by the executor submission and AtomicRef
erence setting are |
| 88 * sufficient to guarantee the correct behavior of this field; however, this
is an |
| 89 * AtomicReference so that we can cleanly dispose of a new connection if we'
re cancelled during |
| 90 * a redirect, which requires get-and-set semantics. |
| 91 * */ |
| 92 private final AtomicReference<HttpURLConnection> mCurrentUrlConnection = |
| 93 new AtomicReference<>(); |
| 94 |
| 95 /** |
| 96 * /- AWAITING_FOLLOW_REDIRECT <- REDIRECT_RECEIVED <-\ /-
READING <--\ |
| 97 * | | |
| |
| 98 * \ / \
/ |
| 99 * NOT_STARTED ---> STARTED ----> AW
AITING_READ ---> |
| 100 * COMPLETE |
| 101 */ |
| 102 private enum State { |
| 103 NOT_STARTED, |
| 104 STARTED, |
| 105 REDIRECT_RECEIVED, |
| 106 AWAITING_FOLLOW_REDIRECT, |
| 107 AWAITING_READ, |
| 108 READING, |
| 109 ERROR, |
| 110 COMPLETE, |
| 111 CANCELLED, |
| 112 } |
| 113 |
| 114 /** |
| 115 * @param executor The executor used for reading and writing from sockets |
| 116 * @param userExecutor The executor used to dispatch to {@code callback} |
| 117 */ |
| 118 JavaUrlRequest(Callback callback, final Executor executor, Executor userExec
utor, String url, |
| 119 String userAgent) { |
| 120 if (url == null) { |
| 121 throw new NullPointerException("URL is required"); |
| 122 } |
| 123 if (callback == null) { |
| 124 throw new NullPointerException("Listener is required"); |
| 125 } |
| 126 if (executor == null) { |
| 127 throw new NullPointerException("Executor is required"); |
| 128 } |
| 129 if (userExecutor == null) { |
| 130 throw new NullPointerException("Executor is required"); |
| 131 } |
| 132 this.mCallbackAsync = new AsyncUrlRequestCallback(callback, userExecutor
); |
| 133 this.mTrafficStatsTag = TrafficStats.getThreadStatsTag(); |
| 134 this.mExecutor = new Executor() { |
| 135 @Override |
| 136 public void execute(final Runnable command) { |
| 137 executor.execute(new Runnable() { |
| 138 @Override |
| 139 public void run() { |
| 140 int oldTag = TrafficStats.getThreadStatsTag(); |
| 141 TrafficStats.setThreadStatsTag(mTrafficStatsTag); |
| 142 try { |
| 143 command.run(); |
| 144 } finally { |
| 145 TrafficStats.setThreadStatsTag(oldTag); |
| 146 } |
| 147 } |
| 148 }); |
| 149 } |
| 150 }; |
| 151 this.mCurrentUrl = url; |
| 152 this.mUserAgent = userAgent; |
| 153 } |
| 154 |
| 155 @Override |
| 156 public void setHttpMethod(String method) { |
| 157 checkNotStarted(); |
| 158 if (method == null) { |
| 159 throw new NullPointerException("Method is required."); |
| 160 } |
| 161 if ("OPTIONS".equalsIgnoreCase(method) || "GET".equalsIgnoreCase(method) |
| 162 || "HEAD".equalsIgnoreCase(method) || "POST".equalsIgnoreCase(me
thod) |
| 163 || "PUT".equalsIgnoreCase(method) || "DELETE".equalsIgnoreCase(m
ethod) |
| 164 || "TRACE".equalsIgnoreCase(method) || "PATCH".equalsIgnoreCase(
method)) { |
| 165 mInitialMethod = method; |
| 166 } else { |
| 167 throw new IllegalArgumentException("Invalid http method " + method); |
| 168 } |
| 169 } |
| 170 |
| 171 private void checkNotStarted() { |
| 172 State state = mState.get(); |
| 173 if (state != State.NOT_STARTED) { |
| 174 throw new IllegalStateException("Request is already started. State i
s: " + state); |
| 175 } |
| 176 } |
| 177 |
| 178 @Override |
| 179 public void addHeader(String header, String value) { |
| 180 checkNotStarted(); |
| 181 if (!isValidHeaderName(header) || value.contains("\r\n")) { |
| 182 throw new IllegalArgumentException("Invalid header " + header + "="
+ value); |
| 183 } |
| 184 if (mRequestHeaders.containsKey(header)) { |
| 185 mRequestHeaders.remove(header); |
| 186 } |
| 187 mRequestHeaders.put(header, value); |
| 188 } |
| 189 |
| 190 private boolean isValidHeaderName(String header) { |
| 191 for (int i = 0; i < header.length(); i++) { |
| 192 char c = header.charAt(i); |
| 193 switch (c) { |
| 194 case '(': |
| 195 case ')': |
| 196 case '<': |
| 197 case '>': |
| 198 case '@': |
| 199 case ',': |
| 200 case ';': |
| 201 case ':': |
| 202 case '\\': |
| 203 case '\'': |
| 204 case '/': |
| 205 case '[': |
| 206 case ']': |
| 207 case '?': |
| 208 case '=': |
| 209 case '{': |
| 210 case '}': |
| 211 return false; |
| 212 default: { |
| 213 if (Character.isISOControl(c) || Character.isWhitespace(c))
{ |
| 214 return false; |
| 215 } |
| 216 } |
| 217 } |
| 218 } |
| 219 return true; |
| 220 } |
| 221 |
| 222 @Override |
| 223 public void setUploadDataProvider(UploadDataProvider uploadDataProvider, Exe
cutor executor) { |
| 224 if (uploadDataProvider == null) { |
| 225 throw new NullPointerException("Invalid UploadDataProvider."); |
| 226 } |
| 227 if (!mRequestHeaders.containsKey("Content-Type")) { |
| 228 throw new IllegalArgumentException( |
| 229 "Requests with upload data must have a Content-Type."); |
| 230 } |
| 231 checkNotStarted(); |
| 232 if (mInitialMethod == null) { |
| 233 mInitialMethod = "POST"; |
| 234 } |
| 235 this.mUploadDataProvider = uploadDataProvider; |
| 236 this.mUploadExecutor = executor; |
| 237 } |
| 238 |
| 239 private enum SinkState { |
| 240 AWAITING_READ_RESULT, |
| 241 AWAITING_REWIND_RESULT, |
| 242 UPLOADING, |
| 243 NOT_STARTED, |
| 244 } |
| 245 |
| 246 private final class OutputStreamDataSink implements UploadDataSink { |
| 247 final AtomicReference<SinkState> mSinkState = new AtomicReference<>(Sink
State.NOT_STARTED); |
| 248 final Executor mUserExecutor; |
| 249 final Executor mExecutor; |
| 250 final HttpURLConnection mUrlConnection; |
| 251 WritableByteChannel mOutputChannel; |
| 252 final UploadDataProvider mUploadProvider; |
| 253 ByteBuffer mBuffer; |
| 254 /** This holds the total bytes to send (the content-length). -1 if unkno
wn. */ |
| 255 long mTotalBytes; |
| 256 /** This holds the bytes written so far */ |
| 257 long mWrittenBytes = 0; |
| 258 |
| 259 OutputStreamDataSink(final Executor userExecutor, Executor executor, |
| 260 HttpURLConnection urlConnection, UploadDataProvider provider) { |
| 261 this.mUserExecutor = new Executor() { |
| 262 @Override |
| 263 public void execute(Runnable runnable) { |
| 264 try { |
| 265 userExecutor.execute(runnable); |
| 266 } catch (RejectedExecutionException e) { |
| 267 enterUploadErrorState(e); |
| 268 } |
| 269 } |
| 270 }; |
| 271 this.mExecutor = executor; |
| 272 this.mUrlConnection = urlConnection; |
| 273 this.mUploadProvider = provider; |
| 274 } |
| 275 |
| 276 @Override |
| 277 public void onReadSucceeded(final boolean finalChunk) { |
| 278 if (!mSinkState.compareAndSet(SinkState.AWAITING_READ_RESULT, SinkSt
ate.UPLOADING)) { |
| 279 throw new IllegalStateException( |
| 280 "Not expecting a read result, expecting: " + mSinkState.
get()); |
| 281 } |
| 282 mExecutor.execute(errorSetting(State.STARTED, new CheckedRunnable()
{ |
| 283 @Override |
| 284 public void run() throws Exception { |
| 285 mBuffer.flip(); |
| 286 while (mBuffer.hasRemaining()) { |
| 287 mWrittenBytes += mOutputChannel.write(mBuffer); |
| 288 } |
| 289 if (mWrittenBytes < mTotalBytes || (mTotalBytes == -1 && !fi
nalChunk)) { |
| 290 mBuffer.clear(); |
| 291 mSinkState.set(SinkState.AWAITING_READ_RESULT); |
| 292 mUserExecutor.execute(uploadErrorSetting(new CheckedRunn
able() { |
| 293 @Override |
| 294 public void run() throws Exception { |
| 295 mUploadProvider.read(OutputStreamDataSink.this,
mBuffer); |
| 296 } |
| 297 })); |
| 298 } else if (mTotalBytes == -1) { |
| 299 finish(); |
| 300 } else if (mTotalBytes == mWrittenBytes) { |
| 301 finish(); |
| 302 } else { |
| 303 throw new IllegalStateException("Wrote more bytes than w
ere available"); |
| 304 } |
| 305 } |
| 306 })); |
| 307 } |
| 308 |
| 309 @Override |
| 310 public void onRewindSucceeded() { |
| 311 if (!mSinkState.compareAndSet(SinkState.AWAITING_REWIND_RESULT, Sink
State.UPLOADING)) { |
| 312 throw new IllegalStateException("Not expecting a read result"); |
| 313 } |
| 314 startRead(); |
| 315 } |
| 316 |
| 317 @Override |
| 318 public void onReadError(Exception exception) { |
| 319 enterUploadErrorState(exception); |
| 320 } |
| 321 |
| 322 @Override |
| 323 public void onRewindError(Exception exception) { |
| 324 enterUploadErrorState(exception); |
| 325 } |
| 326 |
| 327 void startRead() { |
| 328 mUserExecutor.execute(uploadErrorSetting(new CheckedRunnable() { |
| 329 @Override |
| 330 public void run() throws Exception { |
| 331 if (mOutputChannel == null) { |
| 332 mAdditionalStatusDetails = Status.CONNECTING; |
| 333 mUrlConnection.connect(); |
| 334 mAdditionalStatusDetails = Status.SENDING_REQUEST; |
| 335 mOutputChannel = Channels.newChannel(mUrlConnection.getO
utputStream()); |
| 336 } |
| 337 mSinkState.set(SinkState.AWAITING_READ_RESULT); |
| 338 mUploadProvider.read(OutputStreamDataSink.this, mBuffer); |
| 339 } |
| 340 })); |
| 341 } |
| 342 |
| 343 void finish() throws IOException { |
| 344 if (mOutputChannel != null) { |
| 345 mOutputChannel.close(); |
| 346 } |
| 347 mAdditionalStatusDetails = Status.WAITING_FOR_RESPONSE; |
| 348 fireGetHeaders(); |
| 349 } |
| 350 |
| 351 void start(final boolean firstTime) { |
| 352 mUserExecutor.execute(uploadErrorSetting(new CheckedRunnable() { |
| 353 @Override |
| 354 public void run() throws Exception { |
| 355 mTotalBytes = mUploadProvider.getLength(); |
| 356 if (mTotalBytes == 0) { |
| 357 finish(); |
| 358 } else { |
| 359 // If we know how much data we have to upload, and it's
small, we can save |
| 360 // memory by allocating a reasonably sized buffer to rea
d into. |
| 361 if (mTotalBytes > 0 && mTotalBytes < DEFAULT_UPLOAD_BUFF
ER_SIZE) { |
| 362 // Allocate one byte more than necessary, to detect
callers uploading |
| 363 // more bytes than they specified in length. |
| 364 mBuffer = ByteBuffer.allocateDirect((int) mTotalByte
s + 1); |
| 365 } else { |
| 366 mBuffer = ByteBuffer.allocateDirect(DEFAULT_UPLOAD_B
UFFER_SIZE); |
| 367 } |
| 368 |
| 369 if (mTotalBytes > 0 && mTotalBytes <= Integer.MAX_VALUE)
{ |
| 370 mUrlConnection.setFixedLengthStreamingMode((int) mTo
talBytes); |
| 371 } else if (mTotalBytes > Integer.MAX_VALUE |
| 372 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.
KITKAT) { |
| 373 mUrlConnection.setFixedLengthStreamingMode(mTotalByt
es); |
| 374 } else { |
| 375 // If we know the length, but we're running pre-kitk
at and it's larger |
| 376 // than an int can hold, we have to use chunked - ot
herwise we'll end up |
| 377 // buffering the whole response in memory. |
| 378 mUrlConnection.setChunkedStreamingMode(DEFAULT_CHUNK
_LENGTH); |
| 379 } |
| 380 if (firstTime) { |
| 381 startRead(); |
| 382 } else { |
| 383 mSinkState.set(SinkState.AWAITING_REWIND_RESULT); |
| 384 mUploadProvider.rewind(OutputStreamDataSink.this); |
| 385 } |
| 386 } |
| 387 } |
| 388 })); |
| 389 } |
| 390 } |
| 391 |
| 392 @Override |
| 393 public void start() { |
| 394 mAdditionalStatusDetails = Status.CONNECTING; |
| 395 transitionStates(State.NOT_STARTED, State.STARTED, new Runnable() { |
| 396 @Override |
| 397 public void run() { |
| 398 mUrlChain.add(mCurrentUrl); |
| 399 fireOpenConnection(); |
| 400 } |
| 401 }); |
| 402 } |
| 403 |
| 404 private void enterErrorState(State previousState, final UrlRequestException
error) { |
| 405 if (mState.compareAndSet(previousState, State.ERROR)) { |
| 406 fireDisconnect(); |
| 407 mCallbackAsync.onFailed(mUrlResponseInfo, error); |
| 408 } |
| 409 } |
| 410 |
| 411 /** Ends the reqeust with an error, caused by an exception thrown from user
code. */ |
| 412 private void enterUserErrorState(State previousState, final Throwable error)
{ |
| 413 enterErrorState(previousState, new UrlRequestException("User Error", err
or)); |
| 414 } |
| 415 |
| 416 /** Ends the requst with an error, caused by an exception thrown from user c
ode. */ |
| 417 private void enterUploadErrorState(final Throwable error) { |
| 418 enterErrorState(State.STARTED, |
| 419 new UrlRequestException("Exception received from UploadDataProvi
der", error)); |
| 420 } |
| 421 |
| 422 private void enterCronetErrorState(State previousState, final Throwable erro
r) { |
| 423 // TODO(clm) mapping from Java exception (UnknownHostException, for exam
ple) to net error |
| 424 // code goes here. |
| 425 enterErrorState(previousState, new UrlRequestException("System error", e
rror)); |
| 426 } |
| 427 |
| 428 /** |
| 429 * Atomically swaps from the expected state to a new state. If the swap fail
s, and it's not |
| 430 * due to an earlier error or cancellation, throws an exception. |
| 431 * |
| 432 * @param afterTransition Callback to run after transition completes success
fully. |
| 433 */ |
| 434 private void transitionStates(State expected, State newState, Runnable after
Transition) { |
| 435 if (!mState.compareAndSet(expected, newState)) { |
| 436 State state = mState.get(); |
| 437 if (!(state == State.CANCELLED || state == State.ERROR)) { |
| 438 throw new IllegalStateException( |
| 439 "Invalid state transition - expected " + expected + " bu
t was " + state); |
| 440 } |
| 441 } else { |
| 442 afterTransition.run(); |
| 443 } |
| 444 } |
| 445 |
| 446 @Override |
| 447 public void followRedirect() { |
| 448 transitionStates(State.AWAITING_FOLLOW_REDIRECT, State.STARTED, new Runn
able() { |
| 449 @Override |
| 450 public void run() { |
| 451 mCurrentUrl = mPendingRedirectUrl; |
| 452 mPendingRedirectUrl = null; |
| 453 fireOpenConnection(); |
| 454 } |
| 455 }); |
| 456 } |
| 457 |
| 458 private void fireGetHeaders() { |
| 459 mExecutor.execute(errorSetting(State.STARTED, new CheckedRunnable() { |
| 460 @Override |
| 461 public void run() throws Exception { |
| 462 HttpURLConnection connection = mCurrentUrlConnection.get(); |
| 463 if (connection == null) { |
| 464 return; // We've been cancelled |
| 465 } |
| 466 final List<Map.Entry<String, String>> headerList = new ArrayList
<>(); |
| 467 String selectedTransport = "http/1.1"; |
| 468 String headerKey; |
| 469 for (int i = 0; (headerKey = connection.getHeaderFieldKey(i)) !=
null; i++) { |
| 470 if (X_ANDROID_SELECTED_TRANSPORT.equalsIgnoreCase(headerKey)
) { |
| 471 selectedTransport = connection.getHeaderField(i); |
| 472 } |
| 473 if (!headerKey.startsWith("X-Android")) { |
| 474 headerList.add(new SimpleEntry<>(headerKey, connection.g
etHeaderField(i))); |
| 475 } |
| 476 } |
| 477 |
| 478 int responseCode = connection.getResponseCode(); |
| 479 // Important to copy the list here, because although we never co
ncurrently modify |
| 480 // the list ourselves, user code might iterate over it while we'
re redirecting, and |
| 481 // that would throw ConcurrentModificationException. |
| 482 mUrlResponseInfo = new UrlResponseInfo(new ArrayList<>(mUrlChain
), responseCode, |
| 483 connection.getResponseMessage(), Collections.unmodifiabl
eList(headerList), |
| 484 false, selectedTransport, ""); |
| 485 // TODO(clm) actual redirect handling? post -> get and whatnot? |
| 486 if (responseCode >= 300 && responseCode < 400) { |
| 487 fireRedirectReceived(mUrlResponseInfo.getAllHeaders()); |
| 488 } else if (responseCode >= 400) { |
| 489 mResponseChannel = InputStreamChannel.wrap(connection.getErr
orStream()); |
| 490 mCallbackAsync.onResponseStarted(mUrlResponseInfo); |
| 491 } else { |
| 492 mResponseChannel = InputStreamChannel.wrap(connection.getInp
utStream()); |
| 493 mCallbackAsync.onResponseStarted(mUrlResponseInfo); |
| 494 } |
| 495 } |
| 496 })); |
| 497 } |
| 498 |
| 499 private void fireRedirectReceived(final Map<String, List<String>> headerFiel
ds) { |
| 500 transitionStates(State.STARTED, State.REDIRECT_RECEIVED, new Runnable()
{ |
| 501 @Override |
| 502 public void run() { |
| 503 mPendingRedirectUrl = URI.create(mCurrentUrl) |
| 504 .resolve(headerFields.get("locatio
n").get(0)) |
| 505 .toString(); |
| 506 mUrlChain.add(mPendingRedirectUrl); |
| 507 transitionStates( |
| 508 State.REDIRECT_RECEIVED, State.AWAITING_FOLLOW_REDIRECT,
new Runnable() { |
| 509 @Override |
| 510 public void run() { |
| 511 mCallbackAsync.onRedirectReceived( |
| 512 mUrlResponseInfo, mPendingRedirectUrl); |
| 513 } |
| 514 }); |
| 515 } |
| 516 }); |
| 517 } |
| 518 |
| 519 private void fireOpenConnection() { |
| 520 mExecutor.execute(errorSetting(State.STARTED, new CheckedRunnable() { |
| 521 @Override |
| 522 public void run() throws Exception { |
| 523 // If we're cancelled, then our old connection will be disconnec
ted for us and |
| 524 // we shouldn't open a new one. |
| 525 if (mState.get() == State.CANCELLED) { |
| 526 return; |
| 527 } |
| 528 |
| 529 final URL url = new URL(mCurrentUrl); |
| 530 HttpURLConnection newConnection = (HttpURLConnection) url.openCo
nnection(); |
| 531 HttpURLConnection oldConnection = mCurrentUrlConnection.getAndSe
t(newConnection); |
| 532 if (oldConnection != null) { |
| 533 oldConnection.disconnect(); |
| 534 } |
| 535 newConnection.setInstanceFollowRedirects(false); |
| 536 if (!mRequestHeaders.containsKey(USER_AGENT)) { |
| 537 mRequestHeaders.put(USER_AGENT, mUserAgent); |
| 538 } |
| 539 for (Map.Entry<String, String> entry : mRequestHeaders.entrySet(
)) { |
| 540 newConnection.setRequestProperty(entry.getKey(), entry.getVa
lue()); |
| 541 } |
| 542 if (mInitialMethod == null) { |
| 543 mInitialMethod = "GET"; |
| 544 } |
| 545 newConnection.setRequestMethod(mInitialMethod); |
| 546 if (mUploadDataProvider != null) { |
| 547 OutputStreamDataSink dataSink = new OutputStreamDataSink( |
| 548 mUploadExecutor, mExecutor, newConnection, mUploadDa
taProvider); |
| 549 dataSink.start(mUrlChain.size() == 1); |
| 550 } else { |
| 551 mAdditionalStatusDetails = Status.CONNECTING; |
| 552 newConnection.connect(); |
| 553 mAdditionalStatusDetails = Status.WAITING_FOR_RESPONSE; |
| 554 fireGetHeaders(); |
| 555 } |
| 556 } |
| 557 })); |
| 558 } |
| 559 |
| 560 @Override |
| 561 public void read(final ByteBuffer buffer) { |
| 562 Preconditions.checkDirect(buffer); |
| 563 if (!(buffer.capacity() - buffer.position() > 0)) { |
| 564 throw new IllegalArgumentException("ByteBuffer is already full."); |
| 565 } |
| 566 transitionStates(State.AWAITING_READ, State.READING, new Runnable() { |
| 567 @Override |
| 568 public void run() { |
| 569 mExecutor.execute(errorSetting(State.READING, new CheckedRunnabl
e() { |
| 570 @Override |
| 571 public void run() throws IOException { |
| 572 int oldPosition = buffer.position(); |
| 573 buffer.limit(buffer.capacity()); |
| 574 int read = mResponseChannel.read(buffer); |
| 575 buffer.limit(buffer.position()); |
| 576 buffer.position(oldPosition); |
| 577 processReadResult(read, buffer); |
| 578 } |
| 579 })); |
| 580 } |
| 581 }); |
| 582 } |
| 583 |
| 584 private Runnable errorSetting(final State expectedState, final CheckedRunnab
le delegate) { |
| 585 return new Runnable() { |
| 586 @Override |
| 587 public void run() { |
| 588 try { |
| 589 delegate.run(); |
| 590 } catch (Throwable t) { |
| 591 enterCronetErrorState(expectedState, t); |
| 592 } |
| 593 } |
| 594 }; |
| 595 } |
| 596 |
| 597 private Runnable userErrorSetting(final State expectedState, final CheckedRu
nnable delegate) { |
| 598 return new Runnable() { |
| 599 @Override |
| 600 public void run() { |
| 601 try { |
| 602 delegate.run(); |
| 603 } catch (Throwable t) { |
| 604 enterUserErrorState(expectedState, t); |
| 605 } |
| 606 } |
| 607 }; |
| 608 } |
| 609 |
| 610 private Runnable uploadErrorSetting(final CheckedRunnable delegate) { |
| 611 return new Runnable() { |
| 612 @Override |
| 613 public void run() { |
| 614 try { |
| 615 delegate.run(); |
| 616 } catch (Throwable t) { |
| 617 enterUploadErrorState(t); |
| 618 } |
| 619 } |
| 620 }; |
| 621 } |
| 622 |
| 623 private interface CheckedRunnable { void run() throws Exception; } |
| 624 |
| 625 @Override |
| 626 public void readNew(final ByteBuffer buffer) { |
| 627 Preconditions.checkDirect(buffer); |
| 628 Preconditions.checkHasRemaining(buffer); |
| 629 transitionStates(State.AWAITING_READ, State.READING, new Runnable() { |
| 630 @Override |
| 631 public void run() { |
| 632 mExecutor.execute(errorSetting(State.READING, new CheckedRunnabl
e() { |
| 633 @Override |
| 634 public void run() throws Exception { |
| 635 int read = mResponseChannel.read(buffer); |
| 636 processReadResult(read, buffer); |
| 637 } |
| 638 })); |
| 639 } |
| 640 }); |
| 641 } |
| 642 |
| 643 private void processReadResult(int read, final ByteBuffer buffer) throws IOE
xception { |
| 644 if (read != -1) { |
| 645 mCallbackAsync.onReadCompleted(mUrlResponseInfo, buffer); |
| 646 } else { |
| 647 mResponseChannel.close(); |
| 648 if (mState.compareAndSet(State.READING, State.COMPLETE)) { |
| 649 fireDisconnect(); |
| 650 mCallbackAsync.onSucceeded(mUrlResponseInfo); |
| 651 } |
| 652 } |
| 653 } |
| 654 |
| 655 private void fireDisconnect() { |
| 656 final HttpURLConnection connection = mCurrentUrlConnection.getAndSet(nul
l); |
| 657 if (connection != null) { |
| 658 mExecutor.execute(new Runnable() { |
| 659 @Override |
| 660 public void run() { |
| 661 connection.disconnect(); |
| 662 } |
| 663 }); |
| 664 } |
| 665 } |
| 666 |
| 667 @Override |
| 668 public void cancel() { |
| 669 State oldState = mState.getAndSet(State.CANCELLED); |
| 670 switch (oldState) { |
| 671 // We've just scheduled some user code to run. When they perform the
ir next operation, |
| 672 // they'll observe it and fail. However, if user code is cancelling
in response to one |
| 673 // of these callbacks, we'll never actually cancel! |
| 674 // TODO(clm) figure out if it's possible to avoid concurrency in use
r callbacks. |
| 675 case REDIRECT_RECEIVED: |
| 676 case AWAITING_FOLLOW_REDIRECT: |
| 677 case AWAITING_READ: |
| 678 |
| 679 // User code is waiting on us - cancel away! |
| 680 case STARTED: |
| 681 case READING: |
| 682 fireDisconnect(); |
| 683 mCallbackAsync.onCanceled(mUrlResponseInfo); |
| 684 break; |
| 685 // The rest are all termination cases - we're too late to cancel. |
| 686 case ERROR: |
| 687 case COMPLETE: |
| 688 case CANCELLED: |
| 689 break; |
| 690 } |
| 691 } |
| 692 |
| 693 @Override |
| 694 public boolean isDone() { |
| 695 State state = mState.get(); |
| 696 return state == State.COMPLETE | state == State.ERROR | state == State.C
ANCELLED; |
| 697 } |
| 698 |
| 699 @Override |
| 700 public void disableCache() { |
| 701 // We have no cache |
| 702 } |
| 703 |
| 704 @Override |
| 705 public void getStatus(StatusListener listener) { |
| 706 State state = mState.get(); |
| 707 int extraStatus = this.mAdditionalStatusDetails; |
| 708 |
| 709 @Status.StatusValues final int status; |
| 710 switch (state) { |
| 711 case ERROR: |
| 712 case COMPLETE: |
| 713 case CANCELLED: |
| 714 case NOT_STARTED: |
| 715 status = Status.INVALID; |
| 716 break; |
| 717 case STARTED: |
| 718 status = extraStatus; |
| 719 break; |
| 720 case REDIRECT_RECEIVED: |
| 721 case AWAITING_FOLLOW_REDIRECT: |
| 722 case AWAITING_READ: |
| 723 status = Status.IDLE; |
| 724 break; |
| 725 case READING: |
| 726 status = Status.READING_RESPONSE; |
| 727 break; |
| 728 default: |
| 729 throw new IllegalStateException("Switch is exhaustive: " + state
); |
| 730 } |
| 731 |
| 732 mCallbackAsync.sendStatus(listener, status); |
| 733 } |
| 734 |
| 735 /** This wrapper ensures that callbacks are always called on the correct exe
cutor */ |
| 736 private final class AsyncUrlRequestCallback { |
| 737 final UrlRequest.Callback mCallback; |
| 738 final Executor mUserExecutor; |
| 739 |
| 740 AsyncUrlRequestCallback(Callback callback, final Executor userExecutor)
{ |
| 741 this.mCallback = callback; |
| 742 this.mUserExecutor = userExecutor; |
| 743 } |
| 744 |
| 745 void sendStatus(final StatusListener listener, final int status) { |
| 746 mUserExecutor.execute(new Runnable() { |
| 747 @Override |
| 748 public void run() { |
| 749 listener.onStatus(status); |
| 750 } |
| 751 }); |
| 752 } |
| 753 |
| 754 void execute(State currentState, CheckedRunnable runnable) { |
| 755 try { |
| 756 mUserExecutor.execute(userErrorSetting(currentState, runnable)); |
| 757 } catch (RejectedExecutionException e) { |
| 758 enterUserErrorState(currentState, e); |
| 759 } |
| 760 } |
| 761 |
| 762 void onRedirectReceived(final UrlResponseInfo info, final String newLoca
tionUrl) { |
| 763 execute(State.AWAITING_FOLLOW_REDIRECT, new CheckedRunnable() { |
| 764 @Override |
| 765 public void run() throws Exception { |
| 766 mCallback.onRedirectReceived(JavaUrlRequest.this, info, newL
ocationUrl); |
| 767 } |
| 768 }); |
| 769 } |
| 770 |
| 771 void onResponseStarted(UrlResponseInfo info) { |
| 772 execute(State.AWAITING_READ, new CheckedRunnable() { |
| 773 @Override |
| 774 public void run() { |
| 775 if (mState.compareAndSet(State.STARTED, State.AWAITING_READ)
) { |
| 776 mCallback.onResponseStarted(JavaUrlRequest.this, mUrlRes
ponseInfo); |
| 777 } |
| 778 } |
| 779 }); |
| 780 } |
| 781 |
| 782 void onReadCompleted(final UrlResponseInfo info, final ByteBuffer byteBu
ffer) { |
| 783 execute(State.AWAITING_READ, new CheckedRunnable() { |
| 784 @Override |
| 785 public void run() { |
| 786 if (mState.compareAndSet(State.READING, State.AWAITING_READ)
) { |
| 787 mCallback.onReadCompleted(JavaUrlRequest.this, info, byt
eBuffer); |
| 788 } |
| 789 } |
| 790 }); |
| 791 } |
| 792 |
| 793 void onCanceled(final UrlResponseInfo info) { |
| 794 closeQuietly(mResponseChannel); |
| 795 mUserExecutor.execute(new Runnable() { |
| 796 @Override |
| 797 public void run() { |
| 798 try { |
| 799 mCallback.onCanceled(JavaUrlRequest.this, info); |
| 800 } catch (Exception exception) { |
| 801 Log.e(TAG, "Exception in onCanceled method", exception); |
| 802 } |
| 803 } |
| 804 }); |
| 805 } |
| 806 |
| 807 void onSucceeded(final UrlResponseInfo info) { |
| 808 mUserExecutor.execute(new Runnable() { |
| 809 @Override |
| 810 public void run() { |
| 811 try { |
| 812 mCallback.onSucceeded(JavaUrlRequest.this, info); |
| 813 } catch (Exception exception) { |
| 814 Log.e(TAG, "Exception in onSucceeded method", exception)
; |
| 815 } |
| 816 } |
| 817 }); |
| 818 } |
| 819 |
| 820 void onFailed(final UrlResponseInfo urlResponseInfo, final UrlRequestExc
eption e) { |
| 821 closeQuietly(mResponseChannel); |
| 822 mUserExecutor.execute(new Runnable() { |
| 823 @Override |
| 824 public void run() { |
| 825 try { |
| 826 mCallback.onFailed(JavaUrlRequest.this, urlResponseInfo,
e); |
| 827 } catch (Exception exception) { |
| 828 Log.e(TAG, "Exception in onFailed method", exception); |
| 829 } |
| 830 } |
| 831 }); |
| 832 } |
| 833 } |
| 834 |
| 835 private static void closeQuietly(Closeable closeable) { |
| 836 if (closeable == null) { |
| 837 return; |
| 838 } |
| 839 try { |
| 840 closeable.close(); |
| 841 } catch (IOException e) { |
| 842 e.printStackTrace(); |
| 843 } |
| 844 } |
| 845 } |
OLD | NEW |