@pigeonmal/react-native-video 7.0.0-beta.16 → 7.0.0-beta.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1246 @@
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 androidx.media3.datasource.cronet;
17
+
18
+ import static androidx.media3.common.util.Util.castNonNull;
19
+ import static androidx.media3.datasource.HttpUtil.buildRangeRequestHeader;
20
+ import static org.chromium.net.UrlRequest.Builder.REQUEST_PRIORITY_MEDIUM;
21
+
22
+ import android.net.Uri;
23
+ import android.text.TextUtils;
24
+ import androidx.annotation.Nullable;
25
+ import androidx.annotation.VisibleForTesting;
26
+ import androidx.media3.common.C;
27
+ import androidx.media3.common.MediaLibraryInfo;
28
+ import androidx.media3.common.PlaybackException;
29
+ import androidx.media3.common.util.Assertions;
30
+ import androidx.media3.common.util.Clock;
31
+ import androidx.media3.common.util.ConditionVariable;
32
+ import androidx.media3.common.util.UnstableApi;
33
+ import androidx.media3.common.util.Util;
34
+ import androidx.media3.datasource.BaseDataSource;
35
+ import androidx.media3.datasource.DataSource;
36
+ import androidx.media3.datasource.DataSourceException;
37
+ import androidx.media3.datasource.DataSpec;
38
+ import androidx.media3.datasource.DefaultHttpDataSource;
39
+ import androidx.media3.datasource.HttpDataSource;
40
+ import androidx.media3.datasource.HttpUtil;
41
+ import androidx.media3.datasource.TransferListener;
42
+ import com.google.common.base.Ascii;
43
+ import com.google.common.base.Predicate;
44
+ import com.google.common.net.HttpHeaders;
45
+ import com.google.common.primitives.Longs;
46
+ import java.io.IOException;
47
+ import java.io.InterruptedIOException;
48
+ import java.net.CookieHandler;
49
+ import java.net.CookieManager;
50
+ import java.net.SocketTimeoutException;
51
+ import java.net.UnknownHostException;
52
+ import java.nio.ByteBuffer;
53
+ import java.util.Arrays;
54
+ import java.util.Collections;
55
+ import java.util.HashMap;
56
+ import java.util.List;
57
+ import java.util.Map;
58
+ import java.util.Map.Entry;
59
+ import java.util.concurrent.Executor;
60
+ import org.chromium.net.CronetEngine;
61
+ import org.chromium.net.CronetException;
62
+ import org.chromium.net.NetworkException;
63
+ import org.chromium.net.UrlRequest;
64
+ import org.chromium.net.UrlRequest.Status;
65
+ import org.chromium.net.UrlResponseInfo;
66
+
67
+ /**
68
+ * DataSource without intermediate buffer based on Cronet API set using UrlRequest.
69
+ *
70
+ * <p>Note: HTTP request headers will be set using all parameters passed via (in order of decreasing
71
+ * priority) the {@code dataSpec}, {@link #setRequestProperty} and the default parameters used to
72
+ * construct the instance.
73
+ */
74
+ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
75
+
76
+ private static final String TAG = "CronetDataSource";
77
+
78
+ static {
79
+ MediaLibraryInfo.registerModule("media3.datasource.cronet");
80
+ }
81
+
82
+ /** {@link DataSource.Factory} for {@link CronetDataSource} instances. */
83
+ public static final class Factory implements HttpDataSource.Factory {
84
+
85
+ // TODO: Remove @Nullable annotation when CronetEngineWrapper is deleted.
86
+ @Nullable private final CronetEngine cronetEngine;
87
+ private final Executor executor;
88
+ private final RequestProperties defaultRequestProperties;
89
+ // TODO: Remove when CronetEngineWrapper is deleted.
90
+ @Nullable private final DefaultHttpDataSource.Factory internalFallbackFactory;
91
+
92
+ // TODO: Remove when CronetEngineWrapper is deleted.
93
+ @Nullable private HttpDataSource.Factory fallbackFactory;
94
+ @Nullable private Predicate<String> contentTypePredicate;
95
+ @Nullable private TransferListener transferListener;
96
+ @Nullable private String userAgent;
97
+ private int requestPriority;
98
+ private int connectTimeoutMs;
99
+ private int readTimeoutMs;
100
+ private int readBufferSize;
101
+ private boolean resetTimeoutOnRedirects;
102
+ private boolean handleSetCookieRequests;
103
+ private boolean keepPostFor302Redirects;
104
+
105
+ /**
106
+ * Creates an instance.
107
+ *
108
+ * @param cronetEngine A {@link CronetEngine} to make the requests. This should <em>not</em> be
109
+ * a fallback instance obtained from {@code JavaCronetProvider}. It's more efficient to use
110
+ * {@link DefaultHttpDataSource} instead in this case.
111
+ * @param executor The {@link java.util.concurrent.Executor} that will handle responses. This
112
+ * may be a direct executor (i.e. executes tasks on the calling thread) in order to avoid a
113
+ * thread hop from Cronet's internal network thread to the response handling thread.
114
+ * However, to avoid slowing down overall network performance, care must be taken to make
115
+ * sure response handling is a fast operation when using a direct executor.
116
+ */
117
+ public Factory(CronetEngine cronetEngine, Executor executor) {
118
+ this.cronetEngine = Assertions.checkNotNull(cronetEngine);
119
+ this.executor = executor;
120
+ defaultRequestProperties = new RequestProperties();
121
+ internalFallbackFactory = null;
122
+ requestPriority = REQUEST_PRIORITY_MEDIUM;
123
+ connectTimeoutMs = DEFAULT_CONNECT_TIMEOUT_MILLIS;
124
+ readTimeoutMs = DEFAULT_READ_TIMEOUT_MILLIS;
125
+ readBufferSize = DEFAULT_READ_BUFFER_SIZE_BYTES;
126
+ }
127
+
128
+ /**
129
+ * Creates an instance.
130
+ *
131
+ * @param cronetEngineWrapper A {@link CronetEngineWrapper}.
132
+ * @param executor The {@link java.util.concurrent.Executor} that will handle responses. This
133
+ * may be a direct executor (i.e. executes tasks on the calling thread) in order to avoid a
134
+ * thread hop from Cronet's internal network thread to the response handling thread.
135
+ * However, to avoid slowing down overall network performance, care must be taken to make
136
+ * sure response handling is a fast operation when using a direct executor.
137
+ * @deprecated Use {@link #Factory(CronetEngine, Executor)} with an instantiated {@link
138
+ * CronetEngine}, or {@link DefaultHttpDataSource} for cases where {@link
139
+ * CronetEngineWrapper#getCronetEngine()} would have returned {@code null}.
140
+ */
141
+ @UnstableApi
142
+ @SuppressWarnings("deprecation") // Intentionally using deprecated parameter
143
+ @Deprecated
144
+ public Factory(CronetEngineWrapper cronetEngineWrapper, Executor executor) {
145
+ this.cronetEngine = cronetEngineWrapper.getCronetEngine();
146
+ this.executor = executor;
147
+ defaultRequestProperties = new RequestProperties();
148
+ internalFallbackFactory = new DefaultHttpDataSource.Factory();
149
+ connectTimeoutMs = DEFAULT_CONNECT_TIMEOUT_MILLIS;
150
+ readTimeoutMs = DEFAULT_READ_TIMEOUT_MILLIS;
151
+ readBufferSize = DEFAULT_READ_BUFFER_SIZE_BYTES;
152
+ }
153
+
154
+ @UnstableApi
155
+ @Override
156
+ public final Factory setDefaultRequestProperties(Map<String, String> defaultRequestProperties) {
157
+ this.defaultRequestProperties.clearAndSet(defaultRequestProperties);
158
+ if (internalFallbackFactory != null) {
159
+ internalFallbackFactory.setDefaultRequestProperties(defaultRequestProperties);
160
+ }
161
+ return this;
162
+ }
163
+
164
+ /**
165
+ * Sets the user agent that will be used.
166
+ *
167
+ * <p>The default is {@code null}, which causes the default user agent of the underlying {@link
168
+ * CronetEngine} to be used.
169
+ *
170
+ * @param userAgent The user agent that will be used, or {@code null} to use the default user
171
+ * agent of the underlying {@link CronetEngine}.
172
+ * @return This factory.
173
+ */
174
+ public Factory setUserAgent(@Nullable String userAgent) {
175
+ this.userAgent = userAgent;
176
+ if (internalFallbackFactory != null) {
177
+ internalFallbackFactory.setUserAgent(userAgent);
178
+ }
179
+ return this;
180
+ }
181
+
182
+ /**
183
+ * Sets the priority of requests made by {@link CronetDataSource} instances created by this
184
+ * factory.
185
+ *
186
+ * <p>The default is {@link UrlRequest.Builder#REQUEST_PRIORITY_MEDIUM}.
187
+ *
188
+ * @param requestPriority The request priority, which should be one of Cronet's {@code
189
+ * UrlRequest.Builder#REQUEST_PRIORITY_*} constants.
190
+ * @return This factory.
191
+ */
192
+ @UnstableApi
193
+ public Factory setRequestPriority(int requestPriority) {
194
+ this.requestPriority = requestPriority;
195
+ return this;
196
+ }
197
+
198
+ /**
199
+ * Sets the connect timeout, in milliseconds.
200
+ *
201
+ * <p>The default is {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS}.
202
+ *
203
+ * @param connectTimeoutMs The connect timeout, in milliseconds, that will be used.
204
+ * @return This factory.
205
+ */
206
+ @UnstableApi
207
+ public Factory setConnectionTimeoutMs(int connectTimeoutMs) {
208
+ this.connectTimeoutMs = connectTimeoutMs;
209
+ if (internalFallbackFactory != null) {
210
+ internalFallbackFactory.setConnectTimeoutMs(connectTimeoutMs);
211
+ }
212
+ return this;
213
+ }
214
+
215
+ /**
216
+ * Sets whether the connect timeout is reset when a redirect occurs.
217
+ *
218
+ * <p>The default is {@code false}.
219
+ *
220
+ * @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
221
+ * @return This factory.
222
+ */
223
+ @UnstableApi
224
+ public Factory setResetTimeoutOnRedirects(boolean resetTimeoutOnRedirects) {
225
+ this.resetTimeoutOnRedirects = resetTimeoutOnRedirects;
226
+ return this;
227
+ }
228
+
229
+ /**
230
+ * Sets whether "Set-Cookie" requests on redirect should be forwarded to the redirect url in the
231
+ * "Cookie" header.
232
+ *
233
+ * <p>The default is {@code false}.
234
+ *
235
+ * @param handleSetCookieRequests Whether "Set-Cookie" requests on redirect should be forwarded
236
+ * to the redirect url in the "Cookie" header.
237
+ * @return This factory.
238
+ */
239
+ @UnstableApi
240
+ public Factory setHandleSetCookieRequests(boolean handleSetCookieRequests) {
241
+ this.handleSetCookieRequests = handleSetCookieRequests;
242
+ return this;
243
+ }
244
+
245
+ /**
246
+ * Sets the read timeout, in milliseconds.
247
+ *
248
+ * <p>The default is {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS}.
249
+ *
250
+ * @param readTimeoutMs The connect timeout, in milliseconds, that will be used.
251
+ * @return This factory.
252
+ */
253
+ @UnstableApi
254
+ public Factory setReadTimeoutMs(int readTimeoutMs) {
255
+ this.readTimeoutMs = readTimeoutMs;
256
+ if (internalFallbackFactory != null) {
257
+ internalFallbackFactory.setReadTimeoutMs(readTimeoutMs);
258
+ }
259
+ return this;
260
+ }
261
+
262
+ /**
263
+ * Sets a content type {@link Predicate}. If a content type is rejected by the predicate then a
264
+ * {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link #open(DataSpec)}.
265
+ *
266
+ * <p>The default is {@code null}.
267
+ *
268
+ * @param contentTypePredicate The content type {@link Predicate}, or {@code null} to clear a
269
+ * predicate that was previously set.
270
+ * @return This factory.
271
+ */
272
+ @UnstableApi
273
+ public Factory setContentTypePredicate(@Nullable Predicate<String> contentTypePredicate) {
274
+ this.contentTypePredicate = contentTypePredicate;
275
+ if (internalFallbackFactory != null) {
276
+ internalFallbackFactory.setContentTypePredicate(contentTypePredicate);
277
+ }
278
+ return this;
279
+ }
280
+
281
+ /**
282
+ * Sets whether we should keep the POST method and body when we have HTTP 302 redirects for a
283
+ * POST request.
284
+ */
285
+ @UnstableApi
286
+ public Factory setKeepPostFor302Redirects(boolean keepPostFor302Redirects) {
287
+ this.keepPostFor302Redirects = keepPostFor302Redirects;
288
+ if (internalFallbackFactory != null) {
289
+ internalFallbackFactory.setKeepPostFor302Redirects(keepPostFor302Redirects);
290
+ }
291
+ return this;
292
+ }
293
+
294
+ /**
295
+ * Sets the {@link TransferListener} that will be used.
296
+ *
297
+ * <p>The default is {@code null}.
298
+ *
299
+ * <p>See {@link DataSource#addTransferListener(TransferListener)}.
300
+ *
301
+ * @param transferListener The listener that will be used.
302
+ * @return This factory.
303
+ */
304
+ @UnstableApi
305
+ public Factory setTransferListener(@Nullable TransferListener transferListener) {
306
+ this.transferListener = transferListener;
307
+ if (internalFallbackFactory != null) {
308
+ internalFallbackFactory.setTransferListener(transferListener);
309
+ }
310
+ return this;
311
+ }
312
+
313
+ /**
314
+ * Sets the fallback {@link HttpDataSource.Factory} that is used as a fallback if the {@link
315
+ * CronetEngineWrapper} fails to provide a {@link CronetEngine}.
316
+ *
317
+ * <p>By default a {@link DefaultHttpDataSource} is used as fallback factory.
318
+ *
319
+ * @param fallbackFactory The fallback factory that will be used.
320
+ * @return This factory.
321
+ * @deprecated Do not use {@link CronetDataSource} or its factory in cases where a suitable
322
+ * {@link CronetEngine} is not available. Use the fallback factory directly in such cases.
323
+ */
324
+ @SuppressWarnings("deprecation") // Intentionally referring to deprecated parameter
325
+ @UnstableApi
326
+ @Deprecated
327
+ public Factory setFallbackFactory(@Nullable HttpDataSource.Factory fallbackFactory) {
328
+ this.fallbackFactory = fallbackFactory;
329
+ return this;
330
+ }
331
+
332
+ /**
333
+ * Sets the read buffer size, in bytes.
334
+ *
335
+ * @param readBufferSize The read buffer size, in bytes.
336
+ * @return This factory.
337
+ */
338
+ @UnstableApi
339
+ public Factory setReadBufferSize(int readBufferSize) {
340
+ this.readBufferSize = readBufferSize;
341
+ return this;
342
+ }
343
+
344
+ @UnstableApi
345
+ @Override
346
+ public HttpDataSource createDataSource() {
347
+ if (cronetEngine == null) {
348
+ return (fallbackFactory != null)
349
+ ? fallbackFactory.createDataSource()
350
+ : Assertions.checkNotNull(internalFallbackFactory).createDataSource();
351
+ }
352
+ CronetDataSource dataSource =
353
+ new CronetDataSource(
354
+ cronetEngine,
355
+ executor,
356
+ requestPriority,
357
+ connectTimeoutMs,
358
+ readTimeoutMs,
359
+ resetTimeoutOnRedirects,
360
+ handleSetCookieRequests,
361
+ userAgent,
362
+ defaultRequestProperties,
363
+ contentTypePredicate,
364
+ keepPostFor302Redirects,
365
+ readBufferSize);
366
+ if (transferListener != null) {
367
+ dataSource.addTransferListener(transferListener);
368
+ }
369
+ return dataSource;
370
+ }
371
+ }
372
+
373
+ /** Thrown when an error is encountered when trying to open a {@link CronetDataSource}. */
374
+ @UnstableApi
375
+ public static final class OpenException extends HttpDataSourceException {
376
+
377
+ /**
378
+ * Returns the status of the connection establishment at the moment when the error occurred, as
379
+ * defined by {@link UrlRequest.Status}.
380
+ */
381
+ public final int cronetConnectionStatus;
382
+
383
+ /**
384
+ * @deprecated Use {@link #OpenException(IOException, DataSpec, int, int)}.
385
+ */
386
+ @Deprecated
387
+ public OpenException(IOException cause, DataSpec dataSpec, int cronetConnectionStatus) {
388
+ super(cause, dataSpec, PlaybackException.ERROR_CODE_IO_UNSPECIFIED, TYPE_OPEN);
389
+ this.cronetConnectionStatus = cronetConnectionStatus;
390
+ }
391
+
392
+ public OpenException(
393
+ IOException cause,
394
+ DataSpec dataSpec,
395
+ @PlaybackException.ErrorCode int errorCode,
396
+ int cronetConnectionStatus) {
397
+ super(cause, dataSpec, errorCode, TYPE_OPEN);
398
+ this.cronetConnectionStatus = cronetConnectionStatus;
399
+ }
400
+
401
+ /**
402
+ * @deprecated Use {@link #OpenException(String, DataSpec, int, int)}.
403
+ */
404
+ @Deprecated
405
+ public OpenException(String errorMessage, DataSpec dataSpec, int cronetConnectionStatus) {
406
+ super(errorMessage, dataSpec, PlaybackException.ERROR_CODE_IO_UNSPECIFIED, TYPE_OPEN);
407
+ this.cronetConnectionStatus = cronetConnectionStatus;
408
+ }
409
+
410
+ public OpenException(
411
+ String errorMessage,
412
+ DataSpec dataSpec,
413
+ @PlaybackException.ErrorCode int errorCode,
414
+ int cronetConnectionStatus) {
415
+ super(errorMessage, dataSpec, errorCode, TYPE_OPEN);
416
+ this.cronetConnectionStatus = cronetConnectionStatus;
417
+ }
418
+
419
+ public OpenException(
420
+ DataSpec dataSpec, @PlaybackException.ErrorCode int errorCode, int cronetConnectionStatus) {
421
+ super(dataSpec, errorCode, TYPE_OPEN);
422
+ this.cronetConnectionStatus = cronetConnectionStatus;
423
+ }
424
+ }
425
+
426
+ /** The default connection timeout, in milliseconds. */
427
+ @UnstableApi public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS = 8 * 1000;
428
+
429
+ /** The default read timeout, in milliseconds. */
430
+ @UnstableApi public static final int DEFAULT_READ_TIMEOUT_MILLIS = 8 * 1000;
431
+
432
+ // The size of read buffer passed to cronet UrlRequest.read().
433
+ private static final int DEFAULT_READ_BUFFER_SIZE_BYTES = 32 * 1024;
434
+
435
+ private final CronetEngine cronetEngine;
436
+ private final Executor executor;
437
+ private final int requestPriority;
438
+ private final int connectTimeoutMs;
439
+ private final int readTimeoutMs;
440
+ private final boolean resetTimeoutOnRedirects;
441
+ private final boolean handleSetCookieRequests;
442
+ @Nullable private final String userAgent;
443
+ @Nullable private final RequestProperties defaultRequestProperties;
444
+ private final RequestProperties requestProperties;
445
+ private final ConditionVariable operation;
446
+ private final Clock clock;
447
+ private final int readBufferSize;
448
+
449
+ @Nullable private final Predicate<String> contentTypePredicate;
450
+ private final boolean keepPostFor302Redirects;
451
+
452
+ // Accessed by the calling thread only.
453
+ private boolean transferStarted;
454
+ private long bytesRemaining;
455
+
456
+ // Written from the calling thread only. currentUrlRequest.start() calls ensure writes are visible
457
+ // to reads made by the Cronet thread.
458
+ @Nullable private UrlRequest currentUrlRequest;
459
+ @VisibleForTesting @Nullable /* package */ UrlRequestCallback currentUrlRequestCallback;
460
+ @Nullable private DataSpec currentDataSpec;
461
+
462
+ // Reference written and read by calling thread only. Passed to Cronet thread as a local variable.
463
+ // operation.open() calls ensure writes into the buffer are visible to reads made by the calling
464
+ // thread.
465
+ @Nullable private ByteBuffer readBuffer;
466
+
467
+ // Written from the Cronet thread only. operation.open() calls ensure writes are visible to reads
468
+ // made by the calling thread.
469
+ @Nullable private UrlResponseInfo responseInfo;
470
+ @Nullable private IOException exception;
471
+ private boolean finished;
472
+
473
+ private volatile long currentConnectTimeoutMs;
474
+
475
+ @UnstableApi
476
+ protected CronetDataSource(
477
+ CronetEngine cronetEngine,
478
+ Executor executor,
479
+ int requestPriority,
480
+ int connectTimeoutMs,
481
+ int readTimeoutMs,
482
+ boolean resetTimeoutOnRedirects,
483
+ boolean handleSetCookieRequests,
484
+ @Nullable String userAgent,
485
+ @Nullable RequestProperties defaultRequestProperties,
486
+ @Nullable Predicate<String> contentTypePredicate,
487
+ boolean keepPostFor302Redirects,
488
+ int readBufferSize) {
489
+ super(/* isNetwork= */ true);
490
+ this.cronetEngine = Assertions.checkNotNull(cronetEngine);
491
+ this.executor = Assertions.checkNotNull(executor);
492
+ this.requestPriority = requestPriority;
493
+ this.connectTimeoutMs = connectTimeoutMs;
494
+ this.readTimeoutMs = readTimeoutMs;
495
+ this.resetTimeoutOnRedirects = resetTimeoutOnRedirects;
496
+ this.handleSetCookieRequests = handleSetCookieRequests;
497
+ this.userAgent = userAgent;
498
+ this.defaultRequestProperties = defaultRequestProperties;
499
+ this.contentTypePredicate = contentTypePredicate;
500
+ this.keepPostFor302Redirects = keepPostFor302Redirects;
501
+ clock = Clock.DEFAULT;
502
+ this.readBufferSize = readBufferSize;
503
+ requestProperties = new RequestProperties();
504
+ operation = new ConditionVariable();
505
+ }
506
+
507
+ // HttpDataSource implementation.
508
+
509
+ @UnstableApi
510
+ @Override
511
+ public void setRequestProperty(String name, String value) {
512
+ requestProperties.set(name, value);
513
+ }
514
+
515
+ @UnstableApi
516
+ @Override
517
+ public void clearRequestProperty(String name) {
518
+ requestProperties.remove(name);
519
+ }
520
+
521
+ @UnstableApi
522
+ @Override
523
+ public void clearAllRequestProperties() {
524
+ requestProperties.clear();
525
+ }
526
+
527
+ @UnstableApi
528
+ @Override
529
+ public int getResponseCode() {
530
+ return responseInfo == null || responseInfo.getHttpStatusCode() <= 0
531
+ ? -1
532
+ : responseInfo.getHttpStatusCode();
533
+ }
534
+
535
+ @UnstableApi
536
+ @Override
537
+ public Map<String, List<String>> getResponseHeaders() {
538
+ return responseInfo == null ? Collections.emptyMap() : responseInfo.getAllHeaders();
539
+ }
540
+
541
+ @UnstableApi
542
+ @Override
543
+ @Nullable
544
+ public Uri getUri() {
545
+ if (responseInfo != null) {
546
+ return Uri.parse(responseInfo.getUrl());
547
+ } else if (currentDataSpec != null) {
548
+ return currentDataSpec.uri;
549
+ } else {
550
+ return null;
551
+ }
552
+ }
553
+
554
+ @UnstableApi
555
+ @Override
556
+ public long open(DataSpec dataSpec) throws HttpDataSourceException {
557
+ Assertions.checkNotNull(dataSpec);
558
+ Assertions.checkState(!transferStarted);
559
+
560
+ operation.close();
561
+ resetConnectTimeout();
562
+ currentDataSpec = dataSpec;
563
+ UrlRequest urlRequest;
564
+ try {
565
+ createCurrentUrlRequestAndCallback(dataSpec);
566
+ urlRequest = currentUrlRequest;
567
+ } catch (IOException e) {
568
+ if (e instanceof HttpDataSourceException) {
569
+ throw (HttpDataSourceException) e;
570
+ } else {
571
+ throw new OpenException(
572
+ e, dataSpec, PlaybackException.ERROR_CODE_IO_UNSPECIFIED, Status.IDLE);
573
+ }
574
+ }
575
+ urlRequest.start();
576
+
577
+ transferInitializing(dataSpec);
578
+ try {
579
+ boolean connectionOpened = blockUntilConnectTimeout();
580
+ @Nullable IOException connectionOpenException = exception;
581
+ if (connectionOpenException != null) {
582
+ @Nullable String message = connectionOpenException.getMessage();
583
+ if (message != null && Ascii.toLowerCase(message).contains("err_cleartext_not_permitted")) {
584
+ throw new CleartextNotPermittedException(connectionOpenException, dataSpec);
585
+ }
586
+ throw new OpenException(
587
+ connectionOpenException,
588
+ dataSpec,
589
+ PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED,
590
+ getStatus(urlRequest));
591
+ } else if (!connectionOpened) {
592
+ // The timeout was reached before the connection was opened.
593
+ throw new OpenException(
594
+ new SocketTimeoutException(),
595
+ dataSpec,
596
+ PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT,
597
+ getStatus(urlRequest));
598
+ }
599
+ } catch (InterruptedException e) {
600
+ Thread.currentThread().interrupt();
601
+ // An interruption means the operation is being cancelled, in which case this exception should
602
+ // not cause the player to fail. If it does, it likely means that the owner of the operation
603
+ // is failing to swallow the interruption, which makes us enter an invalid state.
604
+ throw new OpenException(
605
+ new InterruptedIOException(),
606
+ dataSpec,
607
+ PlaybackException.ERROR_CODE_FAILED_RUNTIME_CHECK,
608
+ Status.INVALID);
609
+ }
610
+
611
+ // Check for a valid response code.
612
+ UrlResponseInfo responseInfo = Assertions.checkNotNull(this.responseInfo);
613
+ int responseCode = responseInfo.getHttpStatusCode();
614
+ Map<String, List<String>> responseHeaders = responseInfo.getAllHeaders();
615
+ if (responseCode < 200 || responseCode > 299) {
616
+ if (responseCode == 416) {
617
+ long documentSize =
618
+ HttpUtil.getDocumentSize(getFirstHeader(responseHeaders, HttpHeaders.CONTENT_RANGE));
619
+ if (dataSpec.position == documentSize) {
620
+ transferStarted = true;
621
+ transferStarted(dataSpec);
622
+ return dataSpec.length != C.LENGTH_UNSET ? dataSpec.length : 0;
623
+ }
624
+ }
625
+
626
+ byte[] responseBody;
627
+ try {
628
+ responseBody = readResponseBody();
629
+ } catch (IOException e) {
630
+ responseBody = Util.EMPTY_BYTE_ARRAY;
631
+ }
632
+
633
+ @Nullable
634
+ IOException cause =
635
+ responseCode == 416
636
+ ? new DataSourceException(PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE)
637
+ : null;
638
+ throw new InvalidResponseCodeException(
639
+ responseCode,
640
+ responseInfo.getHttpStatusText(),
641
+ cause,
642
+ responseHeaders,
643
+ dataSpec,
644
+ responseBody);
645
+ }
646
+
647
+ // Check for a valid content type.
648
+ Predicate<String> contentTypePredicate = this.contentTypePredicate;
649
+ if (contentTypePredicate != null) {
650
+ @Nullable String contentType = getFirstHeader(responseHeaders, HttpHeaders.CONTENT_TYPE);
651
+ if (contentType != null && !contentTypePredicate.apply(contentType)) {
652
+ throw new InvalidContentTypeException(contentType, dataSpec);
653
+ }
654
+ }
655
+
656
+ // If we requested a range starting from a non-zero position and received a 200 rather than a
657
+ // 206, then the server does not support partial requests. We'll need to manually skip to the
658
+ // requested position.
659
+ long bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0;
660
+
661
+ // Calculate the content length.
662
+ if (!isCompressed(responseInfo)) {
663
+ if (dataSpec.length != C.LENGTH_UNSET) {
664
+ bytesRemaining = dataSpec.length;
665
+ } else {
666
+ long contentLength =
667
+ HttpUtil.getContentLength(
668
+ getFirstHeader(responseHeaders, HttpHeaders.CONTENT_LENGTH),
669
+ getFirstHeader(responseHeaders, HttpHeaders.CONTENT_RANGE));
670
+ bytesRemaining =
671
+ contentLength != C.LENGTH_UNSET ? (contentLength - bytesToSkip) : C.LENGTH_UNSET;
672
+ }
673
+ } else {
674
+ // If the response is compressed then the content length will be that of the compressed data
675
+ // which isn't what we want. Always use the dataSpec length in this case.
676
+ bytesRemaining = dataSpec.length;
677
+ }
678
+
679
+ transferStarted = true;
680
+ transferStarted(dataSpec);
681
+
682
+ skipFully(bytesToSkip, dataSpec);
683
+ return bytesRemaining;
684
+ }
685
+
686
+ @UnstableApi
687
+ @Override
688
+ public int read(byte[] buffer, int offset, int length) throws HttpDataSourceException {
689
+ Assertions.checkState(transferStarted);
690
+
691
+ if (length == 0) {
692
+ return 0;
693
+ } else if (bytesRemaining == 0) {
694
+ return C.RESULT_END_OF_INPUT;
695
+ }
696
+
697
+ ByteBuffer readBuffer = getOrCreateReadBuffer();
698
+ if (!readBuffer.hasRemaining()) {
699
+ // Fill readBuffer with more data from Cronet.
700
+ operation.close();
701
+ readBuffer.clear();
702
+
703
+ readInternal(readBuffer, castNonNull(currentDataSpec));
704
+
705
+ if (finished) {
706
+ bytesRemaining = 0;
707
+ return C.RESULT_END_OF_INPUT;
708
+ }
709
+
710
+ // The operation didn't time out, fail or finish, and therefore data must have been read.
711
+ readBuffer.flip();
712
+ Assertions.checkState(readBuffer.hasRemaining());
713
+ }
714
+
715
+ // Ensure we read up to bytesRemaining, in case this was a Range request with finite end, but
716
+ // the server does not support Range requests and transmitted the entire resource.
717
+ int bytesRead =
718
+ (int)
719
+ Longs.min(
720
+ bytesRemaining != C.LENGTH_UNSET ? bytesRemaining : Long.MAX_VALUE,
721
+ readBuffer.remaining(),
722
+ length);
723
+
724
+ readBuffer.get(buffer, offset, bytesRead);
725
+
726
+ if (bytesRemaining != C.LENGTH_UNSET) {
727
+ bytesRemaining -= bytesRead;
728
+ }
729
+ bytesTransferred(bytesRead);
730
+ return bytesRead;
731
+ }
732
+
733
+ /**
734
+ * Reads up to {@code buffer.remaining()} bytes of data and stores them into {@code buffer},
735
+ * starting at {@code buffer.position()}. Advances the position of the buffer by the number of
736
+ * bytes read and returns this length.
737
+ *
738
+ * <p>If there is an error, a {@link HttpDataSourceException} is thrown and the contents of {@code
739
+ * buffer} should be ignored. If the exception has error code {@code
740
+ * HttpDataSourceException.TYPE_READ}, note that Cronet may continue writing into {@code buffer}
741
+ * after the method has returned. Thus the caller should not attempt to reuse the buffer.
742
+ *
743
+ * <p>If {@code buffer.remaining()} is zero then 0 is returned. Otherwise, if no data is available
744
+ * because the end of the opened range has been reached, then {@link C#RESULT_END_OF_INPUT} is
745
+ * returned. Otherwise, the call will block until at least one byte of data has been read and the
746
+ * number of bytes read is returned.
747
+ *
748
+ * <p>Passed buffer must be direct ByteBuffer. If you have a non-direct ByteBuffer, consider the
749
+ * alternative read method with its backed array.
750
+ *
751
+ * @param buffer The ByteBuffer into which the read data should be stored. Must be a direct
752
+ * ByteBuffer.
753
+ * @return The number of bytes read, or {@link C#RESULT_END_OF_INPUT} if no data is available
754
+ * because the end of the opened range has been reached.
755
+ * @throws HttpDataSourceException If an error occurs reading from the source.
756
+ * @throws IllegalArgumentException If {@code buffer} is not a direct ByteBuffer.
757
+ */
758
+ @UnstableApi
759
+ public int read(ByteBuffer buffer) throws HttpDataSourceException {
760
+ Assertions.checkState(transferStarted);
761
+
762
+ if (!buffer.isDirect()) {
763
+ throw new IllegalArgumentException("Passed buffer is not a direct ByteBuffer");
764
+ }
765
+ if (!buffer.hasRemaining()) {
766
+ return 0;
767
+ } else if (bytesRemaining == 0) {
768
+ return C.RESULT_END_OF_INPUT;
769
+ }
770
+ int readLength = buffer.remaining();
771
+
772
+ if (readBuffer != null) {
773
+ // If there is existing data in the readBuffer, read as much as possible. Return if any read.
774
+ int copyBytes = copyByteBuffer(/* src= */ readBuffer, /* dst= */ buffer);
775
+ if (copyBytes != 0) {
776
+ if (bytesRemaining != C.LENGTH_UNSET) {
777
+ bytesRemaining -= copyBytes;
778
+ }
779
+ bytesTransferred(copyBytes);
780
+ return copyBytes;
781
+ }
782
+ }
783
+
784
+ // Fill buffer with more data from Cronet.
785
+ operation.close();
786
+ readInternal(buffer, castNonNull(currentDataSpec));
787
+
788
+ if (finished) {
789
+ bytesRemaining = 0;
790
+ return C.RESULT_END_OF_INPUT;
791
+ }
792
+
793
+ // The operation didn't time out, fail or finish, and therefore data must have been read.
794
+ Assertions.checkState(readLength > buffer.remaining());
795
+ int bytesRead = readLength - buffer.remaining();
796
+ if (bytesRemaining != C.LENGTH_UNSET) {
797
+ bytesRemaining -= bytesRead;
798
+ }
799
+ bytesTransferred(bytesRead);
800
+ return bytesRead;
801
+ }
802
+
803
+ @UnstableApi
804
+ @Override
805
+ public synchronized void close() {
806
+ closeCurrentUrlRequestAndCallback();
807
+ if (readBuffer != null) {
808
+ readBuffer.limit(0);
809
+ }
810
+ currentDataSpec = null;
811
+ responseInfo = null;
812
+ exception = null;
813
+ finished = false;
814
+ if (transferStarted) {
815
+ transferStarted = false;
816
+ transferEnded();
817
+ }
818
+ }
819
+
820
+ private void closeCurrentUrlRequestAndCallback() {
821
+ if (currentUrlRequest != null) {
822
+ currentUrlRequest.cancel();
823
+ currentUrlRequest = null;
824
+ }
825
+
826
+ if (currentUrlRequestCallback != null) {
827
+ currentUrlRequestCallback.close();
828
+ currentUrlRequestCallback = null;
829
+ }
830
+ }
831
+
832
+ /** Returns current {@link UrlRequest}. May be null if the data source is not opened. */
833
+ @UnstableApi
834
+ @Nullable
835
+ protected UrlRequest getCurrentUrlRequest() {
836
+ return currentUrlRequest;
837
+ }
838
+
839
+ /** Returns current {@link UrlResponseInfo}. May be null if the data source is not opened. */
840
+ @UnstableApi
841
+ @Nullable
842
+ protected UrlResponseInfo getCurrentUrlResponseInfo() {
843
+ return responseInfo;
844
+ }
845
+
846
+ // The nullness checker can't prove that UrlRequest.Builder.build() doesn't null out
847
+ // this.currentUrlRequestCallback.
848
+ // TODO: Add @SideEffectFree to the UrlRequest.Builder.build() stub
849
+ @SuppressWarnings("nullness:contracts.postcondition.not.satisfied")
850
+ private void createCurrentUrlRequestAndCallback(DataSpec dataSpec) throws IOException {
851
+ currentUrlRequestCallback = new UrlRequestCallback();
852
+ currentUrlRequest = buildRequestBuilder(dataSpec).build();
853
+ }
854
+
855
+ /**
856
+ * Returns {@link UrlRequest.Builder} from dataSpec. Would not work if data source is not opened.
857
+ */
858
+ @UnstableApi
859
+ protected UrlRequest.Builder buildRequestBuilder(DataSpec dataSpec) throws IOException {
860
+ UrlRequest.Builder requestBuilder =
861
+ cronetEngine
862
+ .newUrlRequestBuilder(dataSpec.uri.toString(), currentUrlRequestCallback, executor)
863
+ .setPriority(requestPriority)
864
+ .allowDirectExecutor();
865
+
866
+ // Set the headers.
867
+ Map<String, String> requestHeaders = new HashMap<>();
868
+ if (defaultRequestProperties != null) {
869
+ requestHeaders.putAll(defaultRequestProperties.getSnapshot());
870
+ }
871
+ requestHeaders.putAll(requestProperties.getSnapshot());
872
+ requestHeaders.putAll(dataSpec.httpRequestHeaders);
873
+
874
+ for (Entry<String, String> headerEntry : requestHeaders.entrySet()) {
875
+ String key = headerEntry.getKey();
876
+ String value = headerEntry.getValue();
877
+ requestBuilder.addHeader(key, value);
878
+ }
879
+
880
+ if (dataSpec.httpBody != null && !requestHeaders.containsKey(HttpHeaders.CONTENT_TYPE)) {
881
+ throw new OpenException(
882
+ "HTTP request with non-empty body must set Content-Type",
883
+ dataSpec,
884
+ PlaybackException.ERROR_CODE_FAILED_RUNTIME_CHECK,
885
+ Status.IDLE);
886
+ }
887
+
888
+ @Nullable String rangeHeader = buildRangeRequestHeader(dataSpec.position, dataSpec.length);
889
+ if (rangeHeader != null) {
890
+ requestBuilder.addHeader(HttpHeaders.RANGE, rangeHeader);
891
+ }
892
+ if (userAgent != null) {
893
+ requestBuilder.addHeader(HttpHeaders.USER_AGENT, userAgent);
894
+ }
895
+ // TODO: Uncomment when https://bugs.chromium.org/p/chromium/issues/detail?id=711810 is fixed
896
+ // (adjusting the code as necessary).
897
+ // Force identity encoding unless gzip is allowed.
898
+ // if (!dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP)) {
899
+ // requestBuilder.addHeader("Accept-Encoding", "identity");
900
+ // }
901
+ // Set the method and (if non-empty) the body.
902
+ requestBuilder.setHttpMethod(dataSpec.getHttpMethodString());
903
+ if (dataSpec.httpBody != null) {
904
+ requestBuilder.setUploadDataProvider(
905
+ new ByteArrayUploadDataProvider(dataSpec.httpBody), executor);
906
+ }
907
+ return requestBuilder;
908
+ }
909
+
910
+ // Internal methods.
911
+
912
+ private boolean blockUntilConnectTimeout() throws InterruptedException {
913
+ long now = clock.elapsedRealtime();
914
+ boolean opened = false;
915
+ while (!opened && now < currentConnectTimeoutMs) {
916
+ opened = operation.block(currentConnectTimeoutMs - now + 5 /* fudge factor */);
917
+ now = clock.elapsedRealtime();
918
+ }
919
+ return opened;
920
+ }
921
+
922
+ private void resetConnectTimeout() {
923
+ currentConnectTimeoutMs = clock.elapsedRealtime() + connectTimeoutMs;
924
+ }
925
+
926
+ /**
927
+ * Attempts to skip the specified number of bytes in full.
928
+ *
929
+ * <p>The methods throws an {@link OpenException} with {@link OpenException#reason} set to {@link
930
+ * PlaybackException#ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE} when the data ended before the
931
+ * specified number of bytes were skipped.
932
+ *
933
+ * @param bytesToSkip The number of bytes to skip.
934
+ * @param dataSpec The {@link DataSpec}.
935
+ * @throws HttpDataSourceException If the thread is interrupted during the operation, or an error
936
+ * occurs reading from the source; or when the data ended before the specified number of bytes
937
+ * were skipped.
938
+ */
939
+ private void skipFully(long bytesToSkip, DataSpec dataSpec) throws HttpDataSourceException {
940
+ if (bytesToSkip == 0) {
941
+ return;
942
+ }
943
+ ByteBuffer readBuffer = getOrCreateReadBuffer();
944
+
945
+ try {
946
+ while (bytesToSkip > 0) {
947
+ // Fill readBuffer with more data from Cronet.
948
+ operation.close();
949
+ readBuffer.clear();
950
+ readInternal(readBuffer, dataSpec);
951
+ if (Thread.currentThread().isInterrupted()) {
952
+ throw new InterruptedIOException();
953
+ }
954
+ if (finished) {
955
+ throw new OpenException(
956
+ dataSpec,
957
+ PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE,
958
+ Status.READING_RESPONSE);
959
+ } else {
960
+ // The operation didn't time out, fail or finish, and therefore data must have been read.
961
+ readBuffer.flip();
962
+ Assertions.checkState(readBuffer.hasRemaining());
963
+ int bytesSkipped = (int) Math.min(readBuffer.remaining(), bytesToSkip);
964
+ readBuffer.position(readBuffer.position() + bytesSkipped);
965
+ bytesToSkip -= bytesSkipped;
966
+ }
967
+ }
968
+ } catch (IOException e) {
969
+ if (e instanceof HttpDataSourceException) {
970
+ throw (HttpDataSourceException) e;
971
+ } else {
972
+ throw new OpenException(
973
+ e,
974
+ dataSpec,
975
+ e instanceof SocketTimeoutException
976
+ ? PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT
977
+ : PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED,
978
+ Status.READING_RESPONSE);
979
+ }
980
+ }
981
+ }
982
+
983
+ /**
984
+ * Reads the whole response body.
985
+ *
986
+ * @return The response body.
987
+ * @throws IOException If an error occurs reading from the source.
988
+ */
989
+ private byte[] readResponseBody() throws IOException {
990
+ byte[] responseBody = Util.EMPTY_BYTE_ARRAY;
991
+ ByteBuffer readBuffer = getOrCreateReadBuffer();
992
+ while (!finished) {
993
+ operation.close();
994
+ readBuffer.clear();
995
+ readInternal(readBuffer, castNonNull(currentDataSpec));
996
+ readBuffer.flip();
997
+ if (readBuffer.remaining() > 0) {
998
+ int existingResponseBodyEnd = responseBody.length;
999
+ responseBody = Arrays.copyOf(responseBody, responseBody.length + readBuffer.remaining());
1000
+ readBuffer.get(responseBody, existingResponseBodyEnd, readBuffer.remaining());
1001
+ }
1002
+ }
1003
+ return responseBody;
1004
+ }
1005
+
1006
+ /**
1007
+ * Reads up to {@code buffer.remaining()} bytes of data from {@code currentUrlRequest} and stores
1008
+ * them into {@code buffer}. If there is an error and {@code buffer == readBuffer}, then it resets
1009
+ * the current {@code readBuffer} object so that it is not reused in the future.
1010
+ *
1011
+ * @param buffer The ByteBuffer into which the read data is stored. Must be a direct ByteBuffer.
1012
+ * @throws HttpDataSourceException If an error occurs reading from the source.
1013
+ */
1014
+ @SuppressWarnings("ReferenceEquality")
1015
+ private void readInternal(ByteBuffer buffer, DataSpec dataSpec) throws HttpDataSourceException {
1016
+ castNonNull(currentUrlRequest).read(buffer);
1017
+ try {
1018
+ if (!operation.block(readTimeoutMs)) {
1019
+ throw new SocketTimeoutException();
1020
+ }
1021
+ } catch (InterruptedException e) {
1022
+ // The operation is ongoing so replace buffer to avoid it being written to by this
1023
+ // operation during a subsequent request.
1024
+ if (buffer == readBuffer) {
1025
+ readBuffer = null;
1026
+ }
1027
+ Thread.currentThread().interrupt();
1028
+ exception = new InterruptedIOException();
1029
+ } catch (SocketTimeoutException e) {
1030
+ // The operation is ongoing so replace buffer to avoid it being written to by this
1031
+ // operation during a subsequent request.
1032
+ if (buffer == readBuffer) {
1033
+ readBuffer = null;
1034
+ }
1035
+ exception =
1036
+ new HttpDataSourceException(
1037
+ e,
1038
+ dataSpec,
1039
+ PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT,
1040
+ HttpDataSourceException.TYPE_READ);
1041
+ }
1042
+
1043
+ if (exception != null) {
1044
+ if (exception instanceof HttpDataSourceException) {
1045
+ throw (HttpDataSourceException) exception;
1046
+ } else {
1047
+ throw HttpDataSourceException.createForIOException(
1048
+ exception, dataSpec, HttpDataSourceException.TYPE_READ);
1049
+ }
1050
+ }
1051
+ }
1052
+
1053
+ private ByteBuffer getOrCreateReadBuffer() {
1054
+ if (readBuffer == null) {
1055
+ readBuffer = ByteBuffer.allocateDirect(readBufferSize);
1056
+ readBuffer.limit(0);
1057
+ }
1058
+ return readBuffer;
1059
+ }
1060
+
1061
+ private static boolean isCompressed(UrlResponseInfo info) {
1062
+ for (Map.Entry<String, String> entry : info.getAllHeadersAsList()) {
1063
+ if (entry.getKey().equalsIgnoreCase("Content-Encoding")) {
1064
+ return !entry.getValue().equalsIgnoreCase("identity");
1065
+ }
1066
+ }
1067
+ return false;
1068
+ }
1069
+
1070
+ private static int getStatus(UrlRequest request) throws InterruptedException {
1071
+ final ConditionVariable conditionVariable = new ConditionVariable();
1072
+ final int[] statusHolder = new int[1];
1073
+ request.getStatus(
1074
+ new UrlRequest.StatusListener() {
1075
+ @Override
1076
+ public void onStatus(int status) {
1077
+ statusHolder[0] = status;
1078
+ conditionVariable.open();
1079
+ }
1080
+ });
1081
+ conditionVariable.block();
1082
+ return statusHolder[0];
1083
+ }
1084
+
1085
+ @Nullable
1086
+ private static String getFirstHeader(Map<String, List<String>> allHeaders, String headerName) {
1087
+ @Nullable List<String> headers = allHeaders.get(headerName);
1088
+ return headers != null && !headers.isEmpty() ? headers.get(0) : null;
1089
+ }
1090
+
1091
+ // Copy as much as possible from the src buffer into dst buffer.
1092
+ // Returns the number of bytes copied.
1093
+ private static int copyByteBuffer(ByteBuffer src, ByteBuffer dst) {
1094
+ int remaining = Math.min(src.remaining(), dst.remaining());
1095
+ int limit = src.limit();
1096
+ src.limit(src.position() + remaining);
1097
+ dst.put(src);
1098
+ src.limit(limit);
1099
+ return remaining;
1100
+ }
1101
+
1102
+ @VisibleForTesting
1103
+ /* package */ final class UrlRequestCallback extends UrlRequest.Callback {
1104
+
1105
+ private volatile boolean isClosed = false;
1106
+
1107
+ public void close() {
1108
+ this.isClosed = true;
1109
+ }
1110
+
1111
+ @Override
1112
+ public synchronized void onRedirectReceived(
1113
+ UrlRequest request, UrlResponseInfo info, String newLocationUrl) {
1114
+ if (isClosed) {
1115
+ return;
1116
+ }
1117
+ Assertions.checkNotNull(currentUrlRequest);
1118
+ Assertions.checkNotNull(currentUrlRequestCallback);
1119
+ DataSpec dataSpec = Assertions.checkNotNull(currentDataSpec);
1120
+ int responseCode = info.getHttpStatusCode();
1121
+ if (dataSpec.httpMethod == DataSpec.HTTP_METHOD_POST) {
1122
+ // The industry standard is to disregard POST redirects when the status code is 307 or 308.
1123
+ if (responseCode == 307 || responseCode == 308) {
1124
+ exception =
1125
+ new InvalidResponseCodeException(
1126
+ responseCode,
1127
+ info.getHttpStatusText(),
1128
+ /* cause= */ null,
1129
+ info.getAllHeaders(),
1130
+ dataSpec,
1131
+ /* responseBody= */ Util.EMPTY_BYTE_ARRAY);
1132
+ operation.open();
1133
+ return;
1134
+ }
1135
+ }
1136
+ if (resetTimeoutOnRedirects) {
1137
+ resetConnectTimeout();
1138
+ }
1139
+
1140
+ CookieHandler cookieHandler = CookieHandler.getDefault();
1141
+ if (cookieHandler == null && handleSetCookieRequests) {
1142
+ // A temporary CookieManager is created for the duration of this request - this guarantees
1143
+ // redirects preserve the cookies correctly.
1144
+ cookieHandler = new CookieManager();
1145
+ }
1146
+
1147
+ String url = info.getUrl();
1148
+ Map<String, List<String>> headers = info.getAllHeaders();
1149
+ HttpUtil.storeCookiesFromHeaders(url, headers, cookieHandler);
1150
+ String cookieHeaders = HttpUtil.getCookieHeader(url, headers, cookieHandler);
1151
+
1152
+ boolean shouldKeepPost =
1153
+ keepPostFor302Redirects
1154
+ && dataSpec.httpMethod == DataSpec.HTTP_METHOD_POST
1155
+ && responseCode == 302;
1156
+
1157
+ // request.followRedirect() transforms a POST request into a GET request, so if we want to
1158
+ // keep it as a POST we need to fall through to the manual redirect logic below.
1159
+ if (!shouldKeepPost) {
1160
+ // No cookies, or we're not handling them - so just follow the redirect.
1161
+ if (!handleSetCookieRequests || TextUtils.isEmpty(cookieHeaders)) {
1162
+ request.followRedirect();
1163
+ return;
1164
+ }
1165
+ }
1166
+
1167
+ DataSpec redirectUrlDataSpec;
1168
+ if (!shouldKeepPost && dataSpec.httpMethod == DataSpec.HTTP_METHOD_POST) {
1169
+ // For POST redirects that aren't 307 or 308, the redirect is followed but request is
1170
+ // transformed into a GET unless shouldKeepPost is true.
1171
+ redirectUrlDataSpec =
1172
+ dataSpec
1173
+ .buildUpon()
1174
+ .setUri(newLocationUrl)
1175
+ .setHttpMethod(DataSpec.HTTP_METHOD_GET)
1176
+ .setHttpBody(null)
1177
+ .build();
1178
+ } else {
1179
+ redirectUrlDataSpec = dataSpec.withUri(Uri.parse(newLocationUrl));
1180
+ }
1181
+
1182
+ if (!TextUtils.isEmpty(cookieHeaders)) {
1183
+ Map<String, String> requestHeaders = new HashMap<>();
1184
+ requestHeaders.putAll(dataSpec.httpRequestHeaders);
1185
+ requestHeaders.put(HttpHeaders.COOKIE, cookieHeaders);
1186
+ redirectUrlDataSpec =
1187
+ redirectUrlDataSpec.buildUpon().setHttpRequestHeaders(requestHeaders).build();
1188
+ }
1189
+
1190
+ closeCurrentUrlRequestAndCallback();
1191
+ try {
1192
+ createCurrentUrlRequestAndCallback(redirectUrlDataSpec);
1193
+ } catch (IOException e) {
1194
+ exception = e;
1195
+ return;
1196
+ }
1197
+
1198
+ currentUrlRequest.start();
1199
+ }
1200
+
1201
+ @Override
1202
+ public synchronized void onResponseStarted(UrlRequest request, UrlResponseInfo info) {
1203
+ if (isClosed) {
1204
+ return;
1205
+ }
1206
+ HttpUtil.storeCookiesFromHeaders(
1207
+ info.getUrl(), info.getAllHeaders(), CookieHandler.getDefault());
1208
+ responseInfo = info;
1209
+ operation.open();
1210
+ }
1211
+
1212
+ @Override
1213
+ public synchronized void onReadCompleted(
1214
+ UrlRequest request, UrlResponseInfo info, ByteBuffer buffer) {
1215
+ if (isClosed) {
1216
+ return;
1217
+ }
1218
+ operation.open();
1219
+ }
1220
+
1221
+ @Override
1222
+ public synchronized void onSucceeded(UrlRequest request, UrlResponseInfo info) {
1223
+ if (isClosed) {
1224
+ return;
1225
+ }
1226
+ finished = true;
1227
+ operation.open();
1228
+ }
1229
+
1230
+ @Override
1231
+ public synchronized void onFailed(
1232
+ UrlRequest request, UrlResponseInfo info, CronetException error) {
1233
+ if (isClosed) {
1234
+ return;
1235
+ }
1236
+ if (error instanceof NetworkException
1237
+ && ((NetworkException) error).getErrorCode()
1238
+ == NetworkException.ERROR_HOSTNAME_NOT_RESOLVED) {
1239
+ exception = new UnknownHostException();
1240
+ } else {
1241
+ exception = error;
1242
+ }
1243
+ operation.open();
1244
+ }
1245
+ }
1246
+ }