Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(64)

Side by Side Diff: components/cronet/android/api/src/org/chromium/net/JavaUrlRequest.java

Issue 1492583002: Add HttpUrlConnection backed implementation of CronetEngine. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Update tests and header ordering. Created 5 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(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 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698