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