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