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