• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.google.android.exoplayer2.ext.cronet;
17 
18 import static com.google.android.exoplayer2.util.Util.castNonNull;
19 
20 import android.net.Uri;
21 import android.text.TextUtils;
22 import androidx.annotation.Nullable;
23 import com.google.android.exoplayer2.C;
24 import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
25 import com.google.android.exoplayer2.upstream.BaseDataSource;
26 import com.google.android.exoplayer2.upstream.DataSourceException;
27 import com.google.android.exoplayer2.upstream.DataSpec;
28 import com.google.android.exoplayer2.upstream.HttpDataSource;
29 import com.google.android.exoplayer2.util.Assertions;
30 import com.google.android.exoplayer2.util.Clock;
31 import com.google.android.exoplayer2.util.ConditionVariable;
32 import com.google.android.exoplayer2.util.Log;
33 import com.google.android.exoplayer2.util.Predicate;
34 import java.io.IOException;
35 import java.net.SocketTimeoutException;
36 import java.net.UnknownHostException;
37 import java.nio.ByteBuffer;
38 import java.util.Collections;
39 import java.util.HashMap;
40 import java.util.List;
41 import java.util.Map;
42 import java.util.Map.Entry;
43 import java.util.concurrent.Executor;
44 import java.util.regex.Matcher;
45 import java.util.regex.Pattern;
46 import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
47 import org.chromium.net.CronetEngine;
48 import org.chromium.net.CronetException;
49 import org.chromium.net.NetworkException;
50 import org.chromium.net.UrlRequest;
51 import org.chromium.net.UrlRequest.Status;
52 import org.chromium.net.UrlResponseInfo;
53 
54 /**
55  * DataSource without intermediate buffer based on Cronet API set using UrlRequest.
56  *
57  * <p>Note: HTTP request headers will be set using all parameters passed via (in order of decreasing
58  * priority) the {@code dataSpec}, {@link #setRequestProperty} and the default parameters used to
59  * construct the instance.
60  */
61 public class CronetDataSource extends BaseDataSource implements HttpDataSource {
62 
63   /**
64    * Thrown when an error is encountered when trying to open a {@link CronetDataSource}.
65    */
66   public static final class OpenException extends HttpDataSourceException {
67 
68     /**
69      * Returns the status of the connection establishment at the moment when the error occurred, as
70      * defined by {@link UrlRequest.Status}.
71      */
72     public final int cronetConnectionStatus;
73 
OpenException(IOException cause, DataSpec dataSpec, int cronetConnectionStatus)74     public OpenException(IOException cause, DataSpec dataSpec, int cronetConnectionStatus) {
75       super(cause, dataSpec, TYPE_OPEN);
76       this.cronetConnectionStatus = cronetConnectionStatus;
77     }
78 
OpenException(String errorMessage, DataSpec dataSpec, int cronetConnectionStatus)79     public OpenException(String errorMessage, DataSpec dataSpec, int cronetConnectionStatus) {
80       super(errorMessage, dataSpec, TYPE_OPEN);
81       this.cronetConnectionStatus = cronetConnectionStatus;
82     }
83 
84   }
85 
86   /** Thrown on catching an InterruptedException. */
87   public static final class InterruptedIOException extends IOException {
88 
InterruptedIOException(InterruptedException e)89     public InterruptedIOException(InterruptedException e) {
90       super(e);
91     }
92   }
93 
94   static {
95     ExoPlayerLibraryInfo.registerModule("goog.exo.cronet");
96   }
97 
98   /**
99    * The default connection timeout, in milliseconds.
100    */
101   public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS = 8 * 1000;
102   /**
103    * The default read timeout, in milliseconds.
104    */
105   public static final int DEFAULT_READ_TIMEOUT_MILLIS = 8 * 1000;
106 
107   /* package */ final UrlRequest.Callback urlRequestCallback;
108 
109   private static final String TAG = "CronetDataSource";
110   private static final String CONTENT_TYPE = "Content-Type";
111   private static final String SET_COOKIE = "Set-Cookie";
112   private static final String COOKIE = "Cookie";
113 
114   private static final Pattern CONTENT_RANGE_HEADER_PATTERN =
115       Pattern.compile("^bytes (\\d+)-(\\d+)/(\\d+)$");
116   // The size of read buffer passed to cronet UrlRequest.read().
117   private static final int READ_BUFFER_SIZE_BYTES = 32 * 1024;
118 
119   private final CronetEngine cronetEngine;
120   private final Executor executor;
121   private final int connectTimeoutMs;
122   private final int readTimeoutMs;
123   private final boolean resetTimeoutOnRedirects;
124   private final boolean handleSetCookieRequests;
125   @Nullable private final RequestProperties defaultRequestProperties;
126   private final RequestProperties requestProperties;
127   private final ConditionVariable operation;
128   private final Clock clock;
129 
130   @Nullable private Predicate<String> contentTypePredicate;
131 
132   // Accessed by the calling thread only.
133   private boolean opened;
134   private long bytesToSkip;
135   private long bytesRemaining;
136 
137   // Written from the calling thread only. currentUrlRequest.start() calls ensure writes are visible
138   // to reads made by the Cronet thread.
139   @Nullable private UrlRequest currentUrlRequest;
140   @Nullable private DataSpec currentDataSpec;
141 
142   // Reference written and read by calling thread only. Passed to Cronet thread as a local variable.
143   // operation.open() calls ensure writes into the buffer are visible to reads made by the calling
144   // thread.
145   @Nullable private ByteBuffer readBuffer;
146 
147   // Written from the Cronet thread only. operation.open() calls ensure writes are visible to reads
148   // made by the calling thread.
149   @Nullable private UrlResponseInfo responseInfo;
150   @Nullable private IOException exception;
151   private boolean finished;
152 
153   private volatile long currentConnectTimeoutMs;
154 
155   /**
156    * @param cronetEngine A CronetEngine.
157    * @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may
158    *     be a direct executor (i.e. executes tasks on the calling thread) in order to avoid a thread
159    *     hop from Cronet's internal network thread to the response handling thread. However, to
160    *     avoid slowing down overall network performance, care must be taken to make sure response
161    *     handling is a fast operation when using a direct executor.
162    */
CronetDataSource(CronetEngine cronetEngine, Executor executor)163   public CronetDataSource(CronetEngine cronetEngine, Executor executor) {
164     this(
165         cronetEngine,
166         executor,
167         DEFAULT_CONNECT_TIMEOUT_MILLIS,
168         DEFAULT_READ_TIMEOUT_MILLIS,
169         /* resetTimeoutOnRedirects= */ false,
170         /* defaultRequestProperties= */ null);
171   }
172 
173   /**
174    * @param cronetEngine A CronetEngine.
175    * @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may
176    *     be a direct executor (i.e. executes tasks on the calling thread) in order to avoid a thread
177    *     hop from Cronet's internal network thread to the response handling thread. However, to
178    *     avoid slowing down overall network performance, care must be taken to make sure response
179    *     handling is a fast operation when using a direct executor.
180    * @param connectTimeoutMs The connection timeout, in milliseconds.
181    * @param readTimeoutMs The read timeout, in milliseconds.
182    * @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
183    * @param defaultRequestProperties Optional default {@link RequestProperties} to be sent to the
184    *     server as HTTP headers on every request.
185    */
CronetDataSource( CronetEngine cronetEngine, Executor executor, int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, @Nullable RequestProperties defaultRequestProperties)186   public CronetDataSource(
187       CronetEngine cronetEngine,
188       Executor executor,
189       int connectTimeoutMs,
190       int readTimeoutMs,
191       boolean resetTimeoutOnRedirects,
192       @Nullable RequestProperties defaultRequestProperties) {
193     this(
194         cronetEngine,
195         executor,
196         connectTimeoutMs,
197         readTimeoutMs,
198         resetTimeoutOnRedirects,
199         Clock.DEFAULT,
200         defaultRequestProperties,
201         /* handleSetCookieRequests= */ false);
202   }
203 
204   /**
205    * @param cronetEngine A CronetEngine.
206    * @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may
207    *     be a direct executor (i.e. executes tasks on the calling thread) in order to avoid a thread
208    *     hop from Cronet's internal network thread to the response handling thread. However, to
209    *     avoid slowing down overall network performance, care must be taken to make sure response
210    *     handling is a fast operation when using a direct executor.
211    * @param connectTimeoutMs The connection timeout, in milliseconds.
212    * @param readTimeoutMs The read timeout, in milliseconds.
213    * @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
214    * @param defaultRequestProperties Optional default {@link RequestProperties} to be sent to the
215    *     server as HTTP headers on every request.
216    * @param handleSetCookieRequests Whether "Set-Cookie" requests on redirect should be forwarded to
217    *     the redirect url in the "Cookie" header.
218    */
CronetDataSource( CronetEngine cronetEngine, Executor executor, int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, @Nullable RequestProperties defaultRequestProperties, boolean handleSetCookieRequests)219   public CronetDataSource(
220       CronetEngine cronetEngine,
221       Executor executor,
222       int connectTimeoutMs,
223       int readTimeoutMs,
224       boolean resetTimeoutOnRedirects,
225       @Nullable RequestProperties defaultRequestProperties,
226       boolean handleSetCookieRequests) {
227     this(
228         cronetEngine,
229         executor,
230         connectTimeoutMs,
231         readTimeoutMs,
232         resetTimeoutOnRedirects,
233         Clock.DEFAULT,
234         defaultRequestProperties,
235         handleSetCookieRequests);
236   }
237 
238   /**
239    * @param cronetEngine A CronetEngine.
240    * @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may
241    *     be a direct executor (i.e. executes tasks on the calling thread) in order to avoid a thread
242    *     hop from Cronet's internal network thread to the response handling thread. However, to
243    *     avoid slowing down overall network performance, care must be taken to make sure response
244    *     handling is a fast operation when using a direct executor.
245    * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
246    *     predicate then an {@link InvalidContentTypeException} is thrown from {@link
247    *     #open(DataSpec)}.
248    * @deprecated Use {@link #CronetDataSource(CronetEngine, Executor)} and {@link
249    *     #setContentTypePredicate(Predicate)}.
250    */
251   @Deprecated
CronetDataSource( CronetEngine cronetEngine, Executor executor, @Nullable Predicate<String> contentTypePredicate)252   public CronetDataSource(
253       CronetEngine cronetEngine,
254       Executor executor,
255       @Nullable Predicate<String> contentTypePredicate) {
256     this(
257         cronetEngine,
258         executor,
259         contentTypePredicate,
260         DEFAULT_CONNECT_TIMEOUT_MILLIS,
261         DEFAULT_READ_TIMEOUT_MILLIS,
262         /* resetTimeoutOnRedirects= */ false,
263         /* defaultRequestProperties= */ null);
264   }
265 
266   /**
267    * @param cronetEngine A CronetEngine.
268    * @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may
269    *     be a direct executor (i.e. executes tasks on the calling thread) in order to avoid a thread
270    *     hop from Cronet's internal network thread to the response handling thread. However, to
271    *     avoid slowing down overall network performance, care must be taken to make sure response
272    *     handling is a fast operation when using a direct executor.
273    * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
274    *     predicate then an {@link InvalidContentTypeException} is thrown from {@link
275    *     #open(DataSpec)}.
276    * @param connectTimeoutMs The connection timeout, in milliseconds.
277    * @param readTimeoutMs The read timeout, in milliseconds.
278    * @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
279    * @param defaultRequestProperties Optional default {@link RequestProperties} to be sent to the
280    *     server as HTTP headers on every request.
281    * @deprecated Use {@link #CronetDataSource(CronetEngine, Executor, int, int, boolean,
282    *     RequestProperties)} and {@link #setContentTypePredicate(Predicate)}.
283    */
284   @Deprecated
CronetDataSource( CronetEngine cronetEngine, Executor executor, @Nullable Predicate<String> contentTypePredicate, int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, @Nullable RequestProperties defaultRequestProperties)285   public CronetDataSource(
286       CronetEngine cronetEngine,
287       Executor executor,
288       @Nullable Predicate<String> contentTypePredicate,
289       int connectTimeoutMs,
290       int readTimeoutMs,
291       boolean resetTimeoutOnRedirects,
292       @Nullable RequestProperties defaultRequestProperties) {
293     this(
294         cronetEngine,
295         executor,
296         contentTypePredicate,
297         connectTimeoutMs,
298         readTimeoutMs,
299         resetTimeoutOnRedirects,
300         defaultRequestProperties,
301         /* handleSetCookieRequests= */ false);
302   }
303 
304   /**
305    * @param cronetEngine A CronetEngine.
306    * @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may
307    *     be a direct executor (i.e. executes tasks on the calling thread) in order to avoid a thread
308    *     hop from Cronet's internal network thread to the response handling thread. However, to
309    *     avoid slowing down overall network performance, care must be taken to make sure response
310    *     handling is a fast operation when using a direct executor.
311    * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
312    *     predicate then an {@link InvalidContentTypeException} is thrown from {@link
313    *     #open(DataSpec)}.
314    * @param connectTimeoutMs The connection timeout, in milliseconds.
315    * @param readTimeoutMs The read timeout, in milliseconds.
316    * @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
317    * @param defaultRequestProperties Optional default {@link RequestProperties} to be sent to the
318    *     server as HTTP headers on every request.
319    * @param handleSetCookieRequests Whether "Set-Cookie" requests on redirect should be forwarded to
320    *     the redirect url in the "Cookie" header.
321    * @deprecated Use {@link #CronetDataSource(CronetEngine, Executor, int, int, boolean,
322    *     RequestProperties, boolean)} and {@link #setContentTypePredicate(Predicate)}.
323    */
324   @Deprecated
CronetDataSource( CronetEngine cronetEngine, Executor executor, @Nullable Predicate<String> contentTypePredicate, int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, @Nullable RequestProperties defaultRequestProperties, boolean handleSetCookieRequests)325   public CronetDataSource(
326       CronetEngine cronetEngine,
327       Executor executor,
328       @Nullable Predicate<String> contentTypePredicate,
329       int connectTimeoutMs,
330       int readTimeoutMs,
331       boolean resetTimeoutOnRedirects,
332       @Nullable RequestProperties defaultRequestProperties,
333       boolean handleSetCookieRequests) {
334     this(
335         cronetEngine,
336         executor,
337         connectTimeoutMs,
338         readTimeoutMs,
339         resetTimeoutOnRedirects,
340         Clock.DEFAULT,
341         defaultRequestProperties,
342         handleSetCookieRequests);
343     this.contentTypePredicate = contentTypePredicate;
344   }
345 
CronetDataSource( CronetEngine cronetEngine, Executor executor, int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, Clock clock, @Nullable RequestProperties defaultRequestProperties, boolean handleSetCookieRequests)346   /* package */ CronetDataSource(
347       CronetEngine cronetEngine,
348       Executor executor,
349       int connectTimeoutMs,
350       int readTimeoutMs,
351       boolean resetTimeoutOnRedirects,
352       Clock clock,
353       @Nullable RequestProperties defaultRequestProperties,
354       boolean handleSetCookieRequests) {
355     super(/* isNetwork= */ true);
356     this.urlRequestCallback = new UrlRequestCallback();
357     this.cronetEngine = Assertions.checkNotNull(cronetEngine);
358     this.executor = Assertions.checkNotNull(executor);
359     this.connectTimeoutMs = connectTimeoutMs;
360     this.readTimeoutMs = readTimeoutMs;
361     this.resetTimeoutOnRedirects = resetTimeoutOnRedirects;
362     this.clock = Assertions.checkNotNull(clock);
363     this.defaultRequestProperties = defaultRequestProperties;
364     this.handleSetCookieRequests = handleSetCookieRequests;
365     requestProperties = new RequestProperties();
366     operation = new ConditionVariable();
367   }
368 
369   /**
370    * Sets a content type {@link Predicate}. If a content type is rejected by the predicate then a
371    * {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link #open(DataSpec)}.
372    *
373    * @param contentTypePredicate The content type {@link Predicate}, or {@code null} to clear a
374    *     predicate that was previously set.
375    */
setContentTypePredicate(@ullable Predicate<String> contentTypePredicate)376   public void setContentTypePredicate(@Nullable Predicate<String> contentTypePredicate) {
377     this.contentTypePredicate = contentTypePredicate;
378   }
379 
380   // HttpDataSource implementation.
381 
382   @Override
setRequestProperty(String name, String value)383   public void setRequestProperty(String name, String value) {
384     requestProperties.set(name, value);
385   }
386 
387   @Override
clearRequestProperty(String name)388   public void clearRequestProperty(String name) {
389     requestProperties.remove(name);
390   }
391 
392   @Override
clearAllRequestProperties()393   public void clearAllRequestProperties() {
394     requestProperties.clear();
395   }
396 
397   @Override
getResponseCode()398   public int getResponseCode() {
399     return responseInfo == null || responseInfo.getHttpStatusCode() <= 0
400         ? -1
401         : responseInfo.getHttpStatusCode();
402   }
403 
404   @Override
getResponseHeaders()405   public Map<String, List<String>> getResponseHeaders() {
406     return responseInfo == null ? Collections.emptyMap() : responseInfo.getAllHeaders();
407   }
408 
409   @Override
410   @Nullable
getUri()411   public Uri getUri() {
412     return responseInfo == null ? null : Uri.parse(responseInfo.getUrl());
413   }
414 
415   @Override
open(DataSpec dataSpec)416   public long open(DataSpec dataSpec) throws HttpDataSourceException {
417     Assertions.checkNotNull(dataSpec);
418     Assertions.checkState(!opened);
419 
420     operation.close();
421     resetConnectTimeout();
422     currentDataSpec = dataSpec;
423     UrlRequest urlRequest;
424     try {
425       urlRequest = buildRequestBuilder(dataSpec).build();
426       currentUrlRequest = urlRequest;
427     } catch (IOException e) {
428       throw new OpenException(e, dataSpec, Status.IDLE);
429     }
430     urlRequest.start();
431 
432     transferInitializing(dataSpec);
433     try {
434       boolean connectionOpened = blockUntilConnectTimeout();
435       if (exception != null) {
436         throw new OpenException(exception, dataSpec, getStatus(urlRequest));
437       } else if (!connectionOpened) {
438         // The timeout was reached before the connection was opened.
439         throw new OpenException(new SocketTimeoutException(), dataSpec, getStatus(urlRequest));
440       }
441     } catch (InterruptedException e) {
442       Thread.currentThread().interrupt();
443       throw new OpenException(new InterruptedIOException(e), dataSpec, Status.INVALID);
444     }
445 
446     // Check for a valid response code.
447     UrlResponseInfo responseInfo = Assertions.checkNotNull(this.responseInfo);
448     int responseCode = responseInfo.getHttpStatusCode();
449     if (responseCode < 200 || responseCode > 299) {
450       InvalidResponseCodeException exception =
451           new InvalidResponseCodeException(
452               responseCode,
453               responseInfo.getHttpStatusText(),
454               responseInfo.getAllHeaders(),
455               dataSpec);
456       if (responseCode == 416) {
457         exception.initCause(new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE));
458       }
459       throw exception;
460     }
461 
462     // Check for a valid content type.
463     Predicate<String> contentTypePredicate = this.contentTypePredicate;
464     if (contentTypePredicate != null) {
465       List<String> contentTypeHeaders = responseInfo.getAllHeaders().get(CONTENT_TYPE);
466       String contentType = isEmpty(contentTypeHeaders) ? null : contentTypeHeaders.get(0);
467       if (contentType != null && !contentTypePredicate.evaluate(contentType)) {
468         throw new InvalidContentTypeException(contentType, dataSpec);
469       }
470     }
471 
472     // If we requested a range starting from a non-zero position and received a 200 rather than a
473     // 206, then the server does not support partial requests. We'll need to manually skip to the
474     // requested position.
475     bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0;
476 
477     // Calculate the content length.
478     if (!isCompressed(responseInfo)) {
479       if (dataSpec.length != C.LENGTH_UNSET) {
480         bytesRemaining = dataSpec.length;
481       } else {
482         bytesRemaining = getContentLength(responseInfo);
483       }
484     } else {
485       // If the response is compressed then the content length will be that of the compressed data
486       // which isn't what we want. Always use the dataSpec length in this case.
487       bytesRemaining = dataSpec.length;
488     }
489 
490     opened = true;
491     transferStarted(dataSpec);
492 
493     return bytesRemaining;
494   }
495 
496   @Override
read(byte[] buffer, int offset, int readLength)497   public int read(byte[] buffer, int offset, int readLength) throws HttpDataSourceException {
498     Assertions.checkState(opened);
499 
500     if (readLength == 0) {
501       return 0;
502     } else if (bytesRemaining == 0) {
503       return C.RESULT_END_OF_INPUT;
504     }
505 
506     ByteBuffer readBuffer = this.readBuffer;
507     if (readBuffer == null) {
508       readBuffer = ByteBuffer.allocateDirect(READ_BUFFER_SIZE_BYTES);
509       readBuffer.limit(0);
510       this.readBuffer = readBuffer;
511     }
512     while (!readBuffer.hasRemaining()) {
513       // Fill readBuffer with more data from Cronet.
514       operation.close();
515       readBuffer.clear();
516       readInternal(castNonNull(readBuffer));
517 
518       if (finished) {
519         bytesRemaining = 0;
520         return C.RESULT_END_OF_INPUT;
521       } else {
522         // The operation didn't time out, fail or finish, and therefore data must have been read.
523         readBuffer.flip();
524         Assertions.checkState(readBuffer.hasRemaining());
525         if (bytesToSkip > 0) {
526           int bytesSkipped = (int) Math.min(readBuffer.remaining(), bytesToSkip);
527           readBuffer.position(readBuffer.position() + bytesSkipped);
528           bytesToSkip -= bytesSkipped;
529         }
530       }
531     }
532 
533     int bytesRead = Math.min(readBuffer.remaining(), readLength);
534     readBuffer.get(buffer, offset, bytesRead);
535 
536     if (bytesRemaining != C.LENGTH_UNSET) {
537       bytesRemaining -= bytesRead;
538     }
539     bytesTransferred(bytesRead);
540     return bytesRead;
541   }
542 
543   /**
544    * Reads up to {@code buffer.remaining()} bytes of data and stores them into {@code buffer},
545    * starting at {@code buffer.position()}. Advances the position of the buffer by the number of
546    * bytes read and returns this length.
547    *
548    * <p>If there is an error, a {@link HttpDataSourceException} is thrown and the contents of {@code
549    * buffer} should be ignored. If the exception has error code {@code
550    * HttpDataSourceException.TYPE_READ}, note that Cronet may continue writing into {@code buffer}
551    * after the method has returned. Thus the caller should not attempt to reuse the buffer.
552    *
553    * <p>If {@code buffer.remaining()} is zero then 0 is returned. Otherwise, if no data is available
554    * because the end of the opened range has been reached, then {@link C#RESULT_END_OF_INPUT} is
555    * returned. Otherwise, the call will block until at least one byte of data has been read and the
556    * number of bytes read is returned.
557    *
558    * <p>Passed buffer must be direct ByteBuffer. If you have a non-direct ByteBuffer, consider the
559    * alternative read method with its backed array.
560    *
561    * @param buffer The ByteBuffer into which the read data should be stored. Must be a direct
562    *     ByteBuffer.
563    * @return The number of bytes read, or {@link C#RESULT_END_OF_INPUT} if no data is available
564    *     because the end of the opened range has been reached.
565    * @throws HttpDataSourceException If an error occurs reading from the source.
566    * @throws IllegalArgumentException If {@code buffer} is not a direct ByteBuffer.
567    */
read(ByteBuffer buffer)568   public int read(ByteBuffer buffer) throws HttpDataSourceException {
569     Assertions.checkState(opened);
570 
571     if (!buffer.isDirect()) {
572       throw new IllegalArgumentException("Passed buffer is not a direct ByteBuffer");
573     }
574     if (!buffer.hasRemaining()) {
575       return 0;
576     } else if (bytesRemaining == 0) {
577       return C.RESULT_END_OF_INPUT;
578     }
579     int readLength = buffer.remaining();
580 
581     if (readBuffer != null) {
582       // Skip all the bytes we can from readBuffer if there are still bytes to skip.
583       if (bytesToSkip != 0) {
584         if (bytesToSkip >= readBuffer.remaining()) {
585           bytesToSkip -= readBuffer.remaining();
586           readBuffer.position(readBuffer.limit());
587         } else {
588           readBuffer.position(readBuffer.position() + (int) bytesToSkip);
589           bytesToSkip = 0;
590         }
591       }
592 
593       // If there is existing data in the readBuffer, read as much as possible. Return if any read.
594       int copyBytes = copyByteBuffer(/* src= */ readBuffer, /* dst= */ buffer);
595       if (copyBytes != 0) {
596         if (bytesRemaining != C.LENGTH_UNSET) {
597           bytesRemaining -= copyBytes;
598         }
599         bytesTransferred(copyBytes);
600         return copyBytes;
601       }
602     }
603 
604     boolean readMore = true;
605     while (readMore) {
606       // If bytesToSkip > 0, read into intermediate buffer that we can discard instead of caller's
607       // buffer. If we do not need to skip bytes, we may write to buffer directly.
608       final boolean useCallerBuffer = bytesToSkip == 0;
609 
610       operation.close();
611 
612       if (!useCallerBuffer) {
613         if (readBuffer == null) {
614           readBuffer = ByteBuffer.allocateDirect(READ_BUFFER_SIZE_BYTES);
615         } else {
616           readBuffer.clear();
617         }
618         if (bytesToSkip < READ_BUFFER_SIZE_BYTES) {
619           readBuffer.limit((int) bytesToSkip);
620         }
621       }
622 
623       // Fill buffer with more data from Cronet.
624       readInternal(useCallerBuffer ? buffer : castNonNull(readBuffer));
625 
626       if (finished) {
627         bytesRemaining = 0;
628         return C.RESULT_END_OF_INPUT;
629       } else {
630         // The operation didn't time out, fail or finish, and therefore data must have been read.
631         Assertions.checkState(
632             useCallerBuffer
633                 ? readLength > buffer.remaining()
634                 : castNonNull(readBuffer).position() > 0);
635         // If we meant to skip bytes, subtract what was left and repeat, otherwise, continue.
636         if (useCallerBuffer) {
637           readMore = false;
638         } else {
639           bytesToSkip -= castNonNull(readBuffer).position();
640         }
641       }
642     }
643 
644     final int bytesRead = readLength - buffer.remaining();
645     if (bytesRemaining != C.LENGTH_UNSET) {
646       bytesRemaining -= bytesRead;
647     }
648     bytesTransferred(bytesRead);
649     return bytesRead;
650   }
651 
652   @Override
close()653   public synchronized void close() {
654     if (currentUrlRequest != null) {
655       currentUrlRequest.cancel();
656       currentUrlRequest = null;
657     }
658     if (readBuffer != null) {
659       readBuffer.limit(0);
660     }
661     currentDataSpec = null;
662     responseInfo = null;
663     exception = null;
664     finished = false;
665     if (opened) {
666       opened = false;
667       transferEnded();
668     }
669   }
670 
671   /** Returns current {@link UrlRequest}. May be null if the data source is not opened. */
672   @Nullable
getCurrentUrlRequest()673   protected UrlRequest getCurrentUrlRequest() {
674     return currentUrlRequest;
675   }
676 
677   /** Returns current {@link UrlResponseInfo}. May be null if the data source is not opened. */
678   @Nullable
getCurrentUrlResponseInfo()679   protected UrlResponseInfo getCurrentUrlResponseInfo() {
680     return responseInfo;
681   }
682 
683   // Internal methods.
684 
buildRequestBuilder(DataSpec dataSpec)685   private UrlRequest.Builder buildRequestBuilder(DataSpec dataSpec) throws IOException {
686     UrlRequest.Builder requestBuilder =
687         cronetEngine
688             .newUrlRequestBuilder(dataSpec.uri.toString(), urlRequestCallback, executor)
689             .allowDirectExecutor();
690 
691     // Set the headers.
692     Map<String, String> requestHeaders = new HashMap<>();
693     if (defaultRequestProperties != null) {
694       requestHeaders.putAll(defaultRequestProperties.getSnapshot());
695     }
696     requestHeaders.putAll(requestProperties.getSnapshot());
697     requestHeaders.putAll(dataSpec.httpRequestHeaders);
698 
699     for (Entry<String, String> headerEntry : requestHeaders.entrySet()) {
700       String key = headerEntry.getKey();
701       String value = headerEntry.getValue();
702       requestBuilder.addHeader(key, value);
703     }
704 
705     if (dataSpec.httpBody != null && !requestHeaders.containsKey(CONTENT_TYPE)) {
706       throw new IOException("HTTP request with non-empty body must set Content-Type");
707     }
708 
709     // Set the Range header.
710     if (dataSpec.position != 0 || dataSpec.length != C.LENGTH_UNSET) {
711       StringBuilder rangeValue = new StringBuilder();
712       rangeValue.append("bytes=");
713       rangeValue.append(dataSpec.position);
714       rangeValue.append("-");
715       if (dataSpec.length != C.LENGTH_UNSET) {
716         rangeValue.append(dataSpec.position + dataSpec.length - 1);
717       }
718       requestBuilder.addHeader("Range", rangeValue.toString());
719     }
720     // TODO: Uncomment when https://bugs.chromium.org/p/chromium/issues/detail?id=711810 is fixed
721     // (adjusting the code as necessary).
722     // Force identity encoding unless gzip is allowed.
723     // if (!dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP)) {
724     //   requestBuilder.addHeader("Accept-Encoding", "identity");
725     // }
726     // Set the method and (if non-empty) the body.
727     requestBuilder.setHttpMethod(dataSpec.getHttpMethodString());
728     if (dataSpec.httpBody != null) {
729       requestBuilder.setUploadDataProvider(
730           new ByteArrayUploadDataProvider(dataSpec.httpBody), executor);
731     }
732     return requestBuilder;
733   }
734 
blockUntilConnectTimeout()735   private boolean blockUntilConnectTimeout() throws InterruptedException {
736     long now = clock.elapsedRealtime();
737     boolean opened = false;
738     while (!opened && now < currentConnectTimeoutMs) {
739       opened = operation.block(currentConnectTimeoutMs - now + 5 /* fudge factor */);
740       now = clock.elapsedRealtime();
741     }
742     return opened;
743   }
744 
resetConnectTimeout()745   private void resetConnectTimeout() {
746     currentConnectTimeoutMs = clock.elapsedRealtime() + connectTimeoutMs;
747   }
748 
749   /**
750    * Reads up to {@code buffer.remaining()} bytes of data from {@code currentUrlRequest} and stores
751    * them into {@code buffer}. If there is an error and {@code buffer == readBuffer}, then it resets
752    * the current {@code readBuffer} object so that it is not reused in the future.
753    *
754    * @param buffer The ByteBuffer into which the read data is stored. Must be a direct ByteBuffer.
755    * @throws HttpDataSourceException If an error occurs reading from the source.
756    */
757   @SuppressWarnings("ReferenceEquality")
readInternal(ByteBuffer buffer)758   private void readInternal(ByteBuffer buffer) throws HttpDataSourceException {
759     castNonNull(currentUrlRequest).read(buffer);
760     try {
761       if (!operation.block(readTimeoutMs)) {
762         throw new SocketTimeoutException();
763       }
764     } catch (InterruptedException e) {
765       // The operation is ongoing so replace buffer to avoid it being written to by this
766       // operation during a subsequent request.
767       if (buffer == readBuffer) {
768         readBuffer = null;
769       }
770       Thread.currentThread().interrupt();
771       throw new HttpDataSourceException(
772           new InterruptedIOException(e),
773           castNonNull(currentDataSpec),
774           HttpDataSourceException.TYPE_READ);
775     } catch (SocketTimeoutException e) {
776       // The operation is ongoing so replace buffer to avoid it being written to by this
777       // operation during a subsequent request.
778       if (buffer == readBuffer) {
779         readBuffer = null;
780       }
781       throw new HttpDataSourceException(
782           e, castNonNull(currentDataSpec), HttpDataSourceException.TYPE_READ);
783     }
784 
785     if (exception != null) {
786       throw new HttpDataSourceException(
787           exception, castNonNull(currentDataSpec), HttpDataSourceException.TYPE_READ);
788     }
789   }
790 
isCompressed(UrlResponseInfo info)791   private static boolean isCompressed(UrlResponseInfo info) {
792     for (Map.Entry<String, String> entry : info.getAllHeadersAsList()) {
793       if (entry.getKey().equalsIgnoreCase("Content-Encoding")) {
794         return !entry.getValue().equalsIgnoreCase("identity");
795       }
796     }
797     return false;
798   }
799 
getContentLength(UrlResponseInfo info)800   private static long getContentLength(UrlResponseInfo info) {
801     long contentLength = C.LENGTH_UNSET;
802     Map<String, List<String>> headers = info.getAllHeaders();
803     List<String> contentLengthHeaders = headers.get("Content-Length");
804     String contentLengthHeader = null;
805     if (!isEmpty(contentLengthHeaders)) {
806       contentLengthHeader = contentLengthHeaders.get(0);
807       if (!TextUtils.isEmpty(contentLengthHeader)) {
808         try {
809           contentLength = Long.parseLong(contentLengthHeader);
810         } catch (NumberFormatException e) {
811           Log.e(TAG, "Unexpected Content-Length [" + contentLengthHeader + "]");
812         }
813       }
814     }
815     List<String> contentRangeHeaders = headers.get("Content-Range");
816     if (!isEmpty(contentRangeHeaders)) {
817       String contentRangeHeader = contentRangeHeaders.get(0);
818       Matcher matcher = CONTENT_RANGE_HEADER_PATTERN.matcher(contentRangeHeader);
819       if (matcher.find()) {
820         try {
821           long contentLengthFromRange =
822               Long.parseLong(Assertions.checkNotNull(matcher.group(2)))
823                   - Long.parseLong(Assertions.checkNotNull(matcher.group(1)))
824                   + 1;
825           if (contentLength < 0) {
826             // Some proxy servers strip the Content-Length header. Fall back to the length
827             // calculated here in this case.
828             contentLength = contentLengthFromRange;
829           } else if (contentLength != contentLengthFromRange) {
830             // If there is a discrepancy between the Content-Length and Content-Range headers,
831             // assume the one with the larger value is correct. We have seen cases where carrier
832             // change one of them to reduce the size of a request, but it is unlikely anybody
833             // would increase it.
834             Log.w(TAG, "Inconsistent headers [" + contentLengthHeader + "] [" + contentRangeHeader
835                 + "]");
836             contentLength = Math.max(contentLength, contentLengthFromRange);
837           }
838         } catch (NumberFormatException e) {
839           Log.e(TAG, "Unexpected Content-Range [" + contentRangeHeader + "]");
840         }
841       }
842     }
843     return contentLength;
844   }
845 
parseCookies(List<String> setCookieHeaders)846   private static String parseCookies(List<String> setCookieHeaders) {
847     return TextUtils.join(";", setCookieHeaders);
848   }
849 
attachCookies(UrlRequest.Builder requestBuilder, String cookies)850   private static void attachCookies(UrlRequest.Builder requestBuilder, String cookies) {
851     if (TextUtils.isEmpty(cookies)) {
852       return;
853     }
854     requestBuilder.addHeader(COOKIE, cookies);
855   }
856 
getStatus(UrlRequest request)857   private static int getStatus(UrlRequest request) throws InterruptedException {
858     final ConditionVariable conditionVariable = new ConditionVariable();
859     final int[] statusHolder = new int[1];
860     request.getStatus(new UrlRequest.StatusListener() {
861       @Override
862       public void onStatus(int status) {
863         statusHolder[0] = status;
864         conditionVariable.open();
865       }
866     });
867     conditionVariable.block();
868     return statusHolder[0];
869   }
870 
871   @EnsuresNonNullIf(result = false, expression = "#1")
isEmpty(@ullable List<?> list)872   private static boolean isEmpty(@Nullable List<?> list) {
873     return list == null || list.isEmpty();
874   }
875 
876   // Copy as much as possible from the src buffer into dst buffer.
877   // Returns the number of bytes copied.
copyByteBuffer(ByteBuffer src, ByteBuffer dst)878   private static int copyByteBuffer(ByteBuffer src, ByteBuffer dst) {
879     int remaining = Math.min(src.remaining(), dst.remaining());
880     int limit = src.limit();
881     src.limit(src.position() + remaining);
882     dst.put(src);
883     src.limit(limit);
884     return remaining;
885   }
886 
887   private final class UrlRequestCallback extends UrlRequest.Callback {
888 
889     @Override
onRedirectReceived( UrlRequest request, UrlResponseInfo info, String newLocationUrl)890     public synchronized void onRedirectReceived(
891         UrlRequest request, UrlResponseInfo info, String newLocationUrl) {
892       if (request != currentUrlRequest) {
893         return;
894       }
895       UrlRequest urlRequest = Assertions.checkNotNull(currentUrlRequest);
896       DataSpec dataSpec = Assertions.checkNotNull(currentDataSpec);
897       if (dataSpec.httpMethod == DataSpec.HTTP_METHOD_POST) {
898         int responseCode = info.getHttpStatusCode();
899         // The industry standard is to disregard POST redirects when the status code is 307 or 308.
900         if (responseCode == 307 || responseCode == 308) {
901           exception =
902               new InvalidResponseCodeException(
903                   responseCode, info.getHttpStatusText(), info.getAllHeaders(), dataSpec);
904           operation.open();
905           return;
906         }
907       }
908       if (resetTimeoutOnRedirects) {
909         resetConnectTimeout();
910       }
911 
912       if (!handleSetCookieRequests) {
913         request.followRedirect();
914         return;
915       }
916 
917       List<String> setCookieHeaders = info.getAllHeaders().get(SET_COOKIE);
918       if (isEmpty(setCookieHeaders)) {
919         request.followRedirect();
920         return;
921       }
922 
923       urlRequest.cancel();
924       DataSpec redirectUrlDataSpec;
925       if (dataSpec.httpMethod == DataSpec.HTTP_METHOD_POST) {
926         // For POST redirects that aren't 307 or 308, the redirect is followed but request is
927         // transformed into a GET.
928         redirectUrlDataSpec =
929             dataSpec
930                 .buildUpon()
931                 .setUri(newLocationUrl)
932                 .setHttpMethod(DataSpec.HTTP_METHOD_GET)
933                 .setHttpBody(null)
934                 .build();
935       } else {
936         redirectUrlDataSpec = dataSpec.withUri(Uri.parse(newLocationUrl));
937       }
938       UrlRequest.Builder requestBuilder;
939       try {
940         requestBuilder = buildRequestBuilder(redirectUrlDataSpec);
941       } catch (IOException e) {
942         exception = e;
943         return;
944       }
945       String cookieHeadersValue = parseCookies(setCookieHeaders);
946       attachCookies(requestBuilder, cookieHeadersValue);
947       currentUrlRequest = requestBuilder.build();
948       currentUrlRequest.start();
949     }
950 
951     @Override
onResponseStarted(UrlRequest request, UrlResponseInfo info)952     public synchronized void onResponseStarted(UrlRequest request, UrlResponseInfo info) {
953       if (request != currentUrlRequest) {
954         return;
955       }
956       responseInfo = info;
957       operation.open();
958     }
959 
960     @Override
onReadCompleted( UrlRequest request, UrlResponseInfo info, ByteBuffer buffer)961     public synchronized void onReadCompleted(
962         UrlRequest request, UrlResponseInfo info, ByteBuffer buffer) {
963       if (request != currentUrlRequest) {
964         return;
965       }
966       operation.open();
967     }
968 
969     @Override
onSucceeded(UrlRequest request, UrlResponseInfo info)970     public synchronized void onSucceeded(UrlRequest request, UrlResponseInfo info) {
971       if (request != currentUrlRequest) {
972         return;
973       }
974       finished = true;
975       operation.open();
976     }
977 
978     @Override
onFailed( UrlRequest request, UrlResponseInfo info, CronetException error)979     public synchronized void onFailed(
980         UrlRequest request, UrlResponseInfo info, CronetException error) {
981       if (request != currentUrlRequest) {
982         return;
983       }
984       if (error instanceof NetworkException
985           && ((NetworkException) error).getErrorCode()
986               == NetworkException.ERROR_HOSTNAME_NOT_RESOLVED) {
987         exception = new UnknownHostException();
988       } else {
989         exception = error;
990       }
991       operation.open();
992     }
993   }
994 }
995