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

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: Addressed comments 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.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
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");
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;
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")) {
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 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698