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

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

Powered by Google App Engine
This is Rietveld 408576698