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