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