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 |