summaryrefslogtreecommitdiffstats
path: root/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream')
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/Allocation.java46
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/Allocator.java63
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/AssetDataSource.java150
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/BandwidthMeter.java71
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/BaseDataSource.java102
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ByteArrayDataSink.java63
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ByteArrayDataSource.java91
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ContentDataSource.java173
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java110
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSink.java67
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSource.java111
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSourceException.java41
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSourceInputStream.java107
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSpec.java478
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultAllocator.java179
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java731
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultDataSource.java289
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.java85
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java798
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java119
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultLoadErrorHandlingPolicy.java110
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DummyDataSource.java59
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/FileDataSource.java171
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/FileDataSourceFactory.java38
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/HttpDataSource.java379
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/LoadErrorHandlingPolicy.java87
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/Loader.java521
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/LoaderErrorThrower.java63
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ParsingLoadable.java177
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/PriorityDataSource.java89
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/PriorityDataSourceFactory.java49
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/RawResourceDataSource.java199
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ResolvingDataSource.java132
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/StatsDataSource.java113
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/TeeDataSource.java105
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/TransferListener.java77
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/UdpDataSource.java176
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/Cache.java286
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java210
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java45
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java580
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java112
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheEvictor.java47
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheFileMetadata.java28
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java252
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheKeyFactory.java29
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheSpan.java106
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheUtil.java434
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CachedContent.java208
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java956
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CachedRegionTracker.java204
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/ContentMetadata.java87
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/ContentMetadataMutations.java145
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadata.java173
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictor.java89
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/NoOpCacheEvictor.java57
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/SimpleCache.java812
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java217
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSink.java99
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSource.java89
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipher.java123
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/crypto/CryptoUtil.java46
62 files changed, 11853 insertions, 0 deletions
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/Allocation.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/Allocation.java
new file mode 100644
index 0000000000..87dd142e6a
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/Allocation.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream;
+
+/**
+ * An allocation within a byte array.
+ * <p>
+ * The allocation's length is obtained by calling {@link Allocator#getIndividualAllocationLength()}
+ * on the {@link Allocator} from which it was obtained.
+ */
+public final class Allocation {
+
+ /**
+ * The array containing the allocated space. The allocated space might not be at the start of the
+ * array, and so {@link #offset} must be used when indexing into it.
+ */
+ public final byte[] data;
+
+ /**
+ * The offset of the allocated space in {@link #data}.
+ */
+ public final int offset;
+
+ /**
+ * @param data The array containing the allocated space.
+ * @param offset The offset of the allocated space in {@code data}.
+ */
+ public Allocation(byte[] data, int offset) {
+ this.data = data;
+ this.offset = offset;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/Allocator.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/Allocator.java
new file mode 100644
index 0000000000..d554d0fe7f
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/Allocator.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream;
+
+/**
+ * A source of allocations.
+ */
+public interface Allocator {
+
+ /**
+ * Obtain an {@link Allocation}.
+ * <p>
+ * When the caller has finished with the {@link Allocation}, it should be returned by calling
+ * {@link #release(Allocation)}.
+ *
+ * @return The {@link Allocation}.
+ */
+ Allocation allocate();
+
+ /**
+ * Releases an {@link Allocation} back to the allocator.
+ *
+ * @param allocation The {@link Allocation} being released.
+ */
+ void release(Allocation allocation);
+
+ /**
+ * Releases an array of {@link Allocation}s back to the allocator.
+ *
+ * @param allocations The array of {@link Allocation}s being released.
+ */
+ void release(Allocation[] allocations);
+
+ /**
+ * Hints to the allocator that it should make a best effort to release any excess
+ * {@link Allocation}s.
+ */
+ void trim();
+
+ /**
+ * Returns the total number of bytes currently allocated.
+ */
+ int getTotalBytesAllocated();
+
+ /**
+ * Returns the length of each individual {@link Allocation}.
+ */
+ int getIndividualAllocationLength();
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/AssetDataSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/AssetDataSource.java
new file mode 100644
index 0000000000..70cd1de8fe
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/AssetDataSource.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream;
+
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull;
+
+import android.content.Context;
+import android.content.res.AssetManager;
+import android.net.Uri;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+
+/** A {@link DataSource} for reading from a local asset. */
+public final class AssetDataSource extends BaseDataSource {
+
+ /**
+ * Thrown when an {@link IOException} is encountered reading a local asset.
+ */
+ public static final class AssetDataSourceException extends IOException {
+
+ public AssetDataSourceException(IOException cause) {
+ super(cause);
+ }
+
+ }
+
+ private final AssetManager assetManager;
+
+ @Nullable private Uri uri;
+ @Nullable private InputStream inputStream;
+ private long bytesRemaining;
+ private boolean opened;
+
+ /** @param context A context. */
+ public AssetDataSource(Context context) {
+ super(/* isNetwork= */ false);
+ this.assetManager = context.getAssets();
+ }
+
+ @Override
+ public long open(DataSpec dataSpec) throws AssetDataSourceException {
+ try {
+ uri = dataSpec.uri;
+ String path = Assertions.checkNotNull(uri.getPath());
+ if (path.startsWith("/android_asset/")) {
+ path = path.substring(15);
+ } else if (path.startsWith("/")) {
+ path = path.substring(1);
+ }
+ transferInitializing(dataSpec);
+ inputStream = assetManager.open(path, AssetManager.ACCESS_RANDOM);
+ long skipped = inputStream.skip(dataSpec.position);
+ if (skipped < dataSpec.position) {
+ // assetManager.open() returns an AssetInputStream, whose skip() implementation only skips
+ // fewer bytes than requested if the skip is beyond the end of the asset's data.
+ throw new EOFException();
+ }
+ if (dataSpec.length != C.LENGTH_UNSET) {
+ bytesRemaining = dataSpec.length;
+ } else {
+ bytesRemaining = inputStream.available();
+ if (bytesRemaining == Integer.MAX_VALUE) {
+ // assetManager.open() returns an AssetInputStream, whose available() implementation
+ // returns Integer.MAX_VALUE if the remaining length is greater than (or equal to)
+ // Integer.MAX_VALUE. We don't know the true length in this case, so treat as unbounded.
+ bytesRemaining = C.LENGTH_UNSET;
+ }
+ }
+ } catch (IOException e) {
+ throw new AssetDataSourceException(e);
+ }
+
+ opened = true;
+ transferStarted(dataSpec);
+ return bytesRemaining;
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int readLength) throws AssetDataSourceException {
+ if (readLength == 0) {
+ return 0;
+ } else if (bytesRemaining == 0) {
+ return C.RESULT_END_OF_INPUT;
+ }
+
+ int bytesRead;
+ try {
+ int bytesToRead = bytesRemaining == C.LENGTH_UNSET ? readLength
+ : (int) Math.min(bytesRemaining, readLength);
+ bytesRead = castNonNull(inputStream).read(buffer, offset, bytesToRead);
+ } catch (IOException e) {
+ throw new AssetDataSourceException(e);
+ }
+
+ if (bytesRead == -1) {
+ if (bytesRemaining != C.LENGTH_UNSET) {
+ // End of stream reached having not read sufficient data.
+ throw new AssetDataSourceException(new EOFException());
+ }
+ return C.RESULT_END_OF_INPUT;
+ }
+ if (bytesRemaining != C.LENGTH_UNSET) {
+ bytesRemaining -= bytesRead;
+ }
+ bytesTransferred(bytesRead);
+ return bytesRead;
+ }
+
+ @Override
+ @Nullable
+ public Uri getUri() {
+ return uri;
+ }
+
+ @Override
+ public void close() throws AssetDataSourceException {
+ uri = null;
+ try {
+ if (inputStream != null) {
+ inputStream.close();
+ }
+ } catch (IOException e) {
+ throw new AssetDataSourceException(e);
+ } finally {
+ inputStream = null;
+ if (opened) {
+ opened = false;
+ transferEnded();
+ }
+ }
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/BandwidthMeter.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/BandwidthMeter.java
new file mode 100644
index 0000000000..5606b45702
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/BandwidthMeter.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream;
+
+import android.os.Handler;
+import androidx.annotation.Nullable;
+
+/**
+ * Provides estimates of the currently available bandwidth.
+ */
+public interface BandwidthMeter {
+
+ /**
+ * A listener of {@link BandwidthMeter} events.
+ */
+ interface EventListener {
+
+ /**
+ * Called periodically to indicate that bytes have been transferred or the estimated bitrate has
+ * changed.
+ *
+ * <p>Note: The estimated bitrate is typically derived from more information than just {@code
+ * bytes} and {@code elapsedMs}.
+ *
+ * @param elapsedMs The time taken to transfer {@code bytesTransferred}, in milliseconds. This
+ * is at most the elapsed time since the last callback, but may be less if there were
+ * periods during which data was not being transferred.
+ * @param bytesTransferred The number of bytes transferred since the last callback.
+ * @param bitrateEstimate The estimated bitrate in bits/sec.
+ */
+ void onBandwidthSample(int elapsedMs, long bytesTransferred, long bitrateEstimate);
+ }
+
+ /** Returns the estimated bitrate. */
+ long getBitrateEstimate();
+
+ /**
+ * Returns the {@link TransferListener} that this instance uses to gather bandwidth information
+ * from data transfers. May be null if the implementation does not listen to data transfers.
+ */
+ @Nullable
+ TransferListener getTransferListener();
+
+ /**
+ * Adds an {@link EventListener}.
+ *
+ * @param eventHandler A handler for events.
+ * @param eventListener A listener of events.
+ */
+ void addEventListener(Handler eventHandler, EventListener eventListener);
+
+ /**
+ * Removes an {@link EventListener}.
+ *
+ * @param eventListener The listener to be removed.
+ */
+ void removeEventListener(EventListener eventListener);
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/BaseDataSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/BaseDataSource.java
new file mode 100644
index 0000000000..3838094927
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/BaseDataSource.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream;
+
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull;
+
+import androidx.annotation.Nullable;
+import java.util.ArrayList;
+
+/**
+ * Base {@link DataSource} implementation to keep a list of {@link TransferListener}s.
+ *
+ * <p>Subclasses must call {@link #transferInitializing(DataSpec)}, {@link
+ * #transferStarted(DataSpec)}, {@link #bytesTransferred(int)}, and {@link #transferEnded()} to
+ * inform listeners of data transfers.
+ */
+public abstract class BaseDataSource implements DataSource {
+
+ private final boolean isNetwork;
+ private final ArrayList<TransferListener> listeners;
+
+ private int listenerCount;
+ @Nullable private DataSpec dataSpec;
+
+ /**
+ * Creates base data source.
+ *
+ * @param isNetwork Whether the data source loads data through a network.
+ */
+ protected BaseDataSource(boolean isNetwork) {
+ this.isNetwork = isNetwork;
+ this.listeners = new ArrayList<>(/* initialCapacity= */ 1);
+ }
+
+ @Override
+ public final void addTransferListener(TransferListener transferListener) {
+ if (!listeners.contains(transferListener)) {
+ listeners.add(transferListener);
+ listenerCount++;
+ }
+ }
+
+ /**
+ * Notifies listeners that data transfer for the specified {@link DataSpec} is being initialized.
+ *
+ * @param dataSpec {@link DataSpec} describing the data for initializing transfer.
+ */
+ protected final void transferInitializing(DataSpec dataSpec) {
+ for (int i = 0; i < listenerCount; i++) {
+ listeners.get(i).onTransferInitializing(/* source= */ this, dataSpec, isNetwork);
+ }
+ }
+
+ /**
+ * Notifies listeners that data transfer for the specified {@link DataSpec} started.
+ *
+ * @param dataSpec {@link DataSpec} describing the data being transferred.
+ */
+ protected final void transferStarted(DataSpec dataSpec) {
+ this.dataSpec = dataSpec;
+ for (int i = 0; i < listenerCount; i++) {
+ listeners.get(i).onTransferStart(/* source= */ this, dataSpec, isNetwork);
+ }
+ }
+
+ /**
+ * Notifies listeners that bytes were transferred.
+ *
+ * @param bytesTransferred The number of bytes transferred since the previous call to this method
+ * (or if the first call, since the transfer was started).
+ */
+ protected final void bytesTransferred(int bytesTransferred) {
+ DataSpec dataSpec = castNonNull(this.dataSpec);
+ for (int i = 0; i < listenerCount; i++) {
+ listeners
+ .get(i)
+ .onBytesTransferred(/* source= */ this, dataSpec, isNetwork, bytesTransferred);
+ }
+ }
+
+ /** Notifies listeners that a transfer ended. */
+ protected final void transferEnded() {
+ DataSpec dataSpec = castNonNull(this.dataSpec);
+ for (int i = 0; i < listenerCount; i++) {
+ listeners.get(i).onTransferEnd(/* source= */ this, dataSpec, isNetwork);
+ }
+ this.dataSpec = null;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ByteArrayDataSink.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ByteArrayDataSink.java
new file mode 100644
index 0000000000..4aa66538ff
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ByteArrayDataSink.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream;
+
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull;
+
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+
+/**
+ * A {@link DataSink} for writing to a byte array.
+ */
+public final class ByteArrayDataSink implements DataSink {
+
+ private @MonotonicNonNull ByteArrayOutputStream stream;
+
+ @Override
+ public void open(DataSpec dataSpec) {
+ if (dataSpec.length == C.LENGTH_UNSET) {
+ stream = new ByteArrayOutputStream();
+ } else {
+ Assertions.checkArgument(dataSpec.length <= Integer.MAX_VALUE);
+ stream = new ByteArrayOutputStream((int) dataSpec.length);
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ castNonNull(stream).close();
+ }
+
+ @Override
+ public void write(byte[] buffer, int offset, int length) {
+ castNonNull(stream).write(buffer, offset, length);
+ }
+
+ /**
+ * Returns the data written to the sink since the last call to {@link #open(DataSpec)}, or null if
+ * {@link #open(DataSpec)} has never been called.
+ */
+ @Nullable
+ public byte[] getData() {
+ return stream == null ? null : stream.toByteArray();
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ByteArrayDataSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ByteArrayDataSource.java
new file mode 100644
index 0000000000..0be103701d
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ByteArrayDataSource.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream;
+
+import android.net.Uri;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import java.io.IOException;
+
+/** A {@link DataSource} for reading from a byte array. */
+public final class ByteArrayDataSource extends BaseDataSource {
+
+ private final byte[] data;
+
+ @Nullable private Uri uri;
+ private int readPosition;
+ private int bytesRemaining;
+ private boolean opened;
+
+ /**
+ * @param data The data to be read.
+ */
+ public ByteArrayDataSource(byte[] data) {
+ super(/* isNetwork= */ false);
+ Assertions.checkNotNull(data);
+ Assertions.checkArgument(data.length > 0);
+ this.data = data;
+ }
+
+ @Override
+ public long open(DataSpec dataSpec) throws IOException {
+ uri = dataSpec.uri;
+ transferInitializing(dataSpec);
+ readPosition = (int) dataSpec.position;
+ bytesRemaining = (int) ((dataSpec.length == C.LENGTH_UNSET)
+ ? (data.length - dataSpec.position) : dataSpec.length);
+ if (bytesRemaining <= 0 || readPosition + bytesRemaining > data.length) {
+ throw new IOException("Unsatisfiable range: [" + readPosition + ", " + dataSpec.length
+ + "], length: " + data.length);
+ }
+ opened = true;
+ transferStarted(dataSpec);
+ return bytesRemaining;
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int readLength) {
+ if (readLength == 0) {
+ return 0;
+ } else if (bytesRemaining == 0) {
+ return C.RESULT_END_OF_INPUT;
+ }
+
+ readLength = Math.min(readLength, bytesRemaining);
+ System.arraycopy(data, readPosition, buffer, offset, readLength);
+ readPosition += readLength;
+ bytesRemaining -= readLength;
+ bytesTransferred(readLength);
+ return readLength;
+ }
+
+ @Override
+ @Nullable
+ public Uri getUri() {
+ return uri;
+ }
+
+ @Override
+ public void close() {
+ if (opened) {
+ opened = false;
+ transferEnded();
+ }
+ uri = null;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ContentDataSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ContentDataSource.java
new file mode 100644
index 0000000000..b73d9d6375
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ContentDataSource.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream;
+
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.res.AssetFileDescriptor;
+import android.net.Uri;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import java.io.EOFException;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.nio.channels.FileChannel;
+
+/** A {@link DataSource} for reading from a content URI. */
+public final class ContentDataSource extends BaseDataSource {
+
+ /**
+ * Thrown when an {@link IOException} is encountered reading from a content URI.
+ */
+ public static class ContentDataSourceException extends IOException {
+
+ public ContentDataSourceException(IOException cause) {
+ super(cause);
+ }
+
+ }
+
+ private final ContentResolver resolver;
+
+ @Nullable private Uri uri;
+ @Nullable private AssetFileDescriptor assetFileDescriptor;
+ @Nullable private FileInputStream inputStream;
+ private long bytesRemaining;
+ private boolean opened;
+
+ /**
+ * @param context A context.
+ */
+ public ContentDataSource(Context context) {
+ super(/* isNetwork= */ false);
+ this.resolver = context.getContentResolver();
+ }
+
+ @Override
+ public long open(DataSpec dataSpec) throws ContentDataSourceException {
+ try {
+ Uri uri = dataSpec.uri;
+ this.uri = uri;
+
+ transferInitializing(dataSpec);
+ AssetFileDescriptor assetFileDescriptor = resolver.openAssetFileDescriptor(uri, "r");
+ this.assetFileDescriptor = assetFileDescriptor;
+ if (assetFileDescriptor == null) {
+ throw new FileNotFoundException("Could not open file descriptor for: " + uri);
+ }
+ FileInputStream inputStream = new FileInputStream(assetFileDescriptor.getFileDescriptor());
+ this.inputStream = inputStream;
+
+ long assetStartOffset = assetFileDescriptor.getStartOffset();
+ long skipped = inputStream.skip(assetStartOffset + dataSpec.position) - assetStartOffset;
+ if (skipped != dataSpec.position) {
+ // We expect the skip to be satisfied in full. If it isn't then we're probably trying to
+ // skip beyond the end of the data.
+ throw new EOFException();
+ }
+ if (dataSpec.length != C.LENGTH_UNSET) {
+ bytesRemaining = dataSpec.length;
+ } else {
+ long assetFileDescriptorLength = assetFileDescriptor.getLength();
+ if (assetFileDescriptorLength == AssetFileDescriptor.UNKNOWN_LENGTH) {
+ // The asset must extend to the end of the file. If FileInputStream.getChannel().size()
+ // returns 0 then the remaining length cannot be determined.
+ FileChannel channel = inputStream.getChannel();
+ long channelSize = channel.size();
+ bytesRemaining = channelSize == 0 ? C.LENGTH_UNSET : channelSize - channel.position();
+ } else {
+ bytesRemaining = assetFileDescriptorLength - skipped;
+ }
+ }
+ } catch (IOException e) {
+ throw new ContentDataSourceException(e);
+ }
+
+ opened = true;
+ transferStarted(dataSpec);
+
+ return bytesRemaining;
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int readLength) throws ContentDataSourceException {
+ if (readLength == 0) {
+ return 0;
+ } else if (bytesRemaining == 0) {
+ return C.RESULT_END_OF_INPUT;
+ }
+
+ int bytesRead;
+ try {
+ int bytesToRead = bytesRemaining == C.LENGTH_UNSET ? readLength
+ : (int) Math.min(bytesRemaining, readLength);
+ bytesRead = castNonNull(inputStream).read(buffer, offset, bytesToRead);
+ } catch (IOException e) {
+ throw new ContentDataSourceException(e);
+ }
+
+ if (bytesRead == -1) {
+ if (bytesRemaining != C.LENGTH_UNSET) {
+ // End of stream reached having not read sufficient data.
+ throw new ContentDataSourceException(new EOFException());
+ }
+ return C.RESULT_END_OF_INPUT;
+ }
+ if (bytesRemaining != C.LENGTH_UNSET) {
+ bytesRemaining -= bytesRead;
+ }
+ bytesTransferred(bytesRead);
+ return bytesRead;
+ }
+
+ @Override
+ @Nullable
+ public Uri getUri() {
+ return uri;
+ }
+
+ @SuppressWarnings("Finally")
+ @Override
+ public void close() throws ContentDataSourceException {
+ uri = null;
+ try {
+ if (inputStream != null) {
+ inputStream.close();
+ }
+ } catch (IOException e) {
+ throw new ContentDataSourceException(e);
+ } finally {
+ inputStream = null;
+ try {
+ if (assetFileDescriptor != null) {
+ assetFileDescriptor.close();
+ }
+ } catch (IOException e) {
+ throw new ContentDataSourceException(e);
+ } finally {
+ assetFileDescriptor = null;
+ if (opened) {
+ opened = false;
+ transferEnded();
+ }
+ }
+ }
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java
new file mode 100644
index 0000000000..57420250ac
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream;
+
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull;
+
+import android.net.Uri;
+import android.util.Base64;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.net.URLDecoder;
+
+/** A {@link DataSource} for reading data URLs, as defined by RFC 2397. */
+public final class DataSchemeDataSource extends BaseDataSource {
+
+ public static final String SCHEME_DATA = "data";
+
+ @Nullable private DataSpec dataSpec;
+ @Nullable private byte[] data;
+ private int endPosition;
+ private int readPosition;
+
+ // the constructor does not initialize fields: data
+ @SuppressWarnings("nullness:initialization.fields.uninitialized")
+ public DataSchemeDataSource() {
+ super(/* isNetwork= */ false);
+ }
+
+ @Override
+ public long open(DataSpec dataSpec) throws IOException {
+ transferInitializing(dataSpec);
+ this.dataSpec = dataSpec;
+ readPosition = (int) dataSpec.position;
+ Uri uri = dataSpec.uri;
+ String scheme = uri.getScheme();
+ if (!SCHEME_DATA.equals(scheme)) {
+ throw new ParserException("Unsupported scheme: " + scheme);
+ }
+ String[] uriParts = Util.split(uri.getSchemeSpecificPart(), ",");
+ if (uriParts.length != 2) {
+ throw new ParserException("Unexpected URI format: " + uri);
+ }
+ String dataString = uriParts[1];
+ if (uriParts[0].contains(";base64")) {
+ try {
+ data = Base64.decode(dataString, 0);
+ } catch (IllegalArgumentException e) {
+ throw new ParserException("Error while parsing Base64 encoded string: " + dataString, e);
+ }
+ } else {
+ // TODO: Add support for other charsets.
+ data = Util.getUtf8Bytes(URLDecoder.decode(dataString, C.ASCII_NAME));
+ }
+ endPosition =
+ dataSpec.length != C.LENGTH_UNSET ? (int) dataSpec.length + readPosition : data.length;
+ if (endPosition > data.length || readPosition > endPosition) {
+ data = null;
+ throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE);
+ }
+ transferStarted(dataSpec);
+ return (long) endPosition - readPosition;
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int readLength) {
+ if (readLength == 0) {
+ return 0;
+ }
+ int remainingBytes = endPosition - readPosition;
+ if (remainingBytes == 0) {
+ return C.RESULT_END_OF_INPUT;
+ }
+ readLength = Math.min(readLength, remainingBytes);
+ System.arraycopy(castNonNull(data), readPosition, buffer, offset, readLength);
+ readPosition += readLength;
+ bytesTransferred(readLength);
+ return readLength;
+ }
+
+ @Override
+ @Nullable
+ public Uri getUri() {
+ return dataSpec != null ? dataSpec.uri : null;
+ }
+
+ @Override
+ public void close() {
+ if (data != null) {
+ data = null;
+ transferEnded();
+ }
+ dataSpec = null;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSink.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSink.java
new file mode 100644
index 0000000000..c85ec8cfca
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSink.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream;
+
+import java.io.IOException;
+
+/**
+ * A component to which streams of data can be written.
+ */
+public interface DataSink {
+
+ /**
+ * A factory for {@link DataSink} instances.
+ */
+ interface Factory {
+
+ /**
+ * Creates a {@link DataSink} instance.
+ */
+ DataSink createDataSink();
+
+ }
+
+ /**
+ * Opens the sink to consume the specified data.
+ *
+ * <p>Note: If an {@link IOException} is thrown, callers must still call {@link #close()} to
+ * ensure that any partial effects of the invocation are cleaned up.
+ *
+ * @param dataSpec Defines the data to be consumed.
+ * @throws IOException If an error occurs opening the sink.
+ */
+ void open(DataSpec dataSpec) throws IOException;
+
+ /**
+ * Consumes the provided data.
+ *
+ * @param buffer The buffer from which data should be consumed.
+ * @param offset The offset of the data to consume in {@code buffer}.
+ * @param length The length of the data to consume, in bytes.
+ * @throws IOException If an error occurs writing to the sink.
+ */
+ void write(byte[] buffer, int offset, int length) throws IOException;
+
+ /**
+ * Closes the sink.
+ *
+ * <p>Note: This method must be called even if the corresponding call to {@link #open(DataSpec)}
+ * threw an {@link IOException}. See {@link #open(DataSpec)} for more details.
+ *
+ * @throws IOException If an error occurs closing the sink.
+ */
+ void close() throws IOException;
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSource.java
new file mode 100644
index 0000000000..26529253f8
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSource.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream;
+
+import android.net.Uri;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A component from which streams of data can be read.
+ */
+public interface DataSource {
+
+ /**
+ * A factory for {@link DataSource} instances.
+ */
+ interface Factory {
+
+ /**
+ * Creates a {@link DataSource} instance.
+ */
+ DataSource createDataSource();
+ }
+
+ /**
+ * Adds a {@link TransferListener} to listen to data transfers. This method is not thread-safe.
+ *
+ * @param transferListener A {@link TransferListener}.
+ */
+ void addTransferListener(TransferListener transferListener);
+
+ /**
+ * Opens the source to read the specified data.
+ * <p>
+ * Note: If an {@link IOException} is thrown, callers must still call {@link #close()} to ensure
+ * that any partial effects of the invocation are cleaned up.
+ *
+ * @param dataSpec Defines the data to be read.
+ * @throws IOException If an error occurs opening the source. {@link DataSourceException} can be
+ * thrown or used as a cause of the thrown exception to specify the reason of the error.
+ * @return The number of bytes that can be read from the opened source. For unbounded requests
+ * (i.e. requests where {@link DataSpec#length} equals {@link C#LENGTH_UNSET}) this value
+ * is the resolved length of the request, or {@link C#LENGTH_UNSET} if the length is still
+ * unresolved. For all other requests, the value returned will be equal to the request's
+ * {@link DataSpec#length}.
+ */
+ long open(DataSpec dataSpec) throws IOException;
+
+ /**
+ * Reads up to {@code readLength} bytes of data and stores them into {@code buffer}, starting at
+ * index {@code offset}.
+ *
+ * <p>If {@code readLength} is zero then 0 is returned. Otherwise, if no data is available because
+ * the end of the opened range has been reached, then {@link C#RESULT_END_OF_INPUT} is returned.
+ * Otherwise, the call will block until at least one byte of data has been read and the number of
+ * bytes read is returned.
+ *
+ * @param buffer The buffer into which the read data should be stored.
+ * @param offset The start offset into {@code buffer} at which data should be written.
+ * @param readLength The maximum number of bytes to read.
+ * @return The number of bytes read, or {@link C#RESULT_END_OF_INPUT} if no data is available
+ * because the end of the opened range has been reached.
+ * @throws IOException If an error occurs reading from the source.
+ */
+ int read(byte[] buffer, int offset, int readLength) throws IOException;
+
+ /**
+ * When the source is open, returns the {@link Uri} from which data is being read. The returned
+ * {@link Uri} will be identical to the one passed {@link #open(DataSpec)} in the {@link DataSpec}
+ * unless redirection has occurred. If redirection has occurred, the {@link Uri} after redirection
+ * is returned.
+ *
+ * @return The {@link Uri} from which data is being read, or null if the source is not open.
+ */
+ @Nullable Uri getUri();
+
+ /**
+ * When the source is open, returns the response headers associated with the last {@link #open}
+ * call. Otherwise, returns an empty map.
+ */
+ default Map<String, List<String>> getResponseHeaders() {
+ return Collections.emptyMap();
+ }
+
+ /**
+ * Closes the source.
+ * <p>
+ * Note: This method must be called even if the corresponding call to {@link #open(DataSpec)}
+ * threw an {@link IOException}. See {@link #open(DataSpec)} for more details.
+ *
+ * @throws IOException If an error occurs closing the source.
+ */
+ void close() throws IOException;
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSourceException.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSourceException.java
new file mode 100644
index 0000000000..13c34d1dfb
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSourceException.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream;
+
+import java.io.IOException;
+
+/**
+ * Used to specify reason of a DataSource error.
+ */
+public final class DataSourceException extends IOException {
+
+ public static final int POSITION_OUT_OF_RANGE = 0;
+
+ /**
+ * The reason of this {@link DataSourceException}. It can only be {@link #POSITION_OUT_OF_RANGE}.
+ */
+ public final int reason;
+
+ /**
+ * Constructs a DataSourceException.
+ *
+ * @param reason Reason of the error. It can only be {@link #POSITION_OUT_OF_RANGE}.
+ */
+ public DataSourceException(int reason) {
+ this.reason = reason;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSourceInputStream.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSourceInputStream.java
new file mode 100644
index 0000000000..c25ba4c10a
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSourceInputStream.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream;
+
+import androidx.annotation.NonNull;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Allows data corresponding to a given {@link DataSpec} to be read from a {@link DataSource} and
+ * consumed through an {@link InputStream}.
+ */
+public final class DataSourceInputStream extends InputStream {
+
+ private final DataSource dataSource;
+ private final DataSpec dataSpec;
+ private final byte[] singleByteArray;
+
+ private boolean opened = false;
+ private boolean closed = false;
+ private long totalBytesRead;
+
+ /**
+ * @param dataSource The {@link DataSource} from which the data should be read.
+ * @param dataSpec The {@link DataSpec} defining the data to be read from {@code dataSource}.
+ */
+ public DataSourceInputStream(DataSource dataSource, DataSpec dataSpec) {
+ this.dataSource = dataSource;
+ this.dataSpec = dataSpec;
+ singleByteArray = new byte[1];
+ }
+
+ /**
+ * Returns the total number of bytes that have been read or skipped.
+ */
+ public long bytesRead() {
+ return totalBytesRead;
+ }
+
+ /**
+ * Optional call to open the underlying {@link DataSource}.
+ * <p>
+ * Calling this method does nothing if the {@link DataSource} is already open. Calling this
+ * method is optional, since the read and skip methods will automatically open the underlying
+ * {@link DataSource} if it's not open already.
+ *
+ * @throws IOException If an error occurs opening the {@link DataSource}.
+ */
+ public void open() throws IOException {
+ checkOpened();
+ }
+
+ @Override
+ public int read() throws IOException {
+ int length = read(singleByteArray);
+ return length == -1 ? -1 : (singleByteArray[0] & 0xFF);
+ }
+
+ @Override
+ public int read(@NonNull byte[] buffer) throws IOException {
+ return read(buffer, 0, buffer.length);
+ }
+
+ @Override
+ public int read(@NonNull byte[] buffer, int offset, int length) throws IOException {
+ Assertions.checkState(!closed);
+ checkOpened();
+ int bytesRead = dataSource.read(buffer, offset, length);
+ if (bytesRead == C.RESULT_END_OF_INPUT) {
+ return -1;
+ } else {
+ totalBytesRead += bytesRead;
+ return bytesRead;
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (!closed) {
+ dataSource.close();
+ closed = true;
+ }
+ }
+
+ private void checkOpened() throws IOException {
+ if (!opened) {
+ dataSource.open(dataSpec);
+ opened = true;
+ }
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSpec.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSpec.java
new file mode 100644
index 0000000000..6a419c6632
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSpec.java
@@ -0,0 +1,478 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream;
+
+import android.net.Uri;
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Defines a region of data.
+ */
+public final class DataSpec {
+
+ /**
+ * The flags that apply to any request for data. Possible flag values are {@link
+ * #FLAG_ALLOW_GZIP}, {@link #FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN} and {@link
+ * #FLAG_ALLOW_CACHE_FRAGMENTATION}.
+ */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ flag = true,
+ value = {FLAG_ALLOW_GZIP, FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN, FLAG_ALLOW_CACHE_FRAGMENTATION})
+ public @interface Flags {}
+ /**
+ * Allows an underlying network stack to request that the server use gzip compression.
+ *
+ * <p>Should not typically be set if the data being requested is already compressed (e.g. most
+ * audio and video requests). May be set when requesting other data.
+ *
+ * <p>When a {@link DataSource} is used to request data with this flag set, and if the {@link
+ * DataSource} does make a network request, then the value returned from {@link
+ * DataSource#open(DataSpec)} will typically be {@link C#LENGTH_UNSET}. The data read from {@link
+ * DataSource#read(byte[], int, int)} will be the decompressed data.
+ */
+ public static final int FLAG_ALLOW_GZIP = 1;
+ /** Prevents caching if the length cannot be resolved when the {@link DataSource} is opened. */
+ public static final int FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN = 1 << 1; // 2
+ /**
+ * Allows fragmentation of this request into multiple cache files, meaning a cache eviction policy
+ * will be able to evict individual fragments of the data. Depending on the cache implementation,
+ * setting this flag may also enable more concurrent access to the data (e.g. reading one fragment
+ * whilst writing another).
+ */
+ public static final int FLAG_ALLOW_CACHE_FRAGMENTATION = 1 << 2; // 4
+
+ /**
+ * The set of HTTP methods that are supported by ExoPlayer {@link HttpDataSource}s. One of {@link
+ * #HTTP_METHOD_GET}, {@link #HTTP_METHOD_POST} or {@link #HTTP_METHOD_HEAD}.
+ */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({HTTP_METHOD_GET, HTTP_METHOD_POST, HTTP_METHOD_HEAD})
+ public @interface HttpMethod {}
+
+ public static final int HTTP_METHOD_GET = 1;
+ public static final int HTTP_METHOD_POST = 2;
+ public static final int HTTP_METHOD_HEAD = 3;
+
+ /**
+ * The source from which data should be read.
+ */
+ public final Uri uri;
+
+ /**
+ * The HTTP method, which will be used by {@link HttpDataSource} when requesting this DataSpec.
+ * This value will be ignored by non-http {@link DataSource}s.
+ */
+ public final @HttpMethod int httpMethod;
+
+ /**
+ * The HTTP request body, null otherwise. If the body is non-null, then httpBody.length will be
+ * non-zero.
+ */
+ @Nullable public final byte[] httpBody;
+
+ /** Immutable map containing the headers to use in HTTP requests. */
+ public final Map<String, String> httpRequestHeaders;
+
+ /** The absolute position of the data in the full stream. */
+ public final long absoluteStreamPosition;
+ /**
+ * The position of the data when read from {@link #uri}.
+ * <p>
+ * Always equal to {@link #absoluteStreamPosition} unless the {@link #uri} defines the location
+ * of a subset of the underlying data.
+ */
+ public final long position;
+ /**
+ * The length of the data, or {@link C#LENGTH_UNSET}.
+ */
+ public final long length;
+ /**
+ * A key that uniquely identifies the original stream. Used for cache indexing. May be null if the
+ * data spec is not intended to be used in conjunction with a cache.
+ */
+ @Nullable public final String key;
+ /** Request {@link Flags flags}. */
+ public final @Flags int flags;
+
+ /**
+ * Construct a data spec for the given uri and with {@link #key} set to null.
+ *
+ * @param uri {@link #uri}.
+ */
+ public DataSpec(Uri uri) {
+ this(uri, 0);
+ }
+
+ /**
+ * Construct a data spec for the given uri and with {@link #key} set to null.
+ *
+ * @param uri {@link #uri}.
+ * @param flags {@link #flags}.
+ */
+ public DataSpec(Uri uri, @Flags int flags) {
+ this(uri, 0, C.LENGTH_UNSET, null, flags);
+ }
+
+ /**
+ * Construct a data spec where {@link #position} equals {@link #absoluteStreamPosition}.
+ *
+ * @param uri {@link #uri}.
+ * @param absoluteStreamPosition {@link #absoluteStreamPosition}, equal to {@link #position}.
+ * @param length {@link #length}.
+ * @param key {@link #key}.
+ */
+ public DataSpec(Uri uri, long absoluteStreamPosition, long length, @Nullable String key) {
+ this(uri, absoluteStreamPosition, absoluteStreamPosition, length, key, 0);
+ }
+
+ /**
+ * Construct a data spec where {@link #position} equals {@link #absoluteStreamPosition}.
+ *
+ * @param uri {@link #uri}.
+ * @param absoluteStreamPosition {@link #absoluteStreamPosition}, equal to {@link #position}.
+ * @param length {@link #length}.
+ * @param key {@link #key}.
+ * @param flags {@link #flags}.
+ */
+ public DataSpec(
+ Uri uri, long absoluteStreamPosition, long length, @Nullable String key, @Flags int flags) {
+ this(uri, absoluteStreamPosition, absoluteStreamPosition, length, key, flags);
+ }
+
+ /**
+ * Construct a data spec where {@link #position} equals {@link #absoluteStreamPosition} and has
+ * request headers.
+ *
+ * @param uri {@link #uri}.
+ * @param absoluteStreamPosition {@link #absoluteStreamPosition}, equal to {@link #position}.
+ * @param length {@link #length}.
+ * @param key {@link #key}.
+ * @param flags {@link #flags}.
+ * @param httpRequestHeaders {@link #httpRequestHeaders}
+ */
+ public DataSpec(
+ Uri uri,
+ long absoluteStreamPosition,
+ long length,
+ @Nullable String key,
+ @Flags int flags,
+ Map<String, String> httpRequestHeaders) {
+ this(
+ uri,
+ inferHttpMethod(null),
+ null,
+ absoluteStreamPosition,
+ absoluteStreamPosition,
+ length,
+ key,
+ flags,
+ httpRequestHeaders);
+ }
+
+ /**
+ * Construct a data spec where {@link #position} may differ from {@link #absoluteStreamPosition}.
+ *
+ * @param uri {@link #uri}.
+ * @param absoluteStreamPosition {@link #absoluteStreamPosition}.
+ * @param position {@link #position}.
+ * @param length {@link #length}.
+ * @param key {@link #key}.
+ * @param flags {@link #flags}.
+ */
+ public DataSpec(
+ Uri uri,
+ long absoluteStreamPosition,
+ long position,
+ long length,
+ @Nullable String key,
+ @Flags int flags) {
+ this(uri, null, absoluteStreamPosition, position, length, key, flags);
+ }
+
+ /**
+ * Construct a data spec by inferring the {@link #httpMethod} based on the {@code postBody}
+ * parameter. If postBody is non-null, then httpMethod is set to {@link #HTTP_METHOD_POST}. If
+ * postBody is null, then httpMethod is set to {@link #HTTP_METHOD_GET}.
+ *
+ * @param uri {@link #uri}.
+ * @param postBody {@link #httpBody} The body of the HTTP request, which is also used to infer the
+ * {@link #httpMethod}.
+ * @param absoluteStreamPosition {@link #absoluteStreamPosition}.
+ * @param position {@link #position}.
+ * @param length {@link #length}.
+ * @param key {@link #key}.
+ * @param flags {@link #flags}.
+ */
+ public DataSpec(
+ Uri uri,
+ @Nullable byte[] postBody,
+ long absoluteStreamPosition,
+ long position,
+ long length,
+ @Nullable String key,
+ @Flags int flags) {
+ this(
+ uri,
+ /* httpMethod= */ inferHttpMethod(postBody),
+ /* httpBody= */ postBody,
+ absoluteStreamPosition,
+ position,
+ length,
+ key,
+ flags);
+ }
+
+ /**
+ * Construct a data spec where {@link #position} may differ from {@link #absoluteStreamPosition}.
+ *
+ * @param uri {@link #uri}.
+ * @param httpMethod {@link #httpMethod}.
+ * @param httpBody {@link #httpBody}.
+ * @param absoluteStreamPosition {@link #absoluteStreamPosition}.
+ * @param position {@link #position}.
+ * @param length {@link #length}.
+ * @param key {@link #key}.
+ * @param flags {@link #flags}.
+ */
+ public DataSpec(
+ Uri uri,
+ @HttpMethod int httpMethod,
+ @Nullable byte[] httpBody,
+ long absoluteStreamPosition,
+ long position,
+ long length,
+ @Nullable String key,
+ @Flags int flags) {
+ this(
+ uri,
+ httpMethod,
+ httpBody,
+ absoluteStreamPosition,
+ position,
+ length,
+ key,
+ flags,
+ /* httpRequestHeaders= */ Collections.emptyMap());
+ }
+
+ /**
+ * Construct a data spec with request parameters to be used as HTTP headers inside HTTP requests.
+ *
+ * @param uri {@link #uri}.
+ * @param httpMethod {@link #httpMethod}.
+ * @param httpBody {@link #httpBody}.
+ * @param absoluteStreamPosition {@link #absoluteStreamPosition}.
+ * @param position {@link #position}.
+ * @param length {@link #length}.
+ * @param key {@link #key}.
+ * @param flags {@link #flags}.
+ * @param httpRequestHeaders {@link #httpRequestHeaders}.
+ */
+ public DataSpec(
+ Uri uri,
+ @HttpMethod int httpMethod,
+ @Nullable byte[] httpBody,
+ long absoluteStreamPosition,
+ long position,
+ long length,
+ @Nullable String key,
+ @Flags int flags,
+ Map<String, String> httpRequestHeaders) {
+ Assertions.checkArgument(absoluteStreamPosition >= 0);
+ Assertions.checkArgument(position >= 0);
+ Assertions.checkArgument(length > 0 || length == C.LENGTH_UNSET);
+ this.uri = uri;
+ this.httpMethod = httpMethod;
+ this.httpBody = (httpBody != null && httpBody.length != 0) ? httpBody : null;
+ this.absoluteStreamPosition = absoluteStreamPosition;
+ this.position = position;
+ this.length = length;
+ this.key = key;
+ this.flags = flags;
+ this.httpRequestHeaders = Collections.unmodifiableMap(new HashMap<>(httpRequestHeaders));
+ }
+
+ /**
+ * Returns whether the given flag is set.
+ *
+ * @param flag Flag to be checked if it is set.
+ */
+ public boolean isFlagSet(@Flags int flag) {
+ return (this.flags & flag) == flag;
+ }
+
+ @Override
+ public String toString() {
+ return "DataSpec["
+ + getHttpMethodString()
+ + " "
+ + uri
+ + ", "
+ + Arrays.toString(httpBody)
+ + ", "
+ + absoluteStreamPosition
+ + ", "
+ + position
+ + ", "
+ + length
+ + ", "
+ + key
+ + ", "
+ + flags
+ + "]";
+ }
+
+ /**
+ * Returns an uppercase HTTP method name (e.g., "GET", "POST", "HEAD") corresponding to the {@link
+ * #httpMethod}.
+ */
+ public final String getHttpMethodString() {
+ return getStringForHttpMethod(httpMethod);
+ }
+
+ /**
+ * Returns an uppercase HTTP method name (e.g., "GET", "POST", "HEAD") corresponding to the {@code
+ * httpMethod}.
+ */
+ public static String getStringForHttpMethod(@HttpMethod int httpMethod) {
+ switch (httpMethod) {
+ case HTTP_METHOD_GET:
+ return "GET";
+ case HTTP_METHOD_POST:
+ return "POST";
+ case HTTP_METHOD_HEAD:
+ return "HEAD";
+ default:
+ throw new AssertionError(httpMethod);
+ }
+ }
+
+ /**
+ * Returns a data spec that represents a subrange of the data defined by this DataSpec. The
+ * subrange includes data from the offset up to the end of this DataSpec.
+ *
+ * @param offset The offset of the subrange.
+ * @return A data spec that represents a subrange of the data defined by this DataSpec.
+ */
+ public DataSpec subrange(long offset) {
+ return subrange(offset, length == C.LENGTH_UNSET ? C.LENGTH_UNSET : length - offset);
+ }
+
+ /**
+ * Returns a data spec that represents a subrange of the data defined by this DataSpec.
+ *
+ * @param offset The offset of the subrange.
+ * @param length The length of the subrange.
+ * @return A data spec that represents a subrange of the data defined by this DataSpec.
+ */
+ public DataSpec subrange(long offset, long length) {
+ if (offset == 0 && this.length == length) {
+ return this;
+ } else {
+ return new DataSpec(
+ uri,
+ httpMethod,
+ httpBody,
+ absoluteStreamPosition + offset,
+ position + offset,
+ length,
+ key,
+ flags,
+ httpRequestHeaders);
+ }
+ }
+
+ /**
+ * Returns a copy of this data spec with the specified Uri.
+ *
+ * @param uri The new source {@link Uri}.
+ * @return The copied data spec with the specified Uri.
+ */
+ public DataSpec withUri(Uri uri) {
+ return new DataSpec(
+ uri,
+ httpMethod,
+ httpBody,
+ absoluteStreamPosition,
+ position,
+ length,
+ key,
+ flags,
+ httpRequestHeaders);
+ }
+
+ /**
+ * Returns a copy of this data spec with the specified request headers.
+ *
+ * @param requestHeaders The HTTP request headers.
+ * @return The copied data spec with the specified request headers.
+ */
+ public DataSpec withRequestHeaders(Map<String, String> requestHeaders) {
+ return new DataSpec(
+ uri,
+ httpMethod,
+ httpBody,
+ absoluteStreamPosition,
+ position,
+ length,
+ key,
+ flags,
+ requestHeaders);
+ }
+
+ /**
+ * Returns a copy this data spec with additional request headers.
+ *
+ * <p>Note: Values in {@code requestHeaders} will overwrite values with the same header key that
+ * were previously set in this instance's {@code #httpRequestHeaders}.
+ *
+ * @param requestHeaders The additional HTTP request headers.
+ * @return The copied data with the additional HTTP request headers.
+ */
+ public DataSpec withAdditionalHeaders(Map<String, String> requestHeaders) {
+ Map<String, String> totalHeaders = new HashMap<>(this.httpRequestHeaders);
+ totalHeaders.putAll(requestHeaders);
+
+ return new DataSpec(
+ uri,
+ httpMethod,
+ httpBody,
+ absoluteStreamPosition,
+ position,
+ length,
+ key,
+ flags,
+ totalHeaders);
+ }
+
+ @HttpMethod
+ private static int inferHttpMethod(@Nullable byte[] postBody) {
+ return postBody != null ? HTTP_METHOD_POST : HTTP_METHOD_GET;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultAllocator.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultAllocator.java
new file mode 100644
index 0000000000..b12efcbe4e
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultAllocator.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.util.Arrays;
+
+/**
+ * Default implementation of {@link Allocator}.
+ */
+public final class DefaultAllocator implements Allocator {
+
+ private static final int AVAILABLE_EXTRA_CAPACITY = 100;
+
+ private final boolean trimOnReset;
+ private final int individualAllocationSize;
+ private final byte[] initialAllocationBlock;
+ private final Allocation[] singleAllocationReleaseHolder;
+
+ private int targetBufferSize;
+ private int allocatedCount;
+ private int availableCount;
+ private Allocation[] availableAllocations;
+
+ /**
+ * Constructs an instance without creating any {@link Allocation}s up front.
+ *
+ * @param trimOnReset Whether memory is freed when the allocator is reset. Should be true unless
+ * the allocator will be re-used by multiple player instances.
+ * @param individualAllocationSize The length of each individual {@link Allocation}.
+ */
+ public DefaultAllocator(boolean trimOnReset, int individualAllocationSize) {
+ this(trimOnReset, individualAllocationSize, 0);
+ }
+
+ /**
+ * Constructs an instance with some {@link Allocation}s created up front.
+ * <p>
+ * Note: {@link Allocation}s created up front will never be discarded by {@link #trim()}.
+ *
+ * @param trimOnReset Whether memory is freed when the allocator is reset. Should be true unless
+ * the allocator will be re-used by multiple player instances.
+ * @param individualAllocationSize The length of each individual {@link Allocation}.
+ * @param initialAllocationCount The number of allocations to create up front.
+ */
+ public DefaultAllocator(boolean trimOnReset, int individualAllocationSize,
+ int initialAllocationCount) {
+ Assertions.checkArgument(individualAllocationSize > 0);
+ Assertions.checkArgument(initialAllocationCount >= 0);
+ this.trimOnReset = trimOnReset;
+ this.individualAllocationSize = individualAllocationSize;
+ this.availableCount = initialAllocationCount;
+ this.availableAllocations = new Allocation[initialAllocationCount + AVAILABLE_EXTRA_CAPACITY];
+ if (initialAllocationCount > 0) {
+ initialAllocationBlock = new byte[initialAllocationCount * individualAllocationSize];
+ for (int i = 0; i < initialAllocationCount; i++) {
+ int allocationOffset = i * individualAllocationSize;
+ availableAllocations[i] = new Allocation(initialAllocationBlock, allocationOffset);
+ }
+ } else {
+ initialAllocationBlock = null;
+ }
+ singleAllocationReleaseHolder = new Allocation[1];
+ }
+
+ public synchronized void reset() {
+ if (trimOnReset) {
+ setTargetBufferSize(0);
+ }
+ }
+
+ public synchronized void setTargetBufferSize(int targetBufferSize) {
+ boolean targetBufferSizeReduced = targetBufferSize < this.targetBufferSize;
+ this.targetBufferSize = targetBufferSize;
+ if (targetBufferSizeReduced) {
+ trim();
+ }
+ }
+
+ @Override
+ public synchronized Allocation allocate() {
+ allocatedCount++;
+ Allocation allocation;
+ if (availableCount > 0) {
+ allocation = availableAllocations[--availableCount];
+ availableAllocations[availableCount] = null;
+ } else {
+ allocation = new Allocation(new byte[individualAllocationSize], 0);
+ }
+ return allocation;
+ }
+
+ @Override
+ public synchronized void release(Allocation allocation) {
+ singleAllocationReleaseHolder[0] = allocation;
+ release(singleAllocationReleaseHolder);
+ }
+
+ @Override
+ public synchronized void release(Allocation[] allocations) {
+ if (availableCount + allocations.length >= availableAllocations.length) {
+ availableAllocations = Arrays.copyOf(availableAllocations,
+ Math.max(availableAllocations.length * 2, availableCount + allocations.length));
+ }
+ for (Allocation allocation : allocations) {
+ availableAllocations[availableCount++] = allocation;
+ }
+ allocatedCount -= allocations.length;
+ // Wake up threads waiting for the allocated size to drop.
+ notifyAll();
+ }
+
+ @Override
+ public synchronized void trim() {
+ int targetAllocationCount = Util.ceilDivide(targetBufferSize, individualAllocationSize);
+ int targetAvailableCount = Math.max(0, targetAllocationCount - allocatedCount);
+ if (targetAvailableCount >= availableCount) {
+ // We're already at or below the target.
+ return;
+ }
+
+ if (initialAllocationBlock != null) {
+ // Some allocations are backed by an initial block. We need to make sure that we hold onto all
+ // such allocations. Re-order the available allocations so that the ones backed by the initial
+ // block come first.
+ int lowIndex = 0;
+ int highIndex = availableCount - 1;
+ while (lowIndex <= highIndex) {
+ Allocation lowAllocation = availableAllocations[lowIndex];
+ if (lowAllocation.data == initialAllocationBlock) {
+ lowIndex++;
+ } else {
+ Allocation highAllocation = availableAllocations[highIndex];
+ if (highAllocation.data != initialAllocationBlock) {
+ highIndex--;
+ } else {
+ availableAllocations[lowIndex++] = highAllocation;
+ availableAllocations[highIndex--] = lowAllocation;
+ }
+ }
+ }
+ // lowIndex is the index of the first allocation not backed by an initial block.
+ targetAvailableCount = Math.max(targetAvailableCount, lowIndex);
+ if (targetAvailableCount >= availableCount) {
+ // We're already at or below the target.
+ return;
+ }
+ }
+
+ // Discard allocations beyond the target.
+ Arrays.fill(availableAllocations, targetAvailableCount, availableCount, null);
+ availableCount = targetAvailableCount;
+ }
+
+ @Override
+ public synchronized int getTotalBytesAllocated() {
+ return allocatedCount * individualAllocationSize;
+ }
+
+ @Override
+ public int getIndividualAllocationLength() {
+ return individualAllocationSize;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java
new file mode 100644
index 0000000000..63ca7c7eac
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java
@@ -0,0 +1,731 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.ConnectivityManager;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.SparseArray;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Clock;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.EventDispatcher;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.SlidingPercentile;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+
+/**
+ * Estimates bandwidth by listening to data transfers.
+ *
+ * <p>The bandwidth estimate is calculated using a {@link SlidingPercentile} and is updated each
+ * time a transfer ends. The initial estimate is based on the current operator's network country
+ * code or the locale of the user, as well as the network connection type. This can be configured in
+ * the {@link Builder}.
+ */
+public final class DefaultBandwidthMeter implements BandwidthMeter, TransferListener {
+
+ /**
+ * Country groups used to determine the default initial bitrate estimate. The group assignment for
+ * each country is an array of group indices for [Wifi, 2G, 3G, 4G].
+ */
+ public static final Map<String, int[]> DEFAULT_INITIAL_BITRATE_COUNTRY_GROUPS =
+ createInitialBitrateCountryGroupAssignment();
+
+ /** Default initial Wifi bitrate estimate in bits per second. */
+ public static final long[] DEFAULT_INITIAL_BITRATE_ESTIMATES_WIFI =
+ new long[] {5_700_000, 3_500_000, 2_000_000, 1_100_000, 470_000};
+
+ /** Default initial 2G bitrate estimates in bits per second. */
+ public static final long[] DEFAULT_INITIAL_BITRATE_ESTIMATES_2G =
+ new long[] {200_000, 148_000, 132_000, 115_000, 95_000};
+
+ /** Default initial 3G bitrate estimates in bits per second. */
+ public static final long[] DEFAULT_INITIAL_BITRATE_ESTIMATES_3G =
+ new long[] {2_200_000, 1_300_000, 970_000, 810_000, 490_000};
+
+ /** Default initial 4G bitrate estimates in bits per second. */
+ public static final long[] DEFAULT_INITIAL_BITRATE_ESTIMATES_4G =
+ new long[] {5_300_000, 3_200_000, 2_000_000, 1_400_000, 690_000};
+
+ /**
+ * Default initial bitrate estimate used when the device is offline or the network type cannot be
+ * determined, in bits per second.
+ */
+ public static final long DEFAULT_INITIAL_BITRATE_ESTIMATE = 1_000_000;
+
+ /** Default maximum weight for the sliding window. */
+ public static final int DEFAULT_SLIDING_WINDOW_MAX_WEIGHT = 2000;
+
+ @Nullable private static DefaultBandwidthMeter singletonInstance;
+
+ /** Builder for a bandwidth meter. */
+ public static final class Builder {
+
+ @Nullable private final Context context;
+
+ private SparseArray<Long> initialBitrateEstimates;
+ private int slidingWindowMaxWeight;
+ private Clock clock;
+ private boolean resetOnNetworkTypeChange;
+
+ /**
+ * Creates a builder with default parameters and without listener.
+ *
+ * @param context A context.
+ */
+ public Builder(Context context) {
+ // Handling of null is for backward compatibility only.
+ this.context = context == null ? null : context.getApplicationContext();
+ initialBitrateEstimates = getInitialBitrateEstimatesForCountry(Util.getCountryCode(context));
+ slidingWindowMaxWeight = DEFAULT_SLIDING_WINDOW_MAX_WEIGHT;
+ clock = Clock.DEFAULT;
+ resetOnNetworkTypeChange = true;
+ }
+
+ /**
+ * Sets the maximum weight for the sliding window.
+ *
+ * @param slidingWindowMaxWeight The maximum weight for the sliding window.
+ * @return This builder.
+ */
+ public Builder setSlidingWindowMaxWeight(int slidingWindowMaxWeight) {
+ this.slidingWindowMaxWeight = slidingWindowMaxWeight;
+ return this;
+ }
+
+ /**
+ * Sets the initial bitrate estimate in bits per second that should be assumed when a bandwidth
+ * estimate is unavailable.
+ *
+ * @param initialBitrateEstimate The initial bitrate estimate in bits per second.
+ * @return This builder.
+ */
+ public Builder setInitialBitrateEstimate(long initialBitrateEstimate) {
+ for (int i = 0; i < initialBitrateEstimates.size(); i++) {
+ initialBitrateEstimates.setValueAt(i, initialBitrateEstimate);
+ }
+ return this;
+ }
+
+ /**
+ * Sets the initial bitrate estimate in bits per second that should be assumed when a bandwidth
+ * estimate is unavailable and the current network connection is of the specified type.
+ *
+ * @param networkType The {@link C.NetworkType} this initial estimate is for.
+ * @param initialBitrateEstimate The initial bitrate estimate in bits per second.
+ * @return This builder.
+ */
+ public Builder setInitialBitrateEstimate(
+ @C.NetworkType int networkType, long initialBitrateEstimate) {
+ initialBitrateEstimates.put(networkType, initialBitrateEstimate);
+ return this;
+ }
+
+ /**
+ * Sets the initial bitrate estimates to the default values of the specified country. The
+ * initial estimates are used when a bandwidth estimate is unavailable.
+ *
+ * @param countryCode The ISO 3166-1 alpha-2 country code of the country whose default bitrate
+ * estimates should be used.
+ * @return This builder.
+ */
+ public Builder setInitialBitrateEstimate(String countryCode) {
+ initialBitrateEstimates =
+ getInitialBitrateEstimatesForCountry(Util.toUpperInvariant(countryCode));
+ return this;
+ }
+
+ /**
+ * Sets the clock used to estimate bandwidth from data transfers. Should only be set for testing
+ * purposes.
+ *
+ * @param clock The clock used to estimate bandwidth from data transfers.
+ * @return This builder.
+ */
+ public Builder setClock(Clock clock) {
+ this.clock = clock;
+ return this;
+ }
+
+ /**
+ * Sets whether to reset if the network type changes. The default value is {@code true}.
+ *
+ * @param resetOnNetworkTypeChange Whether to reset if the network type changes.
+ * @return This builder.
+ */
+ public Builder setResetOnNetworkTypeChange(boolean resetOnNetworkTypeChange) {
+ this.resetOnNetworkTypeChange = resetOnNetworkTypeChange;
+ return this;
+ }
+
+ /**
+ * Builds the bandwidth meter.
+ *
+ * @return A bandwidth meter with the configured properties.
+ */
+ public DefaultBandwidthMeter build() {
+ return new DefaultBandwidthMeter(
+ context,
+ initialBitrateEstimates,
+ slidingWindowMaxWeight,
+ clock,
+ resetOnNetworkTypeChange);
+ }
+
+ private static SparseArray<Long> getInitialBitrateEstimatesForCountry(String countryCode) {
+ int[] groupIndices = getCountryGroupIndices(countryCode);
+ SparseArray<Long> result = new SparseArray<>(/* initialCapacity= */ 6);
+ result.append(C.NETWORK_TYPE_UNKNOWN, DEFAULT_INITIAL_BITRATE_ESTIMATE);
+ result.append(C.NETWORK_TYPE_WIFI, DEFAULT_INITIAL_BITRATE_ESTIMATES_WIFI[groupIndices[0]]);
+ result.append(C.NETWORK_TYPE_2G, DEFAULT_INITIAL_BITRATE_ESTIMATES_2G[groupIndices[1]]);
+ result.append(C.NETWORK_TYPE_3G, DEFAULT_INITIAL_BITRATE_ESTIMATES_3G[groupIndices[2]]);
+ result.append(C.NETWORK_TYPE_4G, DEFAULT_INITIAL_BITRATE_ESTIMATES_4G[groupIndices[3]]);
+ // Assume default Wifi bitrate for Ethernet and 5G to prevent using the slower fallback.
+ result.append(
+ C.NETWORK_TYPE_ETHERNET, DEFAULT_INITIAL_BITRATE_ESTIMATES_WIFI[groupIndices[0]]);
+ result.append(C.NETWORK_TYPE_5G, DEFAULT_INITIAL_BITRATE_ESTIMATES_WIFI[groupIndices[0]]);
+ return result;
+ }
+
+ private static int[] getCountryGroupIndices(String countryCode) {
+ int[] groupIndices = DEFAULT_INITIAL_BITRATE_COUNTRY_GROUPS.get(countryCode);
+ // Assume median group if not found.
+ return groupIndices == null ? new int[] {2, 2, 2, 2} : groupIndices;
+ }
+ }
+
+ /**
+ * Returns a singleton instance of a {@link DefaultBandwidthMeter} with default configuration.
+ *
+ * @param context A {@link Context}.
+ * @return The singleton instance.
+ */
+ public static synchronized DefaultBandwidthMeter getSingletonInstance(Context context) {
+ if (singletonInstance == null) {
+ singletonInstance = new DefaultBandwidthMeter.Builder(context).build();
+ }
+ return singletonInstance;
+ }
+
+ private static final int ELAPSED_MILLIS_FOR_ESTIMATE = 2000;
+ private static final int BYTES_TRANSFERRED_FOR_ESTIMATE = 512 * 1024;
+
+ @Nullable private final Context context;
+ private final SparseArray<Long> initialBitrateEstimates;
+ private final EventDispatcher<EventListener> eventDispatcher;
+ private final SlidingPercentile slidingPercentile;
+ private final Clock clock;
+
+ private int streamCount;
+ private long sampleStartTimeMs;
+ private long sampleBytesTransferred;
+
+ @C.NetworkType private int networkType;
+ private long totalElapsedTimeMs;
+ private long totalBytesTransferred;
+ private long bitrateEstimate;
+ private long lastReportedBitrateEstimate;
+
+ private boolean networkTypeOverrideSet;
+ @C.NetworkType private int networkTypeOverride;
+
+ /** @deprecated Use {@link Builder} instead. */
+ @Deprecated
+ public DefaultBandwidthMeter() {
+ this(
+ /* context= */ null,
+ /* initialBitrateEstimates= */ new SparseArray<>(),
+ DEFAULT_SLIDING_WINDOW_MAX_WEIGHT,
+ Clock.DEFAULT,
+ /* resetOnNetworkTypeChange= */ false);
+ }
+
+ private DefaultBandwidthMeter(
+ @Nullable Context context,
+ SparseArray<Long> initialBitrateEstimates,
+ int maxWeight,
+ Clock clock,
+ boolean resetOnNetworkTypeChange) {
+ this.context = context == null ? null : context.getApplicationContext();
+ this.initialBitrateEstimates = initialBitrateEstimates;
+ this.eventDispatcher = new EventDispatcher<>();
+ this.slidingPercentile = new SlidingPercentile(maxWeight);
+ this.clock = clock;
+ // Set the initial network type and bitrate estimate
+ networkType = context == null ? C.NETWORK_TYPE_UNKNOWN : Util.getNetworkType(context);
+ bitrateEstimate = getInitialBitrateEstimateForNetworkType(networkType);
+ // Register to receive connectivity actions if possible.
+ if (context != null && resetOnNetworkTypeChange) {
+ ConnectivityActionReceiver connectivityActionReceiver =
+ ConnectivityActionReceiver.getInstance(context);
+ connectivityActionReceiver.register(/* bandwidthMeter= */ this);
+ }
+ }
+
+ /**
+ * Overrides the network type. Handled in the same way as if the meter had detected a change from
+ * the current network type to the specified network type internally.
+ *
+ * <p>Applications should not normally call this method. It is intended for testing purposes.
+ *
+ * @param networkType The overriding network type.
+ */
+ public synchronized void setNetworkTypeOverride(@C.NetworkType int networkType) {
+ networkTypeOverride = networkType;
+ networkTypeOverrideSet = true;
+ onConnectivityAction();
+ }
+
+ @Override
+ public synchronized long getBitrateEstimate() {
+ return bitrateEstimate;
+ }
+
+ @Override
+ @Nullable
+ public TransferListener getTransferListener() {
+ return this;
+ }
+
+ @Override
+ public void addEventListener(Handler eventHandler, EventListener eventListener) {
+ eventDispatcher.addListener(eventHandler, eventListener);
+ }
+
+ @Override
+ public void removeEventListener(EventListener eventListener) {
+ eventDispatcher.removeListener(eventListener);
+ }
+
+ @Override
+ public void onTransferInitializing(DataSource source, DataSpec dataSpec, boolean isNetwork) {
+ // Do nothing.
+ }
+
+ @Override
+ public synchronized void onTransferStart(
+ DataSource source, DataSpec dataSpec, boolean isNetwork) {
+ if (!isNetwork) {
+ return;
+ }
+ if (streamCount == 0) {
+ sampleStartTimeMs = clock.elapsedRealtime();
+ }
+ streamCount++;
+ }
+
+ @Override
+ public synchronized void onBytesTransferred(
+ DataSource source, DataSpec dataSpec, boolean isNetwork, int bytes) {
+ if (!isNetwork) {
+ return;
+ }
+ sampleBytesTransferred += bytes;
+ }
+
+ @Override
+ public synchronized void onTransferEnd(DataSource source, DataSpec dataSpec, boolean isNetwork) {
+ if (!isNetwork) {
+ return;
+ }
+ Assertions.checkState(streamCount > 0);
+ long nowMs = clock.elapsedRealtime();
+ int sampleElapsedTimeMs = (int) (nowMs - sampleStartTimeMs);
+ totalElapsedTimeMs += sampleElapsedTimeMs;
+ totalBytesTransferred += sampleBytesTransferred;
+ if (sampleElapsedTimeMs > 0) {
+ float bitsPerSecond = (sampleBytesTransferred * 8000f) / sampleElapsedTimeMs;
+ slidingPercentile.addSample((int) Math.sqrt(sampleBytesTransferred), bitsPerSecond);
+ if (totalElapsedTimeMs >= ELAPSED_MILLIS_FOR_ESTIMATE
+ || totalBytesTransferred >= BYTES_TRANSFERRED_FOR_ESTIMATE) {
+ bitrateEstimate = (long) slidingPercentile.getPercentile(0.5f);
+ }
+ maybeNotifyBandwidthSample(sampleElapsedTimeMs, sampleBytesTransferred, bitrateEstimate);
+ sampleStartTimeMs = nowMs;
+ sampleBytesTransferred = 0;
+ } // Else any sample bytes transferred will be carried forward into the next sample.
+ streamCount--;
+ }
+
+ private synchronized void onConnectivityAction() {
+ int networkType =
+ networkTypeOverrideSet
+ ? networkTypeOverride
+ : (context == null ? C.NETWORK_TYPE_UNKNOWN : Util.getNetworkType(context));
+ if (this.networkType == networkType) {
+ return;
+ }
+
+ this.networkType = networkType;
+ if (networkType == C.NETWORK_TYPE_OFFLINE
+ || networkType == C.NETWORK_TYPE_UNKNOWN
+ || networkType == C.NETWORK_TYPE_OTHER) {
+ // It's better not to reset the bandwidth meter for these network types.
+ return;
+ }
+
+ // Reset the bitrate estimate and report it, along with any bytes transferred.
+ this.bitrateEstimate = getInitialBitrateEstimateForNetworkType(networkType);
+ long nowMs = clock.elapsedRealtime();
+ int sampleElapsedTimeMs = streamCount > 0 ? (int) (nowMs - sampleStartTimeMs) : 0;
+ maybeNotifyBandwidthSample(sampleElapsedTimeMs, sampleBytesTransferred, bitrateEstimate);
+
+ // Reset the remainder of the state.
+ sampleStartTimeMs = nowMs;
+ sampleBytesTransferred = 0;
+ totalBytesTransferred = 0;
+ totalElapsedTimeMs = 0;
+ slidingPercentile.reset();
+ }
+
+ private void maybeNotifyBandwidthSample(
+ int elapsedMs, long bytesTransferred, long bitrateEstimate) {
+ if (elapsedMs == 0 && bytesTransferred == 0 && bitrateEstimate == lastReportedBitrateEstimate) {
+ return;
+ }
+ lastReportedBitrateEstimate = bitrateEstimate;
+ eventDispatcher.dispatch(
+ listener -> listener.onBandwidthSample(elapsedMs, bytesTransferred, bitrateEstimate));
+ }
+
+ private long getInitialBitrateEstimateForNetworkType(@C.NetworkType int networkType) {
+ Long initialBitrateEstimate = initialBitrateEstimates.get(networkType);
+ if (initialBitrateEstimate == null) {
+ initialBitrateEstimate = initialBitrateEstimates.get(C.NETWORK_TYPE_UNKNOWN);
+ }
+ if (initialBitrateEstimate == null) {
+ initialBitrateEstimate = DEFAULT_INITIAL_BITRATE_ESTIMATE;
+ }
+ return initialBitrateEstimate;
+ }
+
+ /*
+ * Note: This class only holds a weak reference to DefaultBandwidthMeter instances. It should not
+ * be made non-static, since doing so adds a strong reference (i.e. DefaultBandwidthMeter.this).
+ */
+ private static class ConnectivityActionReceiver extends BroadcastReceiver {
+
+ private static @MonotonicNonNull ConnectivityActionReceiver staticInstance;
+
+ private final Handler mainHandler;
+ private final ArrayList<WeakReference<DefaultBandwidthMeter>> bandwidthMeters;
+
+ public static synchronized ConnectivityActionReceiver getInstance(Context context) {
+ if (staticInstance == null) {
+ staticInstance = new ConnectivityActionReceiver();
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
+ context.registerReceiver(staticInstance, filter);
+ }
+ return staticInstance;
+ }
+
+ private ConnectivityActionReceiver() {
+ mainHandler = new Handler(Looper.getMainLooper());
+ bandwidthMeters = new ArrayList<>();
+ }
+
+ public synchronized void register(DefaultBandwidthMeter bandwidthMeter) {
+ removeClearedReferences();
+ bandwidthMeters.add(new WeakReference<>(bandwidthMeter));
+ // Simulate an initial update on the main thread (like the sticky broadcast we'd receive if
+ // we were to register a separate broadcast receiver for each bandwidth meter).
+ mainHandler.post(() -> updateBandwidthMeter(bandwidthMeter));
+ }
+
+ @Override
+ public synchronized void onReceive(Context context, Intent intent) {
+ if (isInitialStickyBroadcast()) {
+ return;
+ }
+ removeClearedReferences();
+ for (int i = 0; i < bandwidthMeters.size(); i++) {
+ WeakReference<DefaultBandwidthMeter> bandwidthMeterReference = bandwidthMeters.get(i);
+ DefaultBandwidthMeter bandwidthMeter = bandwidthMeterReference.get();
+ if (bandwidthMeter != null) {
+ updateBandwidthMeter(bandwidthMeter);
+ }
+ }
+ }
+
+ private void updateBandwidthMeter(DefaultBandwidthMeter bandwidthMeter) {
+ bandwidthMeter.onConnectivityAction();
+ }
+
+ private void removeClearedReferences() {
+ for (int i = bandwidthMeters.size() - 1; i >= 0; i--) {
+ WeakReference<DefaultBandwidthMeter> bandwidthMeterReference = bandwidthMeters.get(i);
+ DefaultBandwidthMeter bandwidthMeter = bandwidthMeterReference.get();
+ if (bandwidthMeter == null) {
+ bandwidthMeters.remove(i);
+ }
+ }
+ }
+ }
+
+ private static Map<String, int[]> createInitialBitrateCountryGroupAssignment() {
+ HashMap<String, int[]> countryGroupAssignment = new HashMap<>();
+ countryGroupAssignment.put("AD", new int[] {1, 1, 0, 0});
+ countryGroupAssignment.put("AE", new int[] {1, 4, 4, 4});
+ countryGroupAssignment.put("AF", new int[] {4, 4, 3, 3});
+ countryGroupAssignment.put("AG", new int[] {3, 1, 0, 1});
+ countryGroupAssignment.put("AI", new int[] {1, 0, 0, 3});
+ countryGroupAssignment.put("AL", new int[] {1, 2, 0, 1});
+ countryGroupAssignment.put("AM", new int[] {2, 2, 2, 2});
+ countryGroupAssignment.put("AO", new int[] {3, 4, 2, 0});
+ countryGroupAssignment.put("AR", new int[] {2, 3, 2, 2});
+ countryGroupAssignment.put("AS", new int[] {3, 0, 4, 2});
+ countryGroupAssignment.put("AT", new int[] {0, 3, 0, 0});
+ countryGroupAssignment.put("AU", new int[] {0, 3, 0, 1});
+ countryGroupAssignment.put("AW", new int[] {1, 1, 0, 3});
+ countryGroupAssignment.put("AX", new int[] {0, 3, 0, 2});
+ countryGroupAssignment.put("AZ", new int[] {3, 3, 3, 3});
+ countryGroupAssignment.put("BA", new int[] {1, 1, 0, 1});
+ countryGroupAssignment.put("BB", new int[] {0, 2, 0, 0});
+ countryGroupAssignment.put("BD", new int[] {2, 1, 3, 3});
+ countryGroupAssignment.put("BE", new int[] {0, 0, 0, 1});
+ countryGroupAssignment.put("BF", new int[] {4, 4, 4, 1});
+ countryGroupAssignment.put("BG", new int[] {0, 1, 0, 0});
+ countryGroupAssignment.put("BH", new int[] {2, 1, 3, 4});
+ countryGroupAssignment.put("BI", new int[] {4, 4, 4, 4});
+ countryGroupAssignment.put("BJ", new int[] {4, 4, 4, 4});
+ countryGroupAssignment.put("BL", new int[] {1, 0, 2, 2});
+ countryGroupAssignment.put("BM", new int[] {1, 2, 0, 0});
+ countryGroupAssignment.put("BN", new int[] {4, 1, 3, 2});
+ countryGroupAssignment.put("BO", new int[] {1, 2, 3, 2});
+ countryGroupAssignment.put("BQ", new int[] {1, 1, 2, 4});
+ countryGroupAssignment.put("BR", new int[] {2, 3, 3, 2});
+ countryGroupAssignment.put("BS", new int[] {2, 1, 1, 4});
+ countryGroupAssignment.put("BT", new int[] {3, 0, 3, 1});
+ countryGroupAssignment.put("BW", new int[] {4, 4, 1, 2});
+ countryGroupAssignment.put("BY", new int[] {0, 1, 1, 2});
+ countryGroupAssignment.put("BZ", new int[] {2, 2, 2, 1});
+ countryGroupAssignment.put("CA", new int[] {0, 3, 1, 3});
+ countryGroupAssignment.put("CD", new int[] {4, 4, 2, 2});
+ countryGroupAssignment.put("CF", new int[] {4, 4, 3, 0});
+ countryGroupAssignment.put("CG", new int[] {3, 4, 2, 4});
+ countryGroupAssignment.put("CH", new int[] {0, 0, 1, 0});
+ countryGroupAssignment.put("CI", new int[] {3, 4, 3, 3});
+ countryGroupAssignment.put("CK", new int[] {2, 4, 1, 0});
+ countryGroupAssignment.put("CL", new int[] {1, 2, 2, 3});
+ countryGroupAssignment.put("CM", new int[] {3, 4, 3, 1});
+ countryGroupAssignment.put("CN", new int[] {2, 0, 2, 3});
+ countryGroupAssignment.put("CO", new int[] {2, 3, 2, 2});
+ countryGroupAssignment.put("CR", new int[] {2, 3, 4, 4});
+ countryGroupAssignment.put("CU", new int[] {4, 4, 3, 1});
+ countryGroupAssignment.put("CV", new int[] {2, 3, 1, 2});
+ countryGroupAssignment.put("CW", new int[] {1, 1, 0, 0});
+ countryGroupAssignment.put("CY", new int[] {1, 1, 0, 0});
+ countryGroupAssignment.put("CZ", new int[] {0, 1, 0, 0});
+ countryGroupAssignment.put("DE", new int[] {0, 1, 1, 3});
+ countryGroupAssignment.put("DJ", new int[] {4, 3, 4, 1});
+ countryGroupAssignment.put("DK", new int[] {0, 0, 1, 1});
+ countryGroupAssignment.put("DM", new int[] {1, 0, 1, 3});
+ countryGroupAssignment.put("DO", new int[] {3, 3, 4, 4});
+ countryGroupAssignment.put("DZ", new int[] {3, 3, 4, 4});
+ countryGroupAssignment.put("EC", new int[] {2, 3, 4, 3});
+ countryGroupAssignment.put("EE", new int[] {0, 1, 0, 0});
+ countryGroupAssignment.put("EG", new int[] {3, 4, 2, 2});
+ countryGroupAssignment.put("EH", new int[] {2, 0, 3, 3});
+ countryGroupAssignment.put("ER", new int[] {4, 2, 2, 0});
+ countryGroupAssignment.put("ES", new int[] {0, 1, 1, 1});
+ countryGroupAssignment.put("ET", new int[] {4, 4, 4, 0});
+ countryGroupAssignment.put("FI", new int[] {0, 0, 1, 0});
+ countryGroupAssignment.put("FJ", new int[] {3, 0, 3, 3});
+ countryGroupAssignment.put("FK", new int[] {3, 4, 2, 2});
+ countryGroupAssignment.put("FM", new int[] {4, 0, 4, 0});
+ countryGroupAssignment.put("FO", new int[] {0, 0, 0, 0});
+ countryGroupAssignment.put("FR", new int[] {1, 0, 3, 1});
+ countryGroupAssignment.put("GA", new int[] {3, 3, 2, 2});
+ countryGroupAssignment.put("GB", new int[] {0, 1, 3, 3});
+ countryGroupAssignment.put("GD", new int[] {2, 0, 4, 4});
+ countryGroupAssignment.put("GE", new int[] {1, 1, 1, 4});
+ countryGroupAssignment.put("GF", new int[] {2, 3, 4, 4});
+ countryGroupAssignment.put("GG", new int[] {0, 1, 0, 0});
+ countryGroupAssignment.put("GH", new int[] {3, 3, 2, 2});
+ countryGroupAssignment.put("GI", new int[] {0, 0, 0, 1});
+ countryGroupAssignment.put("GL", new int[] {2, 2, 0, 2});
+ countryGroupAssignment.put("GM", new int[] {4, 4, 3, 4});
+ countryGroupAssignment.put("GN", new int[] {3, 4, 4, 2});
+ countryGroupAssignment.put("GP", new int[] {2, 1, 1, 4});
+ countryGroupAssignment.put("GQ", new int[] {4, 4, 3, 0});
+ countryGroupAssignment.put("GR", new int[] {1, 1, 0, 2});
+ countryGroupAssignment.put("GT", new int[] {3, 3, 3, 3});
+ countryGroupAssignment.put("GU", new int[] {1, 2, 4, 4});
+ countryGroupAssignment.put("GW", new int[] {4, 4, 4, 1});
+ countryGroupAssignment.put("GY", new int[] {3, 2, 1, 1});
+ countryGroupAssignment.put("HK", new int[] {0, 2, 3, 4});
+ countryGroupAssignment.put("HN", new int[] {3, 2, 3, 2});
+ countryGroupAssignment.put("HR", new int[] {1, 1, 0, 1});
+ countryGroupAssignment.put("HT", new int[] {4, 4, 4, 4});
+ countryGroupAssignment.put("HU", new int[] {0, 1, 0, 0});
+ countryGroupAssignment.put("ID", new int[] {3, 2, 3, 4});
+ countryGroupAssignment.put("IE", new int[] {1, 0, 1, 1});
+ countryGroupAssignment.put("IL", new int[] {0, 0, 2, 3});
+ countryGroupAssignment.put("IM", new int[] {0, 0, 0, 1});
+ countryGroupAssignment.put("IN", new int[] {2, 2, 4, 4});
+ countryGroupAssignment.put("IO", new int[] {4, 2, 2, 2});
+ countryGroupAssignment.put("IQ", new int[] {3, 3, 4, 2});
+ countryGroupAssignment.put("IR", new int[] {3, 0, 2, 2});
+ countryGroupAssignment.put("IS", new int[] {0, 1, 0, 0});
+ countryGroupAssignment.put("IT", new int[] {1, 0, 1, 2});
+ countryGroupAssignment.put("JE", new int[] {1, 0, 0, 1});
+ countryGroupAssignment.put("JM", new int[] {2, 3, 3, 1});
+ countryGroupAssignment.put("JO", new int[] {1, 2, 1, 2});
+ countryGroupAssignment.put("JP", new int[] {0, 2, 1, 1});
+ countryGroupAssignment.put("KE", new int[] {3, 4, 4, 3});
+ countryGroupAssignment.put("KG", new int[] {1, 1, 2, 2});
+ countryGroupAssignment.put("KH", new int[] {1, 0, 4, 4});
+ countryGroupAssignment.put("KI", new int[] {4, 4, 4, 4});
+ countryGroupAssignment.put("KM", new int[] {4, 3, 2, 3});
+ countryGroupAssignment.put("KN", new int[] {1, 0, 1, 3});
+ countryGroupAssignment.put("KP", new int[] {4, 2, 4, 2});
+ countryGroupAssignment.put("KR", new int[] {0, 1, 1, 1});
+ countryGroupAssignment.put("KW", new int[] {2, 3, 1, 1});
+ countryGroupAssignment.put("KY", new int[] {1, 1, 0, 1});
+ countryGroupAssignment.put("KZ", new int[] {1, 2, 2, 3});
+ countryGroupAssignment.put("LA", new int[] {2, 2, 1, 1});
+ countryGroupAssignment.put("LB", new int[] {3, 2, 0, 0});
+ countryGroupAssignment.put("LC", new int[] {1, 1, 0, 0});
+ countryGroupAssignment.put("LI", new int[] {0, 0, 2, 4});
+ countryGroupAssignment.put("LK", new int[] {2, 1, 2, 3});
+ countryGroupAssignment.put("LR", new int[] {3, 4, 3, 1});
+ countryGroupAssignment.put("LS", new int[] {3, 3, 2, 0});
+ countryGroupAssignment.put("LT", new int[] {0, 0, 0, 0});
+ countryGroupAssignment.put("LU", new int[] {0, 0, 0, 0});
+ countryGroupAssignment.put("LV", new int[] {0, 0, 0, 0});
+ countryGroupAssignment.put("LY", new int[] {4, 4, 4, 4});
+ countryGroupAssignment.put("MA", new int[] {2, 1, 2, 1});
+ countryGroupAssignment.put("MC", new int[] {0, 0, 0, 1});
+ countryGroupAssignment.put("MD", new int[] {1, 1, 0, 0});
+ countryGroupAssignment.put("ME", new int[] {1, 2, 1, 2});
+ countryGroupAssignment.put("MF", new int[] {1, 1, 1, 1});
+ countryGroupAssignment.put("MG", new int[] {3, 4, 2, 2});
+ countryGroupAssignment.put("MH", new int[] {4, 0, 2, 4});
+ countryGroupAssignment.put("MK", new int[] {1, 0, 0, 0});
+ countryGroupAssignment.put("ML", new int[] {4, 4, 2, 0});
+ countryGroupAssignment.put("MM", new int[] {3, 3, 1, 2});
+ countryGroupAssignment.put("MN", new int[] {2, 3, 2, 3});
+ countryGroupAssignment.put("MO", new int[] {0, 0, 4, 4});
+ countryGroupAssignment.put("MP", new int[] {0, 2, 4, 4});
+ countryGroupAssignment.put("MQ", new int[] {2, 1, 1, 4});
+ countryGroupAssignment.put("MR", new int[] {4, 2, 4, 2});
+ countryGroupAssignment.put("MS", new int[] {1, 2, 3, 3});
+ countryGroupAssignment.put("MT", new int[] {0, 1, 0, 0});
+ countryGroupAssignment.put("MU", new int[] {2, 2, 3, 4});
+ countryGroupAssignment.put("MV", new int[] {4, 3, 0, 2});
+ countryGroupAssignment.put("MW", new int[] {3, 2, 1, 0});
+ countryGroupAssignment.put("MX", new int[] {2, 4, 4, 3});
+ countryGroupAssignment.put("MY", new int[] {2, 2, 3, 3});
+ countryGroupAssignment.put("MZ", new int[] {3, 3, 2, 1});
+ countryGroupAssignment.put("NA", new int[] {3, 3, 2, 1});
+ countryGroupAssignment.put("NC", new int[] {2, 0, 3, 3});
+ countryGroupAssignment.put("NE", new int[] {4, 4, 4, 3});
+ countryGroupAssignment.put("NF", new int[] {1, 2, 2, 2});
+ countryGroupAssignment.put("NG", new int[] {3, 4, 3, 1});
+ countryGroupAssignment.put("NI", new int[] {3, 3, 4, 4});
+ countryGroupAssignment.put("NL", new int[] {0, 2, 3, 3});
+ countryGroupAssignment.put("NO", new int[] {0, 1, 1, 0});
+ countryGroupAssignment.put("NP", new int[] {2, 2, 2, 2});
+ countryGroupAssignment.put("NR", new int[] {4, 0, 3, 1});
+ countryGroupAssignment.put("NZ", new int[] {0, 0, 1, 2});
+ countryGroupAssignment.put("OM", new int[] {3, 2, 1, 3});
+ countryGroupAssignment.put("PA", new int[] {1, 3, 3, 4});
+ countryGroupAssignment.put("PE", new int[] {2, 3, 4, 4});
+ countryGroupAssignment.put("PF", new int[] {2, 2, 0, 1});
+ countryGroupAssignment.put("PG", new int[] {4, 3, 3, 1});
+ countryGroupAssignment.put("PH", new int[] {3, 0, 3, 4});
+ countryGroupAssignment.put("PK", new int[] {3, 3, 3, 3});
+ countryGroupAssignment.put("PL", new int[] {1, 0, 1, 3});
+ countryGroupAssignment.put("PM", new int[] {0, 2, 2, 0});
+ countryGroupAssignment.put("PR", new int[] {1, 2, 3, 3});
+ countryGroupAssignment.put("PS", new int[] {3, 3, 2, 4});
+ countryGroupAssignment.put("PT", new int[] {1, 1, 0, 0});
+ countryGroupAssignment.put("PW", new int[] {2, 1, 2, 0});
+ countryGroupAssignment.put("PY", new int[] {2, 0, 2, 3});
+ countryGroupAssignment.put("QA", new int[] {2, 2, 1, 2});
+ countryGroupAssignment.put("RE", new int[] {1, 0, 2, 2});
+ countryGroupAssignment.put("RO", new int[] {0, 1, 1, 2});
+ countryGroupAssignment.put("RS", new int[] {1, 2, 0, 0});
+ countryGroupAssignment.put("RU", new int[] {0, 1, 1, 1});
+ countryGroupAssignment.put("RW", new int[] {4, 4, 2, 4});
+ countryGroupAssignment.put("SA", new int[] {2, 2, 2, 1});
+ countryGroupAssignment.put("SB", new int[] {4, 4, 3, 0});
+ countryGroupAssignment.put("SC", new int[] {4, 2, 0, 1});
+ countryGroupAssignment.put("SD", new int[] {4, 4, 4, 3});
+ countryGroupAssignment.put("SE", new int[] {0, 1, 0, 0});
+ countryGroupAssignment.put("SG", new int[] {0, 2, 3, 3});
+ countryGroupAssignment.put("SH", new int[] {4, 4, 2, 3});
+ countryGroupAssignment.put("SI", new int[] {0, 0, 0, 0});
+ countryGroupAssignment.put("SJ", new int[] {2, 0, 2, 4});
+ countryGroupAssignment.put("SK", new int[] {0, 1, 0, 0});
+ countryGroupAssignment.put("SL", new int[] {4, 3, 3, 3});
+ countryGroupAssignment.put("SM", new int[] {0, 0, 2, 4});
+ countryGroupAssignment.put("SN", new int[] {3, 4, 4, 2});
+ countryGroupAssignment.put("SO", new int[] {3, 4, 4, 3});
+ countryGroupAssignment.put("SR", new int[] {2, 2, 1, 0});
+ countryGroupAssignment.put("SS", new int[] {4, 3, 4, 3});
+ countryGroupAssignment.put("ST", new int[] {3, 4, 2, 2});
+ countryGroupAssignment.put("SV", new int[] {2, 3, 3, 4});
+ countryGroupAssignment.put("SX", new int[] {2, 4, 1, 0});
+ countryGroupAssignment.put("SY", new int[] {4, 3, 2, 1});
+ countryGroupAssignment.put("SZ", new int[] {4, 4, 3, 4});
+ countryGroupAssignment.put("TC", new int[] {1, 2, 1, 1});
+ countryGroupAssignment.put("TD", new int[] {4, 4, 4, 2});
+ countryGroupAssignment.put("TG", new int[] {3, 3, 1, 0});
+ countryGroupAssignment.put("TH", new int[] {1, 3, 4, 4});
+ countryGroupAssignment.put("TJ", new int[] {4, 4, 4, 4});
+ countryGroupAssignment.put("TL", new int[] {4, 2, 4, 4});
+ countryGroupAssignment.put("TM", new int[] {4, 1, 2, 2});
+ countryGroupAssignment.put("TN", new int[] {2, 2, 1, 2});
+ countryGroupAssignment.put("TO", new int[] {3, 3, 3, 1});
+ countryGroupAssignment.put("TR", new int[] {2, 2, 1, 2});
+ countryGroupAssignment.put("TT", new int[] {1, 3, 1, 2});
+ countryGroupAssignment.put("TV", new int[] {4, 2, 2, 4});
+ countryGroupAssignment.put("TW", new int[] {0, 0, 0, 0});
+ countryGroupAssignment.put("TZ", new int[] {3, 3, 4, 3});
+ countryGroupAssignment.put("UA", new int[] {0, 2, 1, 2});
+ countryGroupAssignment.put("UG", new int[] {4, 3, 3, 2});
+ countryGroupAssignment.put("US", new int[] {1, 1, 3, 3});
+ countryGroupAssignment.put("UY", new int[] {2, 2, 1, 1});
+ countryGroupAssignment.put("UZ", new int[] {2, 2, 2, 2});
+ countryGroupAssignment.put("VA", new int[] {1, 2, 4, 2});
+ countryGroupAssignment.put("VC", new int[] {2, 0, 2, 4});
+ countryGroupAssignment.put("VE", new int[] {4, 4, 4, 3});
+ countryGroupAssignment.put("VG", new int[] {3, 0, 1, 3});
+ countryGroupAssignment.put("VI", new int[] {1, 1, 4, 4});
+ countryGroupAssignment.put("VN", new int[] {0, 2, 4, 4});
+ countryGroupAssignment.put("VU", new int[] {4, 1, 3, 1});
+ countryGroupAssignment.put("WS", new int[] {3, 3, 3, 2});
+ countryGroupAssignment.put("XK", new int[] {1, 2, 1, 0});
+ countryGroupAssignment.put("YE", new int[] {4, 4, 4, 3});
+ countryGroupAssignment.put("YT", new int[] {2, 2, 2, 3});
+ countryGroupAssignment.put("ZA", new int[] {2, 4, 2, 2});
+ countryGroupAssignment.put("ZM", new int[] {3, 2, 2, 1});
+ countryGroupAssignment.put("ZW", new int[] {3, 3, 2, 1});
+ return Collections.unmodifiableMap(countryGroupAssignment);
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultDataSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultDataSource.java
new file mode 100644
index 0000000000..87e1c728a0
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultDataSource.java
@@ -0,0 +1,289 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream;
+
+import android.content.Context;
+import android.net.Uri;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A {@link DataSource} that supports multiple URI schemes. The supported schemes are:
+ *
+ * <ul>
+ * <li>file: For fetching data from a local file (e.g. file:///path/to/media/media.mp4, or just
+ * /path/to/media/media.mp4 because the implementation assumes that a URI without a scheme is
+ * a local file URI).
+ * <li>asset: For fetching data from an asset in the application's apk (e.g. asset:///media.mp4).
+ * <li>rawresource: For fetching data from a raw resource in the application's apk (e.g.
+ * rawresource:///resourceId, where rawResourceId is the integer identifier of the raw
+ * resource).
+ * <li>content: For fetching data from a content URI (e.g. content://authority/path/123).
+ * <li>rtmp: For fetching data over RTMP. Only supported if the project using ExoPlayer has an
+ * explicit dependency on ExoPlayer's RTMP extension.
+ * <li>data: For parsing data inlined in the URI as defined in RFC 2397.
+ * <li>udp: For fetching data over UDP (e.g. udp://something.com/media).
+ * <li>http(s): For fetching data over HTTP and HTTPS (e.g. https://www.something.com/media.mp4),
+ * if constructed using {@link #DefaultDataSource(Context, String, boolean)}, or any other
+ * schemes supported by a base data source if constructed using {@link
+ * #DefaultDataSource(Context, DataSource)}.
+ * </ul>
+ */
+public final class DefaultDataSource implements DataSource {
+
+ private static final String TAG = "DefaultDataSource";
+
+ private static final String SCHEME_ASSET = "asset";
+ private static final String SCHEME_CONTENT = "content";
+ private static final String SCHEME_RTMP = "rtmp";
+ private static final String SCHEME_UDP = "udp";
+ private static final String SCHEME_RAW = RawResourceDataSource.RAW_RESOURCE_SCHEME;
+
+ private final Context context;
+ private final List<TransferListener> transferListeners;
+ private final DataSource baseDataSource;
+
+ // Lazily initialized.
+ @Nullable private DataSource fileDataSource;
+ @Nullable private DataSource assetDataSource;
+ @Nullable private DataSource contentDataSource;
+ @Nullable private DataSource rtmpDataSource;
+ @Nullable private DataSource udpDataSource;
+ @Nullable private DataSource dataSchemeDataSource;
+ @Nullable private DataSource rawResourceDataSource;
+
+ @Nullable private DataSource dataSource;
+
+ /**
+ * Constructs a new instance, optionally configured to follow cross-protocol redirects.
+ *
+ * @param context A context.
+ * @param userAgent The User-Agent to use when requesting remote data.
+ * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP
+ * to HTTPS and vice versa) are enabled when fetching remote data.
+ */
+ public DefaultDataSource(Context context, String userAgent, boolean allowCrossProtocolRedirects) {
+ this(
+ context,
+ userAgent,
+ DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS,
+ DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS,
+ allowCrossProtocolRedirects);
+ }
+
+ /**
+ * Constructs a new instance, optionally configured to follow cross-protocol redirects.
+ *
+ * @param context A context.
+ * @param userAgent The User-Agent to use when requesting remote data.
+ * @param connectTimeoutMillis The connection timeout that should be used when requesting remote
+ * data, in milliseconds. A timeout of zero is interpreted as an infinite timeout.
+ * @param readTimeoutMillis The read timeout that should be used when requesting remote data, in
+ * milliseconds. A timeout of zero is interpreted as an infinite timeout.
+ * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP
+ * to HTTPS and vice versa) are enabled when fetching remote data.
+ */
+ public DefaultDataSource(
+ Context context,
+ String userAgent,
+ int connectTimeoutMillis,
+ int readTimeoutMillis,
+ boolean allowCrossProtocolRedirects) {
+ this(
+ context,
+ new DefaultHttpDataSource(
+ userAgent,
+ connectTimeoutMillis,
+ readTimeoutMillis,
+ allowCrossProtocolRedirects,
+ /* defaultRequestProperties= */ null));
+ }
+
+ /**
+ * Constructs a new instance that delegates to a provided {@link DataSource} for URI schemes other
+ * than file, asset and content.
+ *
+ * @param context A context.
+ * @param baseDataSource A {@link DataSource} to use for URI schemes other than file, asset and
+ * content. This {@link DataSource} should normally support at least http(s).
+ */
+ public DefaultDataSource(Context context, DataSource baseDataSource) {
+ this.context = context.getApplicationContext();
+ this.baseDataSource = Assertions.checkNotNull(baseDataSource);
+ transferListeners = new ArrayList<>();
+ }
+
+ @Override
+ public void addTransferListener(TransferListener transferListener) {
+ baseDataSource.addTransferListener(transferListener);
+ transferListeners.add(transferListener);
+ maybeAddListenerToDataSource(fileDataSource, transferListener);
+ maybeAddListenerToDataSource(assetDataSource, transferListener);
+ maybeAddListenerToDataSource(contentDataSource, transferListener);
+ maybeAddListenerToDataSource(rtmpDataSource, transferListener);
+ maybeAddListenerToDataSource(udpDataSource, transferListener);
+ maybeAddListenerToDataSource(dataSchemeDataSource, transferListener);
+ maybeAddListenerToDataSource(rawResourceDataSource, transferListener);
+ }
+
+ @Override
+ public long open(DataSpec dataSpec) throws IOException {
+ Assertions.checkState(dataSource == null);
+ // Choose the correct source for the scheme.
+ String scheme = dataSpec.uri.getScheme();
+ if (Util.isLocalFileUri(dataSpec.uri)) {
+ String uriPath = dataSpec.uri.getPath();
+ if (uriPath != null && uriPath.startsWith("/android_asset/")) {
+ dataSource = getAssetDataSource();
+ } else {
+ dataSource = getFileDataSource();
+ }
+ } else if (SCHEME_ASSET.equals(scheme)) {
+ dataSource = getAssetDataSource();
+ } else if (SCHEME_CONTENT.equals(scheme)) {
+ dataSource = getContentDataSource();
+ } else if (SCHEME_RTMP.equals(scheme)) {
+ dataSource = getRtmpDataSource();
+ } else if (SCHEME_UDP.equals(scheme)) {
+ dataSource = getUdpDataSource();
+ } else if (DataSchemeDataSource.SCHEME_DATA.equals(scheme)) {
+ dataSource = getDataSchemeDataSource();
+ } else if (SCHEME_RAW.equals(scheme)) {
+ dataSource = getRawResourceDataSource();
+ } else {
+ dataSource = baseDataSource;
+ }
+ // Open the source and return.
+ return dataSource.open(dataSpec);
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int readLength) throws IOException {
+ return Assertions.checkNotNull(dataSource).read(buffer, offset, readLength);
+ }
+
+ @Override
+ @Nullable
+ public Uri getUri() {
+ return dataSource == null ? null : dataSource.getUri();
+ }
+
+ @Override
+ public Map<String, List<String>> getResponseHeaders() {
+ return dataSource == null ? Collections.emptyMap() : dataSource.getResponseHeaders();
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (dataSource != null) {
+ try {
+ dataSource.close();
+ } finally {
+ dataSource = null;
+ }
+ }
+ }
+
+ private DataSource getUdpDataSource() {
+ if (udpDataSource == null) {
+ udpDataSource = new UdpDataSource();
+ addListenersToDataSource(udpDataSource);
+ }
+ return udpDataSource;
+ }
+
+ private DataSource getFileDataSource() {
+ if (fileDataSource == null) {
+ fileDataSource = new FileDataSource();
+ addListenersToDataSource(fileDataSource);
+ }
+ return fileDataSource;
+ }
+
+ private DataSource getAssetDataSource() {
+ if (assetDataSource == null) {
+ assetDataSource = new AssetDataSource(context);
+ addListenersToDataSource(assetDataSource);
+ }
+ return assetDataSource;
+ }
+
+ private DataSource getContentDataSource() {
+ if (contentDataSource == null) {
+ contentDataSource = new ContentDataSource(context);
+ addListenersToDataSource(contentDataSource);
+ }
+ return contentDataSource;
+ }
+
+ private DataSource getRtmpDataSource() {
+ if (rtmpDataSource == null) {
+ try {
+ // LINT.IfChange
+ Class<?> clazz = Class.forName("com.google.android.exoplayer2.ext.rtmp.RtmpDataSource");
+ rtmpDataSource = (DataSource) clazz.getConstructor().newInstance();
+ // LINT.ThenChange(../../../../../../../../proguard-rules.txt)
+ addListenersToDataSource(rtmpDataSource);
+ } catch (ClassNotFoundException e) {
+ // Expected if the app was built without the RTMP extension.
+ Log.w(TAG, "Attempting to play RTMP stream without depending on the RTMP extension");
+ } catch (Exception e) {
+ // The RTMP extension is present, but instantiation failed.
+ throw new RuntimeException("Error instantiating RTMP extension", e);
+ }
+ if (rtmpDataSource == null) {
+ rtmpDataSource = baseDataSource;
+ }
+ }
+ return rtmpDataSource;
+ }
+
+ private DataSource getDataSchemeDataSource() {
+ if (dataSchemeDataSource == null) {
+ dataSchemeDataSource = new DataSchemeDataSource();
+ addListenersToDataSource(dataSchemeDataSource);
+ }
+ return dataSchemeDataSource;
+ }
+
+ private DataSource getRawResourceDataSource() {
+ if (rawResourceDataSource == null) {
+ rawResourceDataSource = new RawResourceDataSource(context);
+ addListenersToDataSource(rawResourceDataSource);
+ }
+ return rawResourceDataSource;
+ }
+
+ private void addListenersToDataSource(DataSource dataSource) {
+ for (int i = 0; i < transferListeners.size(); i++) {
+ dataSource.addTransferListener(transferListeners.get(i));
+ }
+ }
+
+ private void maybeAddListenerToDataSource(
+ @Nullable DataSource dataSource, TransferListener listener) {
+ if (dataSource != null) {
+ dataSource.addTransferListener(listener);
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.java
new file mode 100644
index 0000000000..81add13c10
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream;
+
+import android.content.Context;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource.Factory;
+
+/**
+ * A {@link Factory} that produces {@link DefaultDataSource} instances that delegate to
+ * {@link DefaultHttpDataSource}s for non-file/asset/content URIs.
+ */
+public final class DefaultDataSourceFactory implements Factory {
+
+ private final Context context;
+ @Nullable private final TransferListener listener;
+ private final DataSource.Factory baseDataSourceFactory;
+
+ /**
+ * @param context A context.
+ * @param userAgent The User-Agent string that should be used.
+ */
+ public DefaultDataSourceFactory(Context context, String userAgent) {
+ this(context, userAgent, /* listener= */ null);
+ }
+
+ /**
+ * @param context A context.
+ * @param userAgent The User-Agent string that should be used.
+ * @param listener An optional listener.
+ */
+ public DefaultDataSourceFactory(
+ Context context, String userAgent, @Nullable TransferListener listener) {
+ this(context, listener, new DefaultHttpDataSourceFactory(userAgent, listener));
+ }
+
+ /**
+ * @param context A context.
+ * @param baseDataSourceFactory A {@link Factory} to be used to create a base {@link DataSource}
+ * for {@link DefaultDataSource}.
+ * @see DefaultDataSource#DefaultDataSource(Context, DataSource)
+ */
+ public DefaultDataSourceFactory(Context context, DataSource.Factory baseDataSourceFactory) {
+ this(context, /* listener= */ null, baseDataSourceFactory);
+ }
+
+ /**
+ * @param context A context.
+ * @param listener An optional listener.
+ * @param baseDataSourceFactory A {@link Factory} to be used to create a base {@link DataSource}
+ * for {@link DefaultDataSource}.
+ * @see DefaultDataSource#DefaultDataSource(Context, DataSource)
+ */
+ public DefaultDataSourceFactory(
+ Context context,
+ @Nullable TransferListener listener,
+ DataSource.Factory baseDataSourceFactory) {
+ this.context = context.getApplicationContext();
+ this.listener = listener;
+ this.baseDataSourceFactory = baseDataSourceFactory;
+ }
+
+ @Override
+ public DefaultDataSource createDataSource() {
+ DefaultDataSource dataSource =
+ new DefaultDataSource(context, baseDataSourceFactory.createDataSource());
+ if (listener != null) {
+ dataSource.addTransferListener(listener);
+ }
+ return dataSource;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java
new file mode 100644
index 0000000000..c0e8e23bfe
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java
@@ -0,0 +1,798 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream;
+
+import android.net.Uri;
+import android.text.TextUtils;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec.HttpMethod;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Predicate;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InterruptedIOException;
+import java.io.OutputStream;
+import java.lang.reflect.Method;
+import java.net.HttpURLConnection;
+import java.net.NoRouteToHostException;
+import java.net.ProtocolException;
+import java.net.Proxy;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.zip.GZIPInputStream;
+
+/**
+ * An {@link HttpDataSource} that uses Android's {@link HttpURLConnection}.
+ *
+ * <p>By default this implementation will not follow cross-protocol redirects (i.e. redirects from
+ * HTTP to HTTPS or vice versa). Cross-protocol redirects can be enabled by using the {@link
+ * #DefaultHttpDataSource(String, int, int, boolean, RequestProperties)} constructor and passing
+ * {@code true} for the {@code allowCrossProtocolRedirects} argument.
+ *
+ * <p>Note: HTTP request headers will be set using all parameters passed via (in order of decreasing
+ * priority) the {@code dataSpec}, {@link #setRequestProperty} and the default parameters used to
+ * construct the instance.
+ */
+public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSource {
+
+ /** The default connection timeout, in milliseconds. */
+ public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS = 8 * 1000;
+ /**
+ * The default read timeout, in milliseconds.
+ */
+ public static final int DEFAULT_READ_TIMEOUT_MILLIS = 8 * 1000;
+
+ private static final String TAG = "DefaultHttpDataSource";
+ private static final int MAX_REDIRECTS = 20; // Same limit as okhttp.
+ private static final int HTTP_STATUS_TEMPORARY_REDIRECT = 307;
+ private static final int HTTP_STATUS_PERMANENT_REDIRECT = 308;
+ private static final long MAX_BYTES_TO_DRAIN = 2048;
+ private static final Pattern CONTENT_RANGE_HEADER =
+ Pattern.compile("^bytes (\\d+)-(\\d+)/(\\d+)$");
+ private static final AtomicReference<byte[]> skipBufferReference = new AtomicReference<>();
+
+ private final boolean allowCrossProtocolRedirects;
+ private final int connectTimeoutMillis;
+ private final int readTimeoutMillis;
+ private final String userAgent;
+ @Nullable private final RequestProperties defaultRequestProperties;
+ private final RequestProperties requestProperties;
+
+ @Nullable private Predicate<String> contentTypePredicate;
+ @Nullable private DataSpec dataSpec;
+ @Nullable private HttpURLConnection connection;
+ @Nullable private InputStream inputStream;
+ private boolean opened;
+ private int responseCode;
+
+ private long bytesToSkip;
+ private long bytesToRead;
+
+ private long bytesSkipped;
+ private long bytesRead;
+
+ /** @param userAgent The User-Agent string that should be used. */
+ public DefaultHttpDataSource(String userAgent) {
+ this(userAgent, DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS);
+ }
+
+ /**
+ * @param userAgent The User-Agent string that should be used.
+ * @param connectTimeoutMillis The connection timeout, in milliseconds. A timeout of zero is
+ * interpreted as an infinite timeout.
+ * @param readTimeoutMillis The read timeout, in milliseconds. A timeout of zero is interpreted as
+ * an infinite timeout.
+ */
+ public DefaultHttpDataSource(String userAgent, int connectTimeoutMillis, int readTimeoutMillis) {
+ this(
+ userAgent,
+ connectTimeoutMillis,
+ readTimeoutMillis,
+ /* allowCrossProtocolRedirects= */ false,
+ /* defaultRequestProperties= */ null);
+ }
+
+ /**
+ * @param userAgent The User-Agent string that should be used.
+ * @param connectTimeoutMillis The connection timeout, in milliseconds. A timeout of zero is
+ * interpreted as an infinite timeout. Pass {@link #DEFAULT_CONNECT_TIMEOUT_MILLIS} to use the
+ * default value.
+ * @param readTimeoutMillis The read timeout, in milliseconds. A timeout of zero is interpreted as
+ * an infinite timeout. Pass {@link #DEFAULT_READ_TIMEOUT_MILLIS} to use the default value.
+ * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP
+ * to HTTPS and vice versa) are enabled.
+ * @param defaultRequestProperties The default request properties to be sent to the server as HTTP
+ * headers or {@code null} if not required.
+ */
+ public DefaultHttpDataSource(
+ String userAgent,
+ int connectTimeoutMillis,
+ int readTimeoutMillis,
+ boolean allowCrossProtocolRedirects,
+ @Nullable RequestProperties defaultRequestProperties) {
+ super(/* isNetwork= */ true);
+ this.userAgent = Assertions.checkNotEmpty(userAgent);
+ this.requestProperties = new RequestProperties();
+ this.connectTimeoutMillis = connectTimeoutMillis;
+ this.readTimeoutMillis = readTimeoutMillis;
+ this.allowCrossProtocolRedirects = allowCrossProtocolRedirects;
+ this.defaultRequestProperties = defaultRequestProperties;
+ }
+
+ /**
+ * @param userAgent The User-Agent string that should be used.
+ * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
+ * predicate then a {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link
+ * #open(DataSpec)}.
+ * @deprecated Use {@link #DefaultHttpDataSource(String)} and {@link
+ * #setContentTypePredicate(Predicate)}.
+ */
+ @Deprecated
+ public DefaultHttpDataSource(String userAgent, @Nullable Predicate<String> contentTypePredicate) {
+ this(
+ userAgent,
+ contentTypePredicate,
+ DEFAULT_CONNECT_TIMEOUT_MILLIS,
+ DEFAULT_READ_TIMEOUT_MILLIS);
+ }
+
+ /**
+ * @param userAgent The User-Agent string that should be used.
+ * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
+ * predicate then a {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link
+ * #open(DataSpec)}.
+ * @param connectTimeoutMillis The connection timeout, in milliseconds. A timeout of zero is
+ * interpreted as an infinite timeout.
+ * @param readTimeoutMillis The read timeout, in milliseconds. A timeout of zero is interpreted as
+ * an infinite timeout.
+ * @deprecated Use {@link #DefaultHttpDataSource(String, int, int)} and {@link
+ * #setContentTypePredicate(Predicate)}.
+ */
+ @SuppressWarnings("deprecation")
+ @Deprecated
+ public DefaultHttpDataSource(
+ String userAgent,
+ @Nullable Predicate<String> contentTypePredicate,
+ int connectTimeoutMillis,
+ int readTimeoutMillis) {
+ this(
+ userAgent,
+ contentTypePredicate,
+ connectTimeoutMillis,
+ readTimeoutMillis,
+ /* allowCrossProtocolRedirects= */ false,
+ /* defaultRequestProperties= */ null);
+ }
+
+ /**
+ * @param userAgent The User-Agent string that should be used.
+ * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
+ * predicate then a {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link
+ * #open(DataSpec)}.
+ * @param connectTimeoutMillis The connection timeout, in milliseconds. A timeout of zero is
+ * interpreted as an infinite timeout. Pass {@link #DEFAULT_CONNECT_TIMEOUT_MILLIS} to use the
+ * default value.
+ * @param readTimeoutMillis The read timeout, in milliseconds. A timeout of zero is interpreted as
+ * an infinite timeout. Pass {@link #DEFAULT_READ_TIMEOUT_MILLIS} to use the default value.
+ * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP
+ * to HTTPS and vice versa) are enabled.
+ * @param defaultRequestProperties The default request properties to be sent to the server as HTTP
+ * headers or {@code null} if not required.
+ * @deprecated Use {@link #DefaultHttpDataSource(String, int, int, boolean, RequestProperties)}
+ * and {@link #setContentTypePredicate(Predicate)}.
+ */
+ @Deprecated
+ public DefaultHttpDataSource(
+ String userAgent,
+ @Nullable Predicate<String> contentTypePredicate,
+ int connectTimeoutMillis,
+ int readTimeoutMillis,
+ boolean allowCrossProtocolRedirects,
+ @Nullable RequestProperties defaultRequestProperties) {
+ super(/* isNetwork= */ true);
+ this.userAgent = Assertions.checkNotEmpty(userAgent);
+ this.contentTypePredicate = contentTypePredicate;
+ this.requestProperties = new RequestProperties();
+ this.connectTimeoutMillis = connectTimeoutMillis;
+ this.readTimeoutMillis = readTimeoutMillis;
+ this.allowCrossProtocolRedirects = allowCrossProtocolRedirects;
+ this.defaultRequestProperties = defaultRequestProperties;
+ }
+
+ /**
+ * Sets a content type {@link Predicate}. If a content type is rejected by the predicate then a
+ * {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link #open(DataSpec)}.
+ *
+ * @param contentTypePredicate The content type {@link Predicate}, or {@code null} to clear a
+ * predicate that was previously set.
+ */
+ public void setContentTypePredicate(@Nullable Predicate<String> contentTypePredicate) {
+ this.contentTypePredicate = contentTypePredicate;
+ }
+
+ @Override
+ @Nullable
+ public Uri getUri() {
+ return connection == null ? null : Uri.parse(connection.getURL().toString());
+ }
+
+ @Override
+ public int getResponseCode() {
+ return connection == null || responseCode <= 0 ? -1 : responseCode;
+ }
+
+ @Override
+ public Map<String, List<String>> getResponseHeaders() {
+ return connection == null ? Collections.emptyMap() : connection.getHeaderFields();
+ }
+
+ @Override
+ public void setRequestProperty(String name, String value) {
+ Assertions.checkNotNull(name);
+ Assertions.checkNotNull(value);
+ requestProperties.set(name, value);
+ }
+
+ @Override
+ public void clearRequestProperty(String name) {
+ Assertions.checkNotNull(name);
+ requestProperties.remove(name);
+ }
+
+ @Override
+ public void clearAllRequestProperties() {
+ requestProperties.clear();
+ }
+
+ /**
+ * Opens the source to read the specified data.
+ */
+ @Override
+ public long open(DataSpec dataSpec) throws HttpDataSourceException {
+ this.dataSpec = dataSpec;
+ this.bytesRead = 0;
+ this.bytesSkipped = 0;
+ transferInitializing(dataSpec);
+ try {
+ connection = makeConnection(dataSpec);
+ } catch (IOException e) {
+ throw new HttpDataSourceException(
+ "Unable to connect", e, dataSpec, HttpDataSourceException.TYPE_OPEN);
+ } catch (URISyntaxException e) {
+ throw new HttpDataSourceException("URI invalid: " + dataSpec.uri.toString(), dataSpec, HttpDataSourceException.TYPE_OPEN);
+ }
+
+ String responseMessage;
+ try {
+ responseCode = connection.getResponseCode();
+ responseMessage = connection.getResponseMessage();
+ } catch (IOException e) {
+ closeConnectionQuietly();
+ throw new HttpDataSourceException(
+ "Unable to connect", e, dataSpec, HttpDataSourceException.TYPE_OPEN);
+ }
+
+ // Check for a valid response code.
+ if (responseCode < 200 || responseCode > 299) {
+ Map<String, List<String>> headers = connection.getHeaderFields();
+ closeConnectionQuietly();
+ InvalidResponseCodeException exception =
+ new InvalidResponseCodeException(responseCode, responseMessage, headers, dataSpec);
+ if (responseCode == 416) {
+ exception.initCause(new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE));
+ }
+ throw exception;
+ }
+
+ // Check for a valid content type.
+ String contentType = connection.getContentType();
+ if (contentTypePredicate != null && !contentTypePredicate.evaluate(contentType)) {
+ closeConnectionQuietly();
+ throw new InvalidContentTypeException(contentType, dataSpec);
+ }
+
+ // If we requested a range starting from a non-zero position and received a 200 rather than a
+ // 206, then the server does not support partial requests. We'll need to manually skip to the
+ // requested position.
+ bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0;
+
+ // Determine the length of the data to be read, after skipping.
+ boolean isCompressed = isCompressed(connection);
+ if (!isCompressed) {
+ if (dataSpec.length != C.LENGTH_UNSET) {
+ bytesToRead = dataSpec.length;
+ } else {
+ long contentLength = getContentLength(connection);
+ bytesToRead = contentLength != C.LENGTH_UNSET ? (contentLength - bytesToSkip)
+ : C.LENGTH_UNSET;
+ }
+ } else {
+ // Gzip is enabled. If the server opts to use gzip then the content length in the response
+ // will be that of the compressed data, which isn't what we want. Always use the dataSpec
+ // length in this case.
+ bytesToRead = dataSpec.length;
+ }
+
+ try {
+ inputStream = connection.getInputStream();
+ if (isCompressed) {
+ inputStream = new GZIPInputStream(inputStream);
+ }
+ } catch (IOException e) {
+ closeConnectionQuietly();
+ throw new HttpDataSourceException(e, dataSpec, HttpDataSourceException.TYPE_OPEN);
+ }
+
+ opened = true;
+ transferStarted(dataSpec);
+
+ return bytesToRead;
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int readLength) throws HttpDataSourceException {
+ try {
+ skipInternal();
+ return readInternal(buffer, offset, readLength);
+ } catch (IOException e) {
+ throw new HttpDataSourceException(e, dataSpec, HttpDataSourceException.TYPE_READ);
+ }
+ }
+
+ @Override
+ public void close() throws HttpDataSourceException {
+ try {
+ if (inputStream != null) {
+ maybeTerminateInputStream(connection, bytesRemaining());
+ try {
+ inputStream.close();
+ } catch (IOException e) {
+ throw new HttpDataSourceException(e, dataSpec, HttpDataSourceException.TYPE_CLOSE);
+ }
+ }
+ } finally {
+ inputStream = null;
+ closeConnectionQuietly();
+ if (opened) {
+ opened = false;
+ transferEnded();
+ }
+ }
+ }
+
+ /**
+ * Returns the current connection, or null if the source is not currently opened.
+ *
+ * @return The current open connection, or null.
+ */
+ protected final @Nullable HttpURLConnection getConnection() {
+ return connection;
+ }
+
+ /**
+ * Returns the number of bytes that have been skipped since the most recent call to
+ * {@link #open(DataSpec)}.
+ *
+ * @return The number of bytes skipped.
+ */
+ protected final long bytesSkipped() {
+ return bytesSkipped;
+ }
+
+ /**
+ * Returns the number of bytes that have been read since the most recent call to
+ * {@link #open(DataSpec)}.
+ *
+ * @return The number of bytes read.
+ */
+ protected final long bytesRead() {
+ return bytesRead;
+ }
+
+ /**
+ * Returns the number of bytes that are still to be read for the current {@link DataSpec}.
+ * <p>
+ * If the total length of the data being read is known, then this length minus {@code bytesRead()}
+ * is returned. If the total length is unknown, {@link C#LENGTH_UNSET} is returned.
+ *
+ * @return The remaining length, or {@link C#LENGTH_UNSET}.
+ */
+ protected final long bytesRemaining() {
+ return bytesToRead == C.LENGTH_UNSET ? bytesToRead : bytesToRead - bytesRead;
+ }
+
+ /**
+ * Establishes a connection, following redirects to do so where permitted.
+ */
+ private HttpURLConnection makeConnection(DataSpec dataSpec) throws IOException, URISyntaxException {
+ URL url = new URL(dataSpec.uri.toString());
+ @HttpMethod int httpMethod = dataSpec.httpMethod;
+ byte[] httpBody = dataSpec.httpBody;
+ long position = dataSpec.position;
+ long length = dataSpec.length;
+ boolean allowGzip = dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP);
+
+ if (!allowCrossProtocolRedirects) {
+ // HttpURLConnection disallows cross-protocol redirects, but otherwise performs redirection
+ // automatically. This is the behavior we want, so use it.
+ return makeConnection(
+ url,
+ httpMethod,
+ httpBody,
+ position,
+ length,
+ allowGzip,
+ /* followRedirects= */ true,
+ dataSpec.httpRequestHeaders);
+ }
+
+ // We need to handle redirects ourselves to allow cross-protocol redirects.
+ int redirectCount = 0;
+ while (redirectCount++ <= MAX_REDIRECTS) {
+ HttpURLConnection connection =
+ makeConnection(
+ url,
+ httpMethod,
+ httpBody,
+ position,
+ length,
+ allowGzip,
+ /* followRedirects= */ false,
+ dataSpec.httpRequestHeaders);
+ int responseCode = connection.getResponseCode();
+ String location = connection.getHeaderField("Location");
+ if ((httpMethod == DataSpec.HTTP_METHOD_GET || httpMethod == DataSpec.HTTP_METHOD_HEAD)
+ && (responseCode == HttpURLConnection.HTTP_MULT_CHOICE
+ || responseCode == HttpURLConnection.HTTP_MOVED_PERM
+ || responseCode == HttpURLConnection.HTTP_MOVED_TEMP
+ || responseCode == HttpURLConnection.HTTP_SEE_OTHER
+ || responseCode == HTTP_STATUS_TEMPORARY_REDIRECT
+ || responseCode == HTTP_STATUS_PERMANENT_REDIRECT)) {
+ connection.disconnect();
+ url = handleRedirect(url, location);
+ } else if (httpMethod == DataSpec.HTTP_METHOD_POST
+ && (responseCode == HttpURLConnection.HTTP_MULT_CHOICE
+ || responseCode == HttpURLConnection.HTTP_MOVED_PERM
+ || responseCode == HttpURLConnection.HTTP_MOVED_TEMP
+ || responseCode == HttpURLConnection.HTTP_SEE_OTHER)) {
+ // POST request follows the redirect and is transformed into a GET request.
+ connection.disconnect();
+ httpMethod = DataSpec.HTTP_METHOD_GET;
+ httpBody = null;
+ url = handleRedirect(url, location);
+ } else {
+ return connection;
+ }
+ }
+
+ // If we get here we've been redirected more times than are permitted.
+ throw new NoRouteToHostException("Too many redirects: " + redirectCount);
+ }
+
+ private static URLConnection openConnectionWithProxy(final URI uri) throws IOException {
+ final java.net.ProxySelector ps = java.net.ProxySelector.getDefault();
+ Proxy proxy = Proxy.NO_PROXY;
+ if (ps != null) {
+ final List<Proxy> proxies = ps.select(uri);
+ if (proxies != null && !proxies.isEmpty()) {
+ proxy = proxies.get(0);
+ }
+ }
+
+ return uri.toURL().openConnection(proxy);
+ }
+
+ /**
+ * Configures a connection and opens it.
+ *
+ * @param url The url to connect to.
+ * @param httpMethod The http method.
+ * @param httpBody The body data.
+ * @param position The byte offset of the requested data.
+ * @param length The length of the requested data, or {@link C#LENGTH_UNSET}.
+ * @param allowGzip Whether to allow the use of gzip.
+ * @param followRedirects Whether to follow redirects.
+ * @param requestParameters parameters (HTTP headers) to include in request.
+ */
+ private HttpURLConnection makeConnection(
+ URL url,
+ @HttpMethod int httpMethod,
+ byte[] httpBody,
+ long position,
+ long length,
+ boolean allowGzip,
+ boolean followRedirects,
+ Map<String, String> requestParameters)
+ throws IOException, URISyntaxException {
+ /**
+ * Tor Project modified the way the connection object was created. For the sake of
+ * simplicity, instead of duplicating the whole file we changed the connection object
+ * to use the ProxySelector.
+ */
+ HttpURLConnection connection = (HttpURLConnection) openConnectionWithProxy(url.toURI());
+
+ connection.setConnectTimeout(connectTimeoutMillis);
+ connection.setReadTimeout(readTimeoutMillis);
+
+ Map<String, String> requestHeaders = new HashMap<>();
+ if (defaultRequestProperties != null) {
+ requestHeaders.putAll(defaultRequestProperties.getSnapshot());
+ }
+ requestHeaders.putAll(requestProperties.getSnapshot());
+ requestHeaders.putAll(requestParameters);
+
+ for (Map.Entry<String, String> property : requestHeaders.entrySet()) {
+ connection.setRequestProperty(property.getKey(), property.getValue());
+ }
+
+ if (!(position == 0 && length == C.LENGTH_UNSET)) {
+ String rangeRequest = "bytes=" + position + "-";
+ if (length != C.LENGTH_UNSET) {
+ rangeRequest += (position + length - 1);
+ }
+ connection.setRequestProperty("Range", rangeRequest);
+ }
+ connection.setRequestProperty("User-Agent", userAgent);
+ connection.setRequestProperty("Accept-Encoding", allowGzip ? "gzip" : "identity");
+ connection.setInstanceFollowRedirects(followRedirects);
+ connection.setDoOutput(httpBody != null);
+ connection.setRequestMethod(DataSpec.getStringForHttpMethod(httpMethod));
+
+ if (httpBody != null) {
+ connection.setFixedLengthStreamingMode(httpBody.length);
+ connection.connect();
+ OutputStream os = connection.getOutputStream();
+ os.write(httpBody);
+ os.close();
+ } else {
+ connection.connect();
+ }
+ return connection;
+ }
+
+ /** Creates an {@link HttpURLConnection} that is connected with the {@code url}. */
+ @VisibleForTesting
+ /* package */ HttpURLConnection openConnection(URL url) throws IOException {
+ return (HttpURLConnection) url.openConnection();
+ }
+
+ /**
+ * Handles a redirect.
+ *
+ * @param originalUrl The original URL.
+ * @param location The Location header in the response.
+ * @return The next URL.
+ * @throws IOException If redirection isn't possible.
+ */
+ private static URL handleRedirect(URL originalUrl, String location) throws IOException {
+ if (location == null) {
+ throw new ProtocolException("Null location redirect");
+ }
+ // Form the new url.
+ URL url = new URL(originalUrl, location);
+ // Check that the protocol of the new url is supported.
+ String protocol = url.getProtocol();
+ if (!"https".equals(protocol) && !"http".equals(protocol)) {
+ throw new ProtocolException("Unsupported protocol redirect: " + protocol);
+ }
+ // Currently this method is only called if allowCrossProtocolRedirects is true, and so the code
+ // below isn't required. If we ever decide to handle redirects ourselves when cross-protocol
+ // redirects are disabled, we'll need to uncomment this block of code.
+ // if (!allowCrossProtocolRedirects && !protocol.equals(originalUrl.getProtocol())) {
+ // throw new ProtocolException("Disallowed cross-protocol redirect ("
+ // + originalUrl.getProtocol() + " to " + protocol + ")");
+ // }
+ return url;
+ }
+
+ /**
+ * Attempts to extract the length of the content from the response headers of an open connection.
+ *
+ * @param connection The open connection.
+ * @return The extracted length, or {@link C#LENGTH_UNSET}.
+ */
+ private static long getContentLength(HttpURLConnection connection) {
+ long contentLength = C.LENGTH_UNSET;
+ String contentLengthHeader = connection.getHeaderField("Content-Length");
+ if (!TextUtils.isEmpty(contentLengthHeader)) {
+ try {
+ contentLength = Long.parseLong(contentLengthHeader);
+ } catch (NumberFormatException e) {
+ Log.e(TAG, "Unexpected Content-Length [" + contentLengthHeader + "]");
+ }
+ }
+ String contentRangeHeader = connection.getHeaderField("Content-Range");
+ if (!TextUtils.isEmpty(contentRangeHeader)) {
+ Matcher matcher = CONTENT_RANGE_HEADER.matcher(contentRangeHeader);
+ if (matcher.find()) {
+ try {
+ long contentLengthFromRange =
+ Long.parseLong(matcher.group(2)) - Long.parseLong(matcher.group(1)) + 1;
+ if (contentLength < 0) {
+ // Some proxy servers strip the Content-Length header. Fall back to the length
+ // calculated here in this case.
+ contentLength = contentLengthFromRange;
+ } else if (contentLength != contentLengthFromRange) {
+ // If there is a discrepancy between the Content-Length and Content-Range headers,
+ // assume the one with the larger value is correct. We have seen cases where carrier
+ // change one of them to reduce the size of a request, but it is unlikely anybody would
+ // increase it.
+ Log.w(TAG, "Inconsistent headers [" + contentLengthHeader + "] [" + contentRangeHeader
+ + "]");
+ contentLength = Math.max(contentLength, contentLengthFromRange);
+ }
+ } catch (NumberFormatException e) {
+ Log.e(TAG, "Unexpected Content-Range [" + contentRangeHeader + "]");
+ }
+ }
+ }
+ return contentLength;
+ }
+
+ /**
+ * Skips any bytes that need skipping. Else does nothing.
+ * <p>
+ * This implementation is based roughly on {@code libcore.io.Streams.skipByReading()}.
+ *
+ * @throws InterruptedIOException If the thread is interrupted during the operation.
+ * @throws EOFException If the end of the input stream is reached before the bytes are skipped.
+ */
+ private void skipInternal() throws IOException {
+ if (bytesSkipped == bytesToSkip) {
+ return;
+ }
+
+ // Acquire the shared skip buffer.
+ byte[] skipBuffer = skipBufferReference.getAndSet(null);
+ if (skipBuffer == null) {
+ skipBuffer = new byte[4096];
+ }
+
+ while (bytesSkipped != bytesToSkip) {
+ int readLength = (int) Math.min(bytesToSkip - bytesSkipped, skipBuffer.length);
+ int read = inputStream.read(skipBuffer, 0, readLength);
+ if (Thread.currentThread().isInterrupted()) {
+ throw new InterruptedIOException();
+ }
+ if (read == -1) {
+ throw new EOFException();
+ }
+ bytesSkipped += read;
+ bytesTransferred(read);
+ }
+
+ // Release the shared skip buffer.
+ skipBufferReference.set(skipBuffer);
+ }
+
+ /**
+ * Reads up to {@code length} bytes of data and stores them into {@code buffer}, starting at
+ * index {@code offset}.
+ * <p>
+ * This method blocks until at least one byte of data can be read, the end of the opened range is
+ * detected, or an exception is thrown.
+ *
+ * @param buffer The buffer into which the read data should be stored.
+ * @param offset The start offset into {@code buffer} at which data should be written.
+ * @param readLength The maximum number of bytes to read.
+ * @return The number of bytes read, or {@link C#RESULT_END_OF_INPUT} if the end of the opened
+ * range is reached.
+ * @throws IOException If an error occurs reading from the source.
+ */
+ private int readInternal(byte[] buffer, int offset, int readLength) throws IOException {
+ if (readLength == 0) {
+ return 0;
+ }
+ if (bytesToRead != C.LENGTH_UNSET) {
+ long bytesRemaining = bytesToRead - bytesRead;
+ if (bytesRemaining == 0) {
+ return C.RESULT_END_OF_INPUT;
+ }
+ readLength = (int) Math.min(readLength, bytesRemaining);
+ }
+
+ int read = inputStream.read(buffer, offset, readLength);
+ if (read == -1) {
+ if (bytesToRead != C.LENGTH_UNSET) {
+ // End of stream reached having not read sufficient data.
+ throw new EOFException();
+ }
+ return C.RESULT_END_OF_INPUT;
+ }
+
+ bytesRead += read;
+ bytesTransferred(read);
+ return read;
+ }
+
+ /**
+ * On platform API levels 19 and 20, okhttp's implementation of {@link InputStream#close} can
+ * block for a long time if the stream has a lot of data remaining. Call this method before
+ * closing the input stream to make a best effort to cause the input stream to encounter an
+ * unexpected end of input, working around this issue. On other platform API levels, the method
+ * does nothing.
+ *
+ * @param connection The connection whose {@link InputStream} should be terminated.
+ * @param bytesRemaining The number of bytes remaining to be read from the input stream if its
+ * length is known. {@link C#LENGTH_UNSET} otherwise.
+ */
+ private static void maybeTerminateInputStream(HttpURLConnection connection, long bytesRemaining) {
+ if (Util.SDK_INT != 19 && Util.SDK_INT != 20) {
+ return;
+ }
+
+ try {
+ InputStream inputStream = connection.getInputStream();
+ if (bytesRemaining == C.LENGTH_UNSET) {
+ // If the input stream has already ended, do nothing. The socket may be re-used.
+ if (inputStream.read() == -1) {
+ return;
+ }
+ } else if (bytesRemaining <= MAX_BYTES_TO_DRAIN) {
+ // There isn't much data left. Prefer to allow it to drain, which may allow the socket to be
+ // re-used.
+ return;
+ }
+ String className = inputStream.getClass().getName();
+ if ("com.android.okhttp.internal.http.HttpTransport$ChunkedInputStream".equals(className)
+ || "com.android.okhttp.internal.http.HttpTransport$FixedLengthInputStream"
+ .equals(className)) {
+ Class<?> superclass = inputStream.getClass().getSuperclass();
+ Method unexpectedEndOfInput = superclass.getDeclaredMethod("unexpectedEndOfInput");
+ unexpectedEndOfInput.setAccessible(true);
+ unexpectedEndOfInput.invoke(inputStream);
+ }
+ } catch (Exception e) {
+ // If an IOException then the connection didn't ever have an input stream, or it was closed
+ // already. If another type of exception then something went wrong, most likely the device
+ // isn't using okhttp.
+ }
+ }
+
+
+ /**
+ * Closes the current connection quietly, if there is one.
+ */
+ private void closeConnectionQuietly() {
+ if (connection != null) {
+ try {
+ connection.disconnect();
+ } catch (Exception e) {
+ Log.e(TAG, "Unexpected error while disconnecting", e);
+ }
+ connection = null;
+ }
+ }
+
+ private static boolean isCompressed(HttpURLConnection connection) {
+ String contentEncoding = connection.getHeaderField("Content-Encoding");
+ return "gzip".equalsIgnoreCase(contentEncoding);
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java
new file mode 100644
index 0000000000..cf7448fbd0
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream;
+
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+
+/** A {@link Factory} that produces {@link DefaultHttpDataSource} instances. */
+public final class DefaultHttpDataSourceFactory extends BaseFactory {
+
+ private final String userAgent;
+ @Nullable private final TransferListener listener;
+ private final int connectTimeoutMillis;
+ private final int readTimeoutMillis;
+ private final boolean allowCrossProtocolRedirects;
+
+ /**
+ * Constructs a DefaultHttpDataSourceFactory. Sets {@link
+ * DefaultHttpDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout, {@link
+ * DefaultHttpDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables
+ * cross-protocol redirects.
+ *
+ * @param userAgent The User-Agent string that should be used.
+ */
+ public DefaultHttpDataSourceFactory(String userAgent) {
+ this(userAgent, null);
+ }
+
+ /**
+ * Constructs a DefaultHttpDataSourceFactory. Sets {@link
+ * DefaultHttpDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout, {@link
+ * DefaultHttpDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables
+ * cross-protocol redirects.
+ *
+ * @param userAgent The User-Agent string that should be used.
+ * @param listener An optional listener.
+ * @see #DefaultHttpDataSourceFactory(String, TransferListener, int, int, boolean)
+ */
+ public DefaultHttpDataSourceFactory(String userAgent, @Nullable TransferListener listener) {
+ this(userAgent, listener, DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS,
+ DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS, false);
+ }
+
+ /**
+ * @param userAgent The User-Agent string that should be used.
+ * @param connectTimeoutMillis The connection timeout that should be used when requesting remote
+ * data, in milliseconds. A timeout of zero is interpreted as an infinite timeout.
+ * @param readTimeoutMillis The read timeout that should be used when requesting remote data, in
+ * milliseconds. A timeout of zero is interpreted as an infinite timeout.
+ * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP
+ * to HTTPS and vice versa) are enabled.
+ */
+ public DefaultHttpDataSourceFactory(
+ String userAgent,
+ int connectTimeoutMillis,
+ int readTimeoutMillis,
+ boolean allowCrossProtocolRedirects) {
+ this(
+ userAgent,
+ /* listener= */ null,
+ connectTimeoutMillis,
+ readTimeoutMillis,
+ allowCrossProtocolRedirects);
+ }
+
+ /**
+ * @param userAgent The User-Agent string that should be used.
+ * @param listener An optional listener.
+ * @param connectTimeoutMillis The connection timeout that should be used when requesting remote
+ * data, in milliseconds. A timeout of zero is interpreted as an infinite timeout.
+ * @param readTimeoutMillis The read timeout that should be used when requesting remote data, in
+ * milliseconds. A timeout of zero is interpreted as an infinite timeout.
+ * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP
+ * to HTTPS and vice versa) are enabled.
+ */
+ public DefaultHttpDataSourceFactory(
+ String userAgent,
+ @Nullable TransferListener listener,
+ int connectTimeoutMillis,
+ int readTimeoutMillis,
+ boolean allowCrossProtocolRedirects) {
+ this.userAgent = Assertions.checkNotEmpty(userAgent);
+ this.listener = listener;
+ this.connectTimeoutMillis = connectTimeoutMillis;
+ this.readTimeoutMillis = readTimeoutMillis;
+ this.allowCrossProtocolRedirects = allowCrossProtocolRedirects;
+ }
+
+ @Override
+ protected DefaultHttpDataSource createDataSourceInternal(
+ HttpDataSource.RequestProperties defaultRequestProperties) {
+ DefaultHttpDataSource dataSource =
+ new DefaultHttpDataSource(
+ userAgent,
+ connectTimeoutMillis,
+ readTimeoutMillis,
+ allowCrossProtocolRedirects,
+ defaultRequestProperties);
+ if (listener != null) {
+ dataSource.addTransferListener(listener);
+ }
+ return dataSource;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultLoadErrorHandlingPolicy.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultLoadErrorHandlingPolicy.java
new file mode 100644
index 0000000000..082014b7ef
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultLoadErrorHandlingPolicy.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.HttpDataSource.InvalidResponseCodeException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Loader.UnexpectedLoaderException;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+/** Default implementation of {@link LoadErrorHandlingPolicy}. */
+public class DefaultLoadErrorHandlingPolicy implements LoadErrorHandlingPolicy {
+
+ /** The default minimum number of times to retry loading data prior to propagating the error. */
+ public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3;
+ /**
+ * The default minimum number of times to retry loading prior to failing for progressive live
+ * streams.
+ */
+ public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT_PROGRESSIVE_LIVE = 6;
+ /** The default duration for which a track is blacklisted in milliseconds. */
+ public static final long DEFAULT_TRACK_BLACKLIST_MS = 60000;
+
+ private static final int DEFAULT_BEHAVIOR_MIN_LOADABLE_RETRY_COUNT = -1;
+
+ private final int minimumLoadableRetryCount;
+
+ /**
+ * Creates an instance with default behavior.
+ *
+ * <p>{@link #getMinimumLoadableRetryCount} will return {@link
+ * #DEFAULT_MIN_LOADABLE_RETRY_COUNT_PROGRESSIVE_LIVE} for {@code dataType} {@link
+ * C#DATA_TYPE_MEDIA_PROGRESSIVE_LIVE}. For other {@code dataType} values, it will return {@link
+ * #DEFAULT_MIN_LOADABLE_RETRY_COUNT}.
+ */
+ public DefaultLoadErrorHandlingPolicy() {
+ this(DEFAULT_BEHAVIOR_MIN_LOADABLE_RETRY_COUNT);
+ }
+
+ /**
+ * Creates an instance with the given value for {@link #getMinimumLoadableRetryCount(int)}.
+ *
+ * @param minimumLoadableRetryCount See {@link #getMinimumLoadableRetryCount}.
+ */
+ public DefaultLoadErrorHandlingPolicy(int minimumLoadableRetryCount) {
+ this.minimumLoadableRetryCount = minimumLoadableRetryCount;
+ }
+
+ /**
+ * Blacklists resources whose load error was an {@link InvalidResponseCodeException} with response
+ * code HTTP 404 or 410. The duration of the blacklisting is {@link #DEFAULT_TRACK_BLACKLIST_MS}.
+ */
+ @Override
+ public long getBlacklistDurationMsFor(
+ int dataType, long loadDurationMs, IOException exception, int errorCount) {
+ if (exception instanceof InvalidResponseCodeException) {
+ int responseCode = ((InvalidResponseCodeException) exception).responseCode;
+ return responseCode == 404 // HTTP 404 Not Found.
+ || responseCode == 410 // HTTP 410 Gone.
+ || responseCode == 416 // HTTP 416 Range Not Satisfiable.
+ ? DEFAULT_TRACK_BLACKLIST_MS
+ : C.TIME_UNSET;
+ }
+ return C.TIME_UNSET;
+ }
+
+ /**
+ * Retries for any exception that is not a subclass of {@link ParserException}, {@link
+ * FileNotFoundException} or {@link UnexpectedLoaderException}. The retry delay is calculated as
+ * {@code Math.min((errorCount - 1) * 1000, 5000)}.
+ */
+ @Override
+ public long getRetryDelayMsFor(
+ int dataType, long loadDurationMs, IOException exception, int errorCount) {
+ return exception instanceof ParserException
+ || exception instanceof FileNotFoundException
+ || exception instanceof UnexpectedLoaderException
+ ? C.TIME_UNSET
+ : Math.min((errorCount - 1) * 1000, 5000);
+ }
+
+ /**
+ * See {@link #DefaultLoadErrorHandlingPolicy()} and {@link #DefaultLoadErrorHandlingPolicy(int)}
+ * for documentation about the behavior of this method.
+ */
+ @Override
+ public int getMinimumLoadableRetryCount(int dataType) {
+ if (minimumLoadableRetryCount == DEFAULT_BEHAVIOR_MIN_LOADABLE_RETRY_COUNT) {
+ return dataType == C.DATA_TYPE_MEDIA_PROGRESSIVE_LIVE
+ ? DEFAULT_MIN_LOADABLE_RETRY_COUNT_PROGRESSIVE_LIVE
+ : DEFAULT_MIN_LOADABLE_RETRY_COUNT;
+ } else {
+ return minimumLoadableRetryCount;
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DummyDataSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DummyDataSource.java
new file mode 100644
index 0000000000..585c37cc78
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DummyDataSource.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream;
+
+import android.net.Uri;
+import androidx.annotation.Nullable;
+import java.io.IOException;
+
+/**
+ * A dummy DataSource which provides no data. {@link #open(DataSpec)} throws {@link IOException}.
+ */
+public final class DummyDataSource implements DataSource {
+
+ public static final DummyDataSource INSTANCE = new DummyDataSource();
+
+ /** A factory that produces {@link DummyDataSource}. */
+ public static final Factory FACTORY = DummyDataSource::new;
+
+ private DummyDataSource() {}
+
+ @Override
+ public void addTransferListener(TransferListener transferListener) {
+ // Do nothing.
+ }
+
+ @Override
+ public long open(DataSpec dataSpec) throws IOException {
+ throw new IOException("Dummy source");
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int readLength) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ @Nullable
+ public Uri getUri() {
+ return null;
+ }
+
+ @Override
+ public void close() {
+ // do nothing.
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/FileDataSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/FileDataSource.java
new file mode 100644
index 0000000000..eee30e668f
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/FileDataSource.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream;
+
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull;
+
+import android.net.Uri;
+import android.text.TextUtils;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import java.io.EOFException;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+
+/** A {@link DataSource} for reading local files. */
+public final class FileDataSource extends BaseDataSource {
+
+ /** Thrown when a {@link FileDataSource} encounters an error reading a file. */
+ public static class FileDataSourceException extends IOException {
+
+ public FileDataSourceException(IOException cause) {
+ super(cause);
+ }
+
+ public FileDataSourceException(String message, IOException cause) {
+ super(message, cause);
+ }
+ }
+
+ /** {@link DataSource.Factory} for {@link FileDataSource} instances. */
+ public static final class Factory implements DataSource.Factory {
+
+ @Nullable private TransferListener listener;
+
+ /**
+ * Sets a {@link TransferListener} for {@link FileDataSource} instances created by this factory.
+ *
+ * @param listener The {@link TransferListener}.
+ * @return This factory.
+ */
+ public Factory setListener(@Nullable TransferListener listener) {
+ this.listener = listener;
+ return this;
+ }
+
+ @Override
+ public FileDataSource createDataSource() {
+ FileDataSource dataSource = new FileDataSource();
+ if (listener != null) {
+ dataSource.addTransferListener(listener);
+ }
+ return dataSource;
+ }
+ }
+
+ @Nullable private RandomAccessFile file;
+ @Nullable private Uri uri;
+ private long bytesRemaining;
+ private boolean opened;
+
+ public FileDataSource() {
+ super(/* isNetwork= */ false);
+ }
+
+ @Override
+ public long open(DataSpec dataSpec) throws FileDataSourceException {
+ try {
+ Uri uri = dataSpec.uri;
+ this.uri = uri;
+
+ transferInitializing(dataSpec);
+
+ this.file = openLocalFile(uri);
+
+ file.seek(dataSpec.position);
+ bytesRemaining = dataSpec.length == C.LENGTH_UNSET ? file.length() - dataSpec.position
+ : dataSpec.length;
+ if (bytesRemaining < 0) {
+ throw new EOFException();
+ }
+ } catch (IOException e) {
+ throw new FileDataSourceException(e);
+ }
+
+ opened = true;
+ transferStarted(dataSpec);
+
+ return bytesRemaining;
+ }
+
+ private static RandomAccessFile openLocalFile(Uri uri) throws FileDataSourceException {
+ try {
+ return new RandomAccessFile(Assertions.checkNotNull(uri.getPath()), "r");
+ } catch (FileNotFoundException e) {
+ if (!TextUtils.isEmpty(uri.getQuery()) || !TextUtils.isEmpty(uri.getFragment())) {
+ throw new FileDataSourceException(
+ String.format(
+ "uri has query and/or fragment, which are not supported. Did you call Uri.parse()"
+ + " on a string containing '?' or '#'? Use Uri.fromFile(new File(path)) to"
+ + " avoid this. path=%s,query=%s,fragment=%s",
+ uri.getPath(), uri.getQuery(), uri.getFragment()),
+ e);
+ }
+ throw new FileDataSourceException(e);
+ }
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int readLength) throws FileDataSourceException {
+ if (readLength == 0) {
+ return 0;
+ } else if (bytesRemaining == 0) {
+ return C.RESULT_END_OF_INPUT;
+ } else {
+ int bytesRead;
+ try {
+ bytesRead =
+ castNonNull(file).read(buffer, offset, (int) Math.min(bytesRemaining, readLength));
+ } catch (IOException e) {
+ throw new FileDataSourceException(e);
+ }
+
+ if (bytesRead > 0) {
+ bytesRemaining -= bytesRead;
+ bytesTransferred(bytesRead);
+ }
+
+ return bytesRead;
+ }
+ }
+
+ @Override
+ @Nullable
+ public Uri getUri() {
+ return uri;
+ }
+
+ @Override
+ public void close() throws FileDataSourceException {
+ uri = null;
+ try {
+ if (file != null) {
+ file.close();
+ }
+ } catch (IOException e) {
+ throw new FileDataSourceException(e);
+ } finally {
+ file = null;
+ if (opened) {
+ opened = false;
+ transferEnded();
+ }
+ }
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/FileDataSourceFactory.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/FileDataSourceFactory.java
new file mode 100644
index 0000000000..660a38161c
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/FileDataSourceFactory.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream;
+
+import androidx.annotation.Nullable;
+
+/** @deprecated Use {@link FileDataSource.Factory}. */
+@Deprecated
+public final class FileDataSourceFactory implements DataSource.Factory {
+
+ private final FileDataSource.Factory wrappedFactory;
+
+ public FileDataSourceFactory() {
+ this(/* listener= */ null);
+ }
+
+ public FileDataSourceFactory(@Nullable TransferListener listener) {
+ wrappedFactory = new FileDataSource.Factory().setListener(listener);
+ }
+
+ @Override
+ public FileDataSource createDataSource() {
+ return wrappedFactory.createDataSource();
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/HttpDataSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/HttpDataSource.java
new file mode 100644
index 0000000000..ffac1ca893
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/HttpDataSource.java
@@ -0,0 +1,379 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream;
+
+import android.text.TextUtils;
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Predicate;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * An HTTP {@link DataSource}.
+ */
+public interface HttpDataSource extends DataSource {
+
+ /**
+ * A factory for {@link HttpDataSource} instances.
+ */
+ interface Factory extends DataSource.Factory {
+
+ @Override
+ HttpDataSource createDataSource();
+
+ /**
+ * Gets the default request properties used by all {@link HttpDataSource}s created by the
+ * factory. Changes to the properties will be reflected in any future requests made by
+ * {@link HttpDataSource}s created by the factory.
+ *
+ * @return The default request properties of the factory.
+ */
+ RequestProperties getDefaultRequestProperties();
+
+ /**
+ * Sets a default request header for {@link HttpDataSource} instances created by the factory.
+ *
+ * @deprecated Use {@link #getDefaultRequestProperties} instead.
+ * @param name The name of the header field.
+ * @param value The value of the field.
+ */
+ @Deprecated
+ void setDefaultRequestProperty(String name, String value);
+
+ /**
+ * Clears a default request header for {@link HttpDataSource} instances created by the factory.
+ *
+ * @deprecated Use {@link #getDefaultRequestProperties} instead.
+ * @param name The name of the header field.
+ */
+ @Deprecated
+ void clearDefaultRequestProperty(String name);
+
+ /**
+ * Clears all default request headers for all {@link HttpDataSource} instances created by the
+ * factory.
+ *
+ * @deprecated Use {@link #getDefaultRequestProperties} instead.
+ */
+ @Deprecated
+ void clearAllDefaultRequestProperties();
+
+ }
+
+ /**
+ * Stores HTTP request properties (aka HTTP headers) and provides methods to modify the headers
+ * in a thread safe way to avoid the potential of creating snapshots of an inconsistent or
+ * unintended state.
+ */
+ final class RequestProperties {
+
+ private final Map<String, String> requestProperties;
+ private Map<String, String> requestPropertiesSnapshot;
+
+ public RequestProperties() {
+ requestProperties = new HashMap<>();
+ }
+
+ /**
+ * Sets the specified property {@code value} for the specified {@code name}. If a property for
+ * this name previously existed, the old value is replaced by the specified value.
+ *
+ * @param name The name of the request property.
+ * @param value The value of the request property.
+ */
+ public synchronized void set(String name, String value) {
+ requestPropertiesSnapshot = null;
+ requestProperties.put(name, value);
+ }
+
+ /**
+ * Sets the keys and values contained in the map. If a property previously existed, the old
+ * value is replaced by the specified value. If a property previously existed and is not in the
+ * map, the property is left unchanged.
+ *
+ * @param properties The request properties.
+ */
+ public synchronized void set(Map<String, String> properties) {
+ requestPropertiesSnapshot = null;
+ requestProperties.putAll(properties);
+ }
+
+ /**
+ * Removes all properties previously existing and sets the keys and values of the map.
+ *
+ * @param properties The request properties.
+ */
+ public synchronized void clearAndSet(Map<String, String> properties) {
+ requestPropertiesSnapshot = null;
+ requestProperties.clear();
+ requestProperties.putAll(properties);
+ }
+
+ /**
+ * Removes a request property by name.
+ *
+ * @param name The name of the request property to remove.
+ */
+ public synchronized void remove(String name) {
+ requestPropertiesSnapshot = null;
+ requestProperties.remove(name);
+ }
+
+ /**
+ * Clears all request properties.
+ */
+ public synchronized void clear() {
+ requestPropertiesSnapshot = null;
+ requestProperties.clear();
+ }
+
+ /**
+ * Gets a snapshot of the request properties.
+ *
+ * @return A snapshot of the request properties.
+ */
+ public synchronized Map<String, String> getSnapshot() {
+ if (requestPropertiesSnapshot == null) {
+ requestPropertiesSnapshot = Collections.unmodifiableMap(new HashMap<>(requestProperties));
+ }
+ return requestPropertiesSnapshot;
+ }
+
+ }
+
+ /**
+ * Base implementation of {@link Factory} that sets default request properties.
+ */
+ abstract class BaseFactory implements Factory {
+
+ private final RequestProperties defaultRequestProperties;
+
+ public BaseFactory() {
+ defaultRequestProperties = new RequestProperties();
+ }
+
+ @Override
+ public final HttpDataSource createDataSource() {
+ return createDataSourceInternal(defaultRequestProperties);
+ }
+
+ @Override
+ public final RequestProperties getDefaultRequestProperties() {
+ return defaultRequestProperties;
+ }
+
+ /** @deprecated Use {@link #getDefaultRequestProperties} instead. */
+ @Deprecated
+ @Override
+ public final void setDefaultRequestProperty(String name, String value) {
+ defaultRequestProperties.set(name, value);
+ }
+
+ /** @deprecated Use {@link #getDefaultRequestProperties} instead. */
+ @Deprecated
+ @Override
+ public final void clearDefaultRequestProperty(String name) {
+ defaultRequestProperties.remove(name);
+ }
+
+ /** @deprecated Use {@link #getDefaultRequestProperties} instead. */
+ @Deprecated
+ @Override
+ public final void clearAllDefaultRequestProperties() {
+ defaultRequestProperties.clear();
+ }
+
+ /**
+ * Called by {@link #createDataSource()} to create a {@link HttpDataSource} instance.
+ *
+ * @param defaultRequestProperties The default {@code RequestProperties} to be used by the
+ * {@link HttpDataSource} instance.
+ * @return A {@link HttpDataSource} instance.
+ */
+ protected abstract HttpDataSource createDataSourceInternal(RequestProperties
+ defaultRequestProperties);
+
+ }
+
+ /** A {@link Predicate} that rejects content types often used for pay-walls. */
+ Predicate<String> REJECT_PAYWALL_TYPES =
+ contentType -> {
+ contentType = Util.toLowerInvariant(contentType);
+ return !TextUtils.isEmpty(contentType)
+ && (!contentType.contains("text") || contentType.contains("text/vtt"))
+ && !contentType.contains("html")
+ && !contentType.contains("xml");
+ };
+
+ /**
+ * Thrown when an error is encountered when trying to read from a {@link HttpDataSource}.
+ */
+ class HttpDataSourceException extends IOException {
+
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({TYPE_OPEN, TYPE_READ, TYPE_CLOSE})
+ public @interface Type {}
+
+ public static final int TYPE_OPEN = 1;
+ public static final int TYPE_READ = 2;
+ public static final int TYPE_CLOSE = 3;
+
+ @Type public final int type;
+
+ /**
+ * The {@link DataSpec} associated with the current connection.
+ */
+ public final DataSpec dataSpec;
+
+ public HttpDataSourceException(DataSpec dataSpec, @Type int type) {
+ super();
+ this.dataSpec = dataSpec;
+ this.type = type;
+ }
+
+ public HttpDataSourceException(String message, DataSpec dataSpec, @Type int type) {
+ super(message);
+ this.dataSpec = dataSpec;
+ this.type = type;
+ }
+
+ public HttpDataSourceException(IOException cause, DataSpec dataSpec, @Type int type) {
+ super(cause);
+ this.dataSpec = dataSpec;
+ this.type = type;
+ }
+
+ public HttpDataSourceException(String message, IOException cause, DataSpec dataSpec,
+ @Type int type) {
+ super(message, cause);
+ this.dataSpec = dataSpec;
+ this.type = type;
+ }
+
+ }
+
+ /**
+ * Thrown when the content type is invalid.
+ */
+ final class InvalidContentTypeException extends HttpDataSourceException {
+
+ public final String contentType;
+
+ public InvalidContentTypeException(String contentType, DataSpec dataSpec) {
+ super("Invalid content type: " + contentType, dataSpec, TYPE_OPEN);
+ this.contentType = contentType;
+ }
+
+ }
+
+ /**
+ * Thrown when an attempt to open a connection results in a response code not in the 2xx range.
+ */
+ final class InvalidResponseCodeException extends HttpDataSourceException {
+
+ /**
+ * The response code that was outside of the 2xx range.
+ */
+ public final int responseCode;
+
+ /** The http status message. */
+ @Nullable public final String responseMessage;
+
+ /**
+ * An unmodifiable map of the response header fields and values.
+ */
+ public final Map<String, List<String>> headerFields;
+
+ /** @deprecated Use {@link #InvalidResponseCodeException(int, String, Map, DataSpec)}. */
+ @Deprecated
+ public InvalidResponseCodeException(
+ int responseCode, Map<String, List<String>> headerFields, DataSpec dataSpec) {
+ this(responseCode, /* responseMessage= */ null, headerFields, dataSpec);
+ }
+
+ public InvalidResponseCodeException(
+ int responseCode,
+ @Nullable String responseMessage,
+ Map<String, List<String>> headerFields,
+ DataSpec dataSpec) {
+ super("Response code: " + responseCode, dataSpec, TYPE_OPEN);
+ this.responseCode = responseCode;
+ this.responseMessage = responseMessage;
+ this.headerFields = headerFields;
+ }
+
+ }
+
+ /**
+ * Opens the source to read the specified data.
+ *
+ * <p>Note: {@link HttpDataSource} implementations are advised to set request headers passed via
+ * (in order of decreasing priority) the {@code dataSpec}, {@link #setRequestProperty} and the
+ * default parameters set in the {@link Factory}.
+ */
+ @Override
+ long open(DataSpec dataSpec) throws HttpDataSourceException;
+
+ @Override
+ void close() throws HttpDataSourceException;
+
+ @Override
+ int read(byte[] buffer, int offset, int readLength) throws HttpDataSourceException;
+
+ /**
+ * Sets the value of a request header. The value will be used for subsequent connections
+ * established by the source.
+ *
+ * <p>Note: If the same header is set as a default parameter in the {@link Factory}, then the
+ * header value set with this method should be preferred when connecting with the data source. See
+ * {@link #open}.
+ *
+ * @param name The name of the header field.
+ * @param value The value of the field.
+ */
+ void setRequestProperty(String name, String value);
+
+ /**
+ * Clears the value of a request header. The change will apply to subsequent connections
+ * established by the source.
+ *
+ * @param name The name of the header field.
+ */
+ void clearRequestProperty(String name);
+
+ /**
+ * Clears all request headers that were set by {@link #setRequestProperty(String, String)}.
+ */
+ void clearAllRequestProperties();
+
+ /**
+ * When the source is open, returns the HTTP response status code associated with the last {@link
+ * #open} call. Otherwise, returns a negative value.
+ */
+ int getResponseCode();
+
+ @Override
+ Map<String, List<String>> getResponseHeaders();
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/LoadErrorHandlingPolicy.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/LoadErrorHandlingPolicy.java
new file mode 100644
index 0000000000..03c861c5f1
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/LoadErrorHandlingPolicy.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Loader.Callback;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Loader.Loadable;
+import java.io.IOException;
+
+/**
+ * Defines how errors encountered by {@link Loader Loaders} are handled.
+ *
+ * <p>Loader clients may blacklist a resource when a load error occurs. Blacklisting works around
+ * load errors by loading an alternative resource. Clients do not try blacklisting when a resource
+ * does not have an alternative. When a resource does have valid alternatives, {@link
+ * #getBlacklistDurationMsFor(int, long, IOException, int)} defines whether the resource should be
+ * blacklisted. Blacklisting will succeed if any of the alternatives is not in the black list.
+ *
+ * <p>When blacklisting does not take place, {@link #getRetryDelayMsFor(int, long, IOException,
+ * int)} defines whether the load is retried. Errors whose load is not retried are propagated. Load
+ * errors whose load is retried are propagated according to {@link
+ * #getMinimumLoadableRetryCount(int)}.
+ *
+ * <p>Methods are invoked on the playback thread.
+ */
+public interface LoadErrorHandlingPolicy {
+
+ /**
+ * Returns the number of milliseconds for which a resource associated to a provided load error
+ * should be blacklisted, or {@link C#TIME_UNSET} if the resource should not be blacklisted.
+ *
+ * @param dataType One of the {@link C C.DATA_TYPE_*} constants indicating the type of data to
+ * load.
+ * @param loadDurationMs The duration in milliseconds of the load from the start of the first load
+ * attempt up to the point at which the error occurred.
+ * @param exception The load error.
+ * @param errorCount The number of errors this load has encountered, including this one.
+ * @return The blacklist duration in milliseconds, or {@link C#TIME_UNSET} if the resource should
+ * not be blacklisted.
+ */
+ long getBlacklistDurationMsFor(
+ int dataType, long loadDurationMs, IOException exception, int errorCount);
+
+ /**
+ * Returns the number of milliseconds to wait before attempting the load again, or {@link
+ * C#TIME_UNSET} if the error is fatal and should not be retried.
+ *
+ * <p>{@link Loader} clients may ignore the retry delay returned by this method in order to wait
+ * for a specific event before retrying. However, the load is retried if and only if this method
+ * does not return {@link C#TIME_UNSET}.
+ *
+ * @param dataType One of the {@link C C.DATA_TYPE_*} constants indicating the type of data to
+ * load.
+ * @param loadDurationMs The duration in milliseconds of the load from the start of the first load
+ * attempt up to the point at which the error occurred.
+ * @param exception The load error.
+ * @param errorCount The number of errors this load has encountered, including this one.
+ * @return The number of milliseconds to wait before attempting the load again, or {@link
+ * C#TIME_UNSET} if the error is fatal and should not be retried.
+ */
+ long getRetryDelayMsFor(int dataType, long loadDurationMs, IOException exception, int errorCount);
+
+ /**
+ * Returns the minimum number of times to retry a load in the case of a load error, before
+ * propagating the error.
+ *
+ * @param dataType One of the {@link C C.DATA_TYPE_*} constants indicating the type of data to
+ * load.
+ * @return The minimum number of times to retry a load in the case of a load error, before
+ * propagating the error.
+ * @see Loader#startLoading(Loadable, Callback, int)
+ */
+ int getMinimumLoadableRetryCount(int dataType);
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/Loader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/Loader.java
new file mode 100644
index 0000000000..0e79759b36
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/Loader.java
@@ -0,0 +1,521 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream;
+
+import android.annotation.SuppressLint;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.SystemClock;
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TraceUtil;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.concurrent.ExecutorService;
+
+/**
+ * Manages the background loading of {@link Loadable}s.
+ */
+public final class Loader implements LoaderErrorThrower {
+
+ /**
+ * Thrown when an unexpected exception or error is encountered during loading.
+ */
+ public static final class UnexpectedLoaderException extends IOException {
+
+ public UnexpectedLoaderException(Throwable cause) {
+ super("Unexpected " + cause.getClass().getSimpleName() + ": " + cause.getMessage(), cause);
+ }
+
+ }
+
+ /**
+ * An object that can be loaded using a {@link Loader}.
+ */
+ public interface Loadable {
+
+ /**
+ * Cancels the load.
+ */
+ void cancelLoad();
+
+ /**
+ * Performs the load, returning on completion or cancellation.
+ *
+ * @throws IOException If the input could not be loaded.
+ * @throws InterruptedException If the thread was interrupted.
+ */
+ void load() throws IOException, InterruptedException;
+
+ }
+
+ /**
+ * A callback to be notified of {@link Loader} events.
+ */
+ public interface Callback<T extends Loadable> {
+
+ /**
+ * Called when a load has completed.
+ *
+ * <p>Note: There is guaranteed to be a memory barrier between {@link Loadable#load()} exiting
+ * and this callback being called.
+ *
+ * @param loadable The loadable whose load has completed.
+ * @param elapsedRealtimeMs {@link SystemClock#elapsedRealtime} when the load ended.
+ * @param loadDurationMs The duration in milliseconds of the load since {@link #startLoading}
+ * was called.
+ */
+ void onLoadCompleted(T loadable, long elapsedRealtimeMs, long loadDurationMs);
+
+ /**
+ * Called when a load has been canceled.
+ *
+ * <p>Note: If the {@link Loader} has not been released then there is guaranteed to be a memory
+ * barrier between {@link Loadable#load()} exiting and this callback being called. If the {@link
+ * Loader} has been released then this callback may be called before {@link Loadable#load()}
+ * exits.
+ *
+ * @param loadable The loadable whose load has been canceled.
+ * @param elapsedRealtimeMs {@link SystemClock#elapsedRealtime} when the load was canceled.
+ * @param loadDurationMs The duration in milliseconds of the load since {@link #startLoading}
+ * was called up to the point at which it was canceled.
+ * @param released True if the load was canceled because the {@link Loader} was released. False
+ * otherwise.
+ */
+ void onLoadCanceled(T loadable, long elapsedRealtimeMs, long loadDurationMs, boolean released);
+
+ /**
+ * Called when a load encounters an error.
+ *
+ * <p>Note: There is guaranteed to be a memory barrier between {@link Loadable#load()} exiting
+ * and this callback being called.
+ *
+ * @param loadable The loadable whose load has encountered an error.
+ * @param elapsedRealtimeMs {@link SystemClock#elapsedRealtime} when the error occurred.
+ * @param loadDurationMs The duration in milliseconds of the load since {@link #startLoading}
+ * was called up to the point at which the error occurred.
+ * @param error The load error.
+ * @param errorCount The number of errors this load has encountered, including this one.
+ * @return The desired error handling action. One of {@link Loader#RETRY}, {@link
+ * Loader#RETRY_RESET_ERROR_COUNT}, {@link Loader#DONT_RETRY}, {@link
+ * Loader#DONT_RETRY_FATAL} or a retry action created by {@link #createRetryAction}.
+ */
+ LoadErrorAction onLoadError(
+ T loadable, long elapsedRealtimeMs, long loadDurationMs, IOException error, int errorCount);
+ }
+
+ /**
+ * A callback to be notified when a {@link Loader} has finished being released.
+ */
+ public interface ReleaseCallback {
+
+ /**
+ * Called when the {@link Loader} has finished being released.
+ */
+ void onLoaderReleased();
+
+ }
+
+ /** Types of action that can be taken in response to a load error. */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ ACTION_TYPE_RETRY,
+ ACTION_TYPE_RETRY_AND_RESET_ERROR_COUNT,
+ ACTION_TYPE_DONT_RETRY,
+ ACTION_TYPE_DONT_RETRY_FATAL
+ })
+ private @interface RetryActionType {}
+
+ private static final int ACTION_TYPE_RETRY = 0;
+ private static final int ACTION_TYPE_RETRY_AND_RESET_ERROR_COUNT = 1;
+ private static final int ACTION_TYPE_DONT_RETRY = 2;
+ private static final int ACTION_TYPE_DONT_RETRY_FATAL = 3;
+
+ /** Retries the load using the default delay. */
+ public static final LoadErrorAction RETRY =
+ createRetryAction(/* resetErrorCount= */ false, C.TIME_UNSET);
+ /** Retries the load using the default delay and resets the error count. */
+ public static final LoadErrorAction RETRY_RESET_ERROR_COUNT =
+ createRetryAction(/* resetErrorCount= */ true, C.TIME_UNSET);
+ /** Discards the failed {@link Loadable} and ignores any errors that have occurred. */
+ public static final LoadErrorAction DONT_RETRY =
+ new LoadErrorAction(ACTION_TYPE_DONT_RETRY, C.TIME_UNSET);
+ /**
+ * Discards the failed {@link Loadable}. The next call to {@link #maybeThrowError()} will throw
+ * the last load error.
+ */
+ public static final LoadErrorAction DONT_RETRY_FATAL =
+ new LoadErrorAction(ACTION_TYPE_DONT_RETRY_FATAL, C.TIME_UNSET);
+
+ /**
+ * Action that can be taken in response to {@link Callback#onLoadError(Loadable, long, long,
+ * IOException, int)}.
+ */
+ public static final class LoadErrorAction {
+
+ private final @RetryActionType int type;
+ private final long retryDelayMillis;
+
+ private LoadErrorAction(@RetryActionType int type, long retryDelayMillis) {
+ this.type = type;
+ this.retryDelayMillis = retryDelayMillis;
+ }
+
+ /** Returns whether this is a retry action. */
+ public boolean isRetry() {
+ return type == ACTION_TYPE_RETRY || type == ACTION_TYPE_RETRY_AND_RESET_ERROR_COUNT;
+ }
+ }
+
+ private final ExecutorService downloadExecutorService;
+
+ @Nullable private LoadTask<? extends Loadable> currentTask;
+ @Nullable private IOException fatalError;
+
+ /**
+ * @param threadName A name for the loader's thread.
+ */
+ public Loader(String threadName) {
+ this.downloadExecutorService = Util.newSingleThreadExecutor(threadName);
+ }
+
+ /**
+ * Creates a {@link LoadErrorAction} for retrying with the given parameters.
+ *
+ * @param resetErrorCount Whether the previous error count should be set to zero.
+ * @param retryDelayMillis The number of milliseconds to wait before retrying.
+ * @return A {@link LoadErrorAction} for retrying with the given parameters.
+ */
+ public static LoadErrorAction createRetryAction(boolean resetErrorCount, long retryDelayMillis) {
+ return new LoadErrorAction(
+ resetErrorCount ? ACTION_TYPE_RETRY_AND_RESET_ERROR_COUNT : ACTION_TYPE_RETRY,
+ retryDelayMillis);
+ }
+
+ /**
+ * Whether the last call to {@link #startLoading} resulted in a fatal error. Calling {@link
+ * #maybeThrowError()} will throw the fatal error.
+ */
+ public boolean hasFatalError() {
+ return fatalError != null;
+ }
+
+ /** Clears any stored fatal error. */
+ public void clearFatalError() {
+ fatalError = null;
+ }
+
+ /**
+ * Starts loading a {@link Loadable}.
+ *
+ * <p>The calling thread must be a {@link Looper} thread, which is the thread on which the {@link
+ * Callback} will be called.
+ *
+ * @param <T> The type of the loadable.
+ * @param loadable The {@link Loadable} to load.
+ * @param callback A callback to be called when the load ends.
+ * @param defaultMinRetryCount The minimum number of times the load must be retried before {@link
+ * #maybeThrowError()} will propagate an error.
+ * @throws IllegalStateException If the calling thread does not have an associated {@link Looper}.
+ * @return {@link SystemClock#elapsedRealtime} when the load started.
+ */
+ public <T extends Loadable> long startLoading(
+ T loadable, Callback<T> callback, int defaultMinRetryCount) {
+ Looper looper = Assertions.checkStateNotNull(Looper.myLooper());
+ fatalError = null;
+ long startTimeMs = SystemClock.elapsedRealtime();
+ new LoadTask<>(looper, loadable, callback, defaultMinRetryCount, startTimeMs).start(0);
+ return startTimeMs;
+ }
+
+ /** Returns whether the loader is currently loading. */
+ public boolean isLoading() {
+ return currentTask != null;
+ }
+
+ /**
+ * Cancels the current load.
+ *
+ * @throws IllegalStateException If the loader is not currently loading.
+ */
+ public void cancelLoading() {
+ Assertions.checkStateNotNull(currentTask).cancel(false);
+ }
+
+ /** Releases the loader. This method should be called when the loader is no longer required. */
+ public void release() {
+ release(null);
+ }
+
+ /**
+ * Releases the loader. This method should be called when the loader is no longer required.
+ *
+ * @param callback An optional callback to be called on the loading thread once the loader has
+ * been released.
+ */
+ public void release(@Nullable ReleaseCallback callback) {
+ if (currentTask != null) {
+ currentTask.cancel(true);
+ }
+ if (callback != null) {
+ downloadExecutorService.execute(new ReleaseTask(callback));
+ }
+ downloadExecutorService.shutdown();
+ }
+
+ // LoaderErrorThrower implementation.
+
+ @Override
+ public void maybeThrowError() throws IOException {
+ maybeThrowError(Integer.MIN_VALUE);
+ }
+
+ @Override
+ public void maybeThrowError(int minRetryCount) throws IOException {
+ if (fatalError != null) {
+ throw fatalError;
+ } else if (currentTask != null) {
+ currentTask.maybeThrowError(minRetryCount == Integer.MIN_VALUE
+ ? currentTask.defaultMinRetryCount : minRetryCount);
+ }
+ }
+
+ // Internal classes.
+
+ @SuppressLint("HandlerLeak")
+ private final class LoadTask<T extends Loadable> extends Handler implements Runnable {
+
+ private static final String TAG = "LoadTask";
+
+ private static final int MSG_START = 0;
+ private static final int MSG_CANCEL = 1;
+ private static final int MSG_END_OF_SOURCE = 2;
+ private static final int MSG_IO_EXCEPTION = 3;
+ private static final int MSG_FATAL_ERROR = 4;
+
+ public final int defaultMinRetryCount;
+
+ private final T loadable;
+ private final long startTimeMs;
+
+ @Nullable private Loader.Callback<T> callback;
+ @Nullable private IOException currentError;
+ private int errorCount;
+
+ @Nullable private volatile Thread executorThread;
+ private volatile boolean canceled;
+ private volatile boolean released;
+
+ public LoadTask(Looper looper, T loadable, Loader.Callback<T> callback,
+ int defaultMinRetryCount, long startTimeMs) {
+ super(looper);
+ this.loadable = loadable;
+ this.callback = callback;
+ this.defaultMinRetryCount = defaultMinRetryCount;
+ this.startTimeMs = startTimeMs;
+ }
+
+ public void maybeThrowError(int minRetryCount) throws IOException {
+ if (currentError != null && errorCount > minRetryCount) {
+ throw currentError;
+ }
+ }
+
+ public void start(long delayMillis) {
+ Assertions.checkState(currentTask == null);
+ currentTask = this;
+ if (delayMillis > 0) {
+ sendEmptyMessageDelayed(MSG_START, delayMillis);
+ } else {
+ execute();
+ }
+ }
+
+ public void cancel(boolean released) {
+ this.released = released;
+ currentError = null;
+ if (hasMessages(MSG_START)) {
+ removeMessages(MSG_START);
+ if (!released) {
+ sendEmptyMessage(MSG_CANCEL);
+ }
+ } else {
+ canceled = true;
+ loadable.cancelLoad();
+ Thread executorThread = this.executorThread;
+ if (executorThread != null) {
+ executorThread.interrupt();
+ }
+ }
+ if (released) {
+ finish();
+ long nowMs = SystemClock.elapsedRealtime();
+ Assertions.checkNotNull(callback)
+ .onLoadCanceled(loadable, nowMs, nowMs - startTimeMs, true);
+ // If loading, this task will be referenced from a GC root (the loading thread) until
+ // cancellation completes. The time taken for cancellation to complete depends on the
+ // implementation of the Loadable that the task is loading. We null the callback reference
+ // here so that it doesn't prevent garbage collection whilst cancellation is ongoing.
+ callback = null;
+ }
+ }
+
+ @Override
+ public void run() {
+ try {
+ executorThread = Thread.currentThread();
+ if (!canceled) {
+ TraceUtil.beginSection("load:" + loadable.getClass().getSimpleName());
+ try {
+ loadable.load();
+ } finally {
+ TraceUtil.endSection();
+ }
+ }
+ if (!released) {
+ sendEmptyMessage(MSG_END_OF_SOURCE);
+ }
+ } catch (IOException e) {
+ if (!released) {
+ obtainMessage(MSG_IO_EXCEPTION, e).sendToTarget();
+ }
+ } catch (InterruptedException e) {
+ // The load was canceled.
+ Assertions.checkState(canceled);
+ if (!released) {
+ sendEmptyMessage(MSG_END_OF_SOURCE);
+ }
+ } catch (Exception e) {
+ // This should never happen, but handle it anyway.
+ Log.e(TAG, "Unexpected exception loading stream", e);
+ if (!released) {
+ obtainMessage(MSG_IO_EXCEPTION, new UnexpectedLoaderException(e)).sendToTarget();
+ }
+ } catch (OutOfMemoryError e) {
+ // This can occur if a stream is malformed in a way that causes an extractor to think it
+ // needs to allocate a large amount of memory. We don't want the process to die in this
+ // case, but we do want the playback to fail.
+ Log.e(TAG, "OutOfMemory error loading stream", e);
+ if (!released) {
+ obtainMessage(MSG_IO_EXCEPTION, new UnexpectedLoaderException(e)).sendToTarget();
+ }
+ } catch (Error e) {
+ // We'd hope that the platform would kill the process if an Error is thrown here, but the
+ // executor may catch the error (b/20616433). Throw it here, but also pass and throw it from
+ // the handler thread so that the process dies even if the executor behaves in this way.
+ Log.e(TAG, "Unexpected error loading stream", e);
+ if (!released) {
+ obtainMessage(MSG_FATAL_ERROR, e).sendToTarget();
+ }
+ throw e;
+ }
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ if (released) {
+ return;
+ }
+ if (msg.what == MSG_START) {
+ execute();
+ return;
+ }
+ if (msg.what == MSG_FATAL_ERROR) {
+ throw (Error) msg.obj;
+ }
+ finish();
+ long nowMs = SystemClock.elapsedRealtime();
+ long durationMs = nowMs - startTimeMs;
+ Loader.Callback<T> callback = Assertions.checkNotNull(this.callback);
+ if (canceled) {
+ callback.onLoadCanceled(loadable, nowMs, durationMs, false);
+ return;
+ }
+ switch (msg.what) {
+ case MSG_CANCEL:
+ callback.onLoadCanceled(loadable, nowMs, durationMs, false);
+ break;
+ case MSG_END_OF_SOURCE:
+ try {
+ callback.onLoadCompleted(loadable, nowMs, durationMs);
+ } catch (RuntimeException e) {
+ // This should never happen, but handle it anyway.
+ Log.e(TAG, "Unexpected exception handling load completed", e);
+ fatalError = new UnexpectedLoaderException(e);
+ }
+ break;
+ case MSG_IO_EXCEPTION:
+ currentError = (IOException) msg.obj;
+ errorCount++;
+ LoadErrorAction action =
+ callback.onLoadError(loadable, nowMs, durationMs, currentError, errorCount);
+ if (action.type == ACTION_TYPE_DONT_RETRY_FATAL) {
+ fatalError = currentError;
+ } else if (action.type != ACTION_TYPE_DONT_RETRY) {
+ if (action.type == ACTION_TYPE_RETRY_AND_RESET_ERROR_COUNT) {
+ errorCount = 1;
+ }
+ start(
+ action.retryDelayMillis != C.TIME_UNSET
+ ? action.retryDelayMillis
+ : getRetryDelayMillis());
+ }
+ break;
+ default:
+ // Never happens.
+ break;
+ }
+ }
+
+ private void execute() {
+ currentError = null;
+ downloadExecutorService.execute(Assertions.checkNotNull(currentTask));
+ }
+
+ private void finish() {
+ currentTask = null;
+ }
+
+ private long getRetryDelayMillis() {
+ return Math.min((errorCount - 1) * 1000, 5000);
+ }
+
+ }
+
+ private static final class ReleaseTask implements Runnable {
+
+ private final ReleaseCallback callback;
+
+ public ReleaseTask(ReleaseCallback callback) {
+ this.callback = callback;
+ }
+
+ @Override
+ public void run() {
+ callback.onLoaderReleased();
+ }
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/LoaderErrorThrower.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/LoaderErrorThrower.java
new file mode 100644
index 0000000000..9a67f20b84
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/LoaderErrorThrower.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Loader.Loadable;
+import java.io.IOException;
+
+/**
+ * Conditionally throws errors affecting a {@link Loader}.
+ */
+public interface LoaderErrorThrower {
+
+ /**
+ * Throws a fatal error, or a non-fatal error if loading is currently backed off and the current
+ * {@link Loadable} has incurred a number of errors greater than the {@link Loader}s default
+ * minimum number of retries. Else does nothing.
+ *
+ * @throws IOException The error.
+ */
+ void maybeThrowError() throws IOException;
+
+ /**
+ * Throws a fatal error, or a non-fatal error if loading is currently backed off and the current
+ * {@link Loadable} has incurred a number of errors greater than the specified minimum number
+ * of retries. Else does nothing.
+ *
+ * @param minRetryCount A minimum retry count that must be exceeded for a non-fatal error to be
+ * thrown. Should be non-negative.
+ * @throws IOException The error.
+ */
+ void maybeThrowError(int minRetryCount) throws IOException;
+
+ /**
+ * A {@link LoaderErrorThrower} that never throws.
+ */
+ final class Dummy implements LoaderErrorThrower {
+
+ @Override
+ public void maybeThrowError() throws IOException {
+ // Do nothing.
+ }
+
+ @Override
+ public void maybeThrowError(int minRetryCount) throws IOException {
+ // Do nothing.
+ }
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ParsingLoadable.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ParsingLoadable.java
new file mode 100644
index 0000000000..3e4192b651
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ParsingLoadable.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream;
+
+import android.net.Uri;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Loader.Loadable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A {@link Loadable} for objects that can be parsed from binary data using a {@link Parser}.
+ *
+ * @param <T> The type of the object being loaded.
+ */
+public final class ParsingLoadable<T> implements Loadable {
+
+ /**
+ * Parses an object from loaded data.
+ */
+ public interface Parser<T> {
+
+ /**
+ * Parses an object from a response.
+ *
+ * @param uri The source {@link Uri} of the response, after any redirection.
+ * @param inputStream An {@link InputStream} from which the response data can be read.
+ * @return The parsed object.
+ * @throws ParserException If an error occurs parsing the data.
+ * @throws IOException If an error occurs reading data from the stream.
+ */
+ T parse(Uri uri, InputStream inputStream) throws IOException;
+
+ }
+
+ /**
+ * Loads a single parsable object.
+ *
+ * @param dataSource The {@link DataSource} through which the object should be read.
+ * @param parser The {@link Parser} to parse the object from the response.
+ * @param uri The {@link Uri} of the object to read.
+ * @param type The type of the data. One of the {@link C}{@code DATA_TYPE_*} constants.
+ * @return The parsed object
+ * @throws IOException Thrown if there is an error while loading or parsing.
+ */
+ public static <T> T load(DataSource dataSource, Parser<? extends T> parser, Uri uri, int type)
+ throws IOException {
+ ParsingLoadable<T> loadable = new ParsingLoadable<>(dataSource, uri, type, parser);
+ loadable.load();
+ return Assertions.checkNotNull(loadable.getResult());
+ }
+
+ /**
+ * Loads a single parsable object.
+ *
+ * @param dataSource The {@link DataSource} through which the object should be read.
+ * @param parser The {@link Parser} to parse the object from the response.
+ * @param dataSpec The {@link DataSpec} of the object to read.
+ * @param type The type of the data. One of the {@link C}{@code DATA_TYPE_*} constants.
+ * @return The parsed object
+ * @throws IOException Thrown if there is an error while loading or parsing.
+ */
+ public static <T> T load(
+ DataSource dataSource, Parser<? extends T> parser, DataSpec dataSpec, int type)
+ throws IOException {
+ ParsingLoadable<T> loadable = new ParsingLoadable<>(dataSource, dataSpec, type, parser);
+ loadable.load();
+ return Assertions.checkNotNull(loadable.getResult());
+ }
+
+ /**
+ * The {@link DataSpec} that defines the data to be loaded.
+ */
+ public final DataSpec dataSpec;
+ /**
+ * The type of the data. One of the {@code DATA_TYPE_*} constants defined in {@link C}. For
+ * reporting only.
+ */
+ public final int type;
+
+ private final StatsDataSource dataSource;
+ private final Parser<? extends T> parser;
+
+ private volatile @Nullable T result;
+
+ /**
+ * @param dataSource A {@link DataSource} to use when loading the data.
+ * @param uri The {@link Uri} from which the object should be loaded.
+ * @param type See {@link #type}.
+ * @param parser Parses the object from the response.
+ */
+ public ParsingLoadable(DataSource dataSource, Uri uri, int type, Parser<? extends T> parser) {
+ this(dataSource, new DataSpec(uri, DataSpec.FLAG_ALLOW_GZIP), type, parser);
+ }
+
+ /**
+ * @param dataSource A {@link DataSource} to use when loading the data.
+ * @param dataSpec The {@link DataSpec} from which the object should be loaded.
+ * @param type See {@link #type}.
+ * @param parser Parses the object from the response.
+ */
+ public ParsingLoadable(DataSource dataSource, DataSpec dataSpec, int type,
+ Parser<? extends T> parser) {
+ this.dataSource = new StatsDataSource(dataSource);
+ this.dataSpec = dataSpec;
+ this.type = type;
+ this.parser = parser;
+ }
+
+ /** Returns the loaded object, or null if an object has not been loaded. */
+ public final @Nullable T getResult() {
+ return result;
+ }
+
+ /**
+ * Returns the number of bytes loaded. In the case that the network response was compressed, the
+ * value returned is the size of the data <em>after</em> decompression. Must only be called after
+ * the load completed, failed, or was canceled.
+ */
+ public long bytesLoaded() {
+ return dataSource.getBytesRead();
+ }
+
+ /**
+ * Returns the {@link Uri} from which data was read. If redirection occurred, this is the
+ * redirected uri. Must only be called after the load completed, failed, or was canceled.
+ */
+ public Uri getUri() {
+ return dataSource.getLastOpenedUri();
+ }
+
+ /**
+ * Returns the response headers associated with the load. Must only be called after the load
+ * completed, failed, or was canceled.
+ */
+ public Map<String, List<String>> getResponseHeaders() {
+ return dataSource.getLastResponseHeaders();
+ }
+
+ @Override
+ public final void cancelLoad() {
+ // Do nothing.
+ }
+
+ @Override
+ public final void load() throws IOException {
+ // We always load from the beginning, so reset bytesRead to 0.
+ dataSource.resetBytesRead();
+ DataSourceInputStream inputStream = new DataSourceInputStream(dataSource, dataSpec);
+ try {
+ inputStream.open();
+ Uri dataSourceUri = Assertions.checkNotNull(dataSource.getUri());
+ result = parser.parse(dataSourceUri, inputStream);
+ } finally {
+ Util.closeQuietly(inputStream);
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/PriorityDataSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/PriorityDataSource.java
new file mode 100644
index 0000000000..18a7fb6238
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/PriorityDataSource.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream;
+
+import android.net.Uri;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.PriorityTaskManager;
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A {@link DataSource} that can be used as part of a task registered with a
+ * {@link PriorityTaskManager}.
+ * <p>
+ * Calls to {@link #open(DataSpec)} and {@link #read(byte[], int, int)} are allowed to proceed only
+ * if there are no higher priority tasks registered to the {@link PriorityTaskManager}. If there
+ * exists a higher priority task then {@link PriorityTaskManager.PriorityTooLowException} is thrown.
+ * <p>
+ * Instances of this class are intended to be used as parts of (possibly larger) tasks that are
+ * registered with the {@link PriorityTaskManager}, and hence do <em>not</em> register as tasks
+ * themselves.
+ */
+public final class PriorityDataSource implements DataSource {
+
+ private final DataSource upstream;
+ private final PriorityTaskManager priorityTaskManager;
+ private final int priority;
+
+ /**
+ * @param upstream The upstream {@link DataSource}.
+ * @param priorityTaskManager The priority manager to which the task is registered.
+ * @param priority The priority of the task.
+ */
+ public PriorityDataSource(DataSource upstream, PriorityTaskManager priorityTaskManager,
+ int priority) {
+ this.upstream = Assertions.checkNotNull(upstream);
+ this.priorityTaskManager = Assertions.checkNotNull(priorityTaskManager);
+ this.priority = priority;
+ }
+
+ @Override
+ public void addTransferListener(TransferListener transferListener) {
+ upstream.addTransferListener(transferListener);
+ }
+
+ @Override
+ public long open(DataSpec dataSpec) throws IOException {
+ priorityTaskManager.proceedOrThrow(priority);
+ return upstream.open(dataSpec);
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int max) throws IOException {
+ priorityTaskManager.proceedOrThrow(priority);
+ return upstream.read(buffer, offset, max);
+ }
+
+ @Override
+ @Nullable
+ public Uri getUri() {
+ return upstream.getUri();
+ }
+
+ @Override
+ public Map<String, List<String>> getResponseHeaders() {
+ return upstream.getResponseHeaders();
+ }
+
+ @Override
+ public void close() throws IOException {
+ upstream.close();
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/PriorityDataSourceFactory.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/PriorityDataSourceFactory.java
new file mode 100644
index 0000000000..cf9a89f51d
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/PriorityDataSourceFactory.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource.Factory;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.PriorityTaskManager;
+
+/**
+ * A {@link DataSource.Factory} that produces {@link PriorityDataSource} instances.
+ */
+public final class PriorityDataSourceFactory implements Factory {
+
+ private final Factory upstreamFactory;
+ private final PriorityTaskManager priorityTaskManager;
+ private final int priority;
+
+ /**
+ * @param upstreamFactory A {@link DataSource.Factory} to be used to create an upstream {@link
+ * DataSource} for {@link PriorityDataSource}.
+ * @param priorityTaskManager The priority manager to which PriorityDataSource task is registered.
+ * @param priority The priority of PriorityDataSource task.
+ */
+ public PriorityDataSourceFactory(Factory upstreamFactory, PriorityTaskManager priorityTaskManager,
+ int priority) {
+ this.upstreamFactory = upstreamFactory;
+ this.priorityTaskManager = priorityTaskManager;
+ this.priority = priority;
+ }
+
+ @Override
+ public PriorityDataSource createDataSource() {
+ return new PriorityDataSource(upstreamFactory.createDataSource(), priorityTaskManager,
+ priority);
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/RawResourceDataSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/RawResourceDataSource.java
new file mode 100644
index 0000000000..ec5263d8ac
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/RawResourceDataSource.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream;
+
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull;
+
+import android.content.Context;
+import android.content.res.AssetFileDescriptor;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.text.TextUtils;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import java.io.EOFException;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * A {@link DataSource} for reading a raw resource inside the APK.
+ *
+ * <p>URIs supported by this source are of the form {@code rawresource:///rawResourceId}, where
+ * rawResourceId is the integer identifier of a raw resource. {@link #buildRawResourceUri(int)} can
+ * be used to build {@link Uri}s in this format.
+ */
+public final class RawResourceDataSource extends BaseDataSource {
+
+ /**
+ * Thrown when an {@link IOException} is encountered reading from a raw resource.
+ */
+ public static class RawResourceDataSourceException extends IOException {
+ public RawResourceDataSourceException(String message) {
+ super(message);
+ }
+
+ public RawResourceDataSourceException(IOException e) {
+ super(e);
+ }
+ }
+
+ /**
+ * Builds a {@link Uri} for the specified raw resource identifier.
+ *
+ * @param rawResourceId A raw resource identifier (i.e. a constant defined in {@code R.raw}).
+ * @return The corresponding {@link Uri}.
+ */
+ public static Uri buildRawResourceUri(int rawResourceId) {
+ return Uri.parse(RAW_RESOURCE_SCHEME + ":///" + rawResourceId);
+ }
+
+ /** The scheme part of a raw resource URI. */
+ public static final String RAW_RESOURCE_SCHEME = "rawresource";
+
+ private final Resources resources;
+
+ @Nullable private Uri uri;
+ @Nullable private AssetFileDescriptor assetFileDescriptor;
+ @Nullable private InputStream inputStream;
+ private long bytesRemaining;
+ private boolean opened;
+
+ /**
+ * @param context A context.
+ */
+ public RawResourceDataSource(Context context) {
+ super(/* isNetwork= */ false);
+ this.resources = context.getResources();
+ }
+
+ @Override
+ public long open(DataSpec dataSpec) throws RawResourceDataSourceException {
+ try {
+ Uri uri = dataSpec.uri;
+ this.uri = uri;
+ if (!TextUtils.equals(RAW_RESOURCE_SCHEME, uri.getScheme())) {
+ throw new RawResourceDataSourceException("URI must use scheme " + RAW_RESOURCE_SCHEME);
+ }
+
+ int resourceId;
+ try {
+ resourceId = Integer.parseInt(Assertions.checkNotNull(uri.getLastPathSegment()));
+ } catch (NumberFormatException e) {
+ throw new RawResourceDataSourceException("Resource identifier must be an integer.");
+ }
+
+ transferInitializing(dataSpec);
+ AssetFileDescriptor assetFileDescriptor = resources.openRawResourceFd(resourceId);
+ this.assetFileDescriptor = assetFileDescriptor;
+ if (assetFileDescriptor == null) {
+ throw new RawResourceDataSourceException("Resource is compressed: " + uri);
+ }
+ FileInputStream inputStream = new FileInputStream(assetFileDescriptor.getFileDescriptor());
+ this.inputStream = inputStream;
+
+ inputStream.skip(assetFileDescriptor.getStartOffset());
+ long skipped = inputStream.skip(dataSpec.position);
+ if (skipped < dataSpec.position) {
+ // We expect the skip to be satisfied in full. If it isn't then we're probably trying to
+ // skip beyond the end of the data.
+ throw new EOFException();
+ }
+ if (dataSpec.length != C.LENGTH_UNSET) {
+ bytesRemaining = dataSpec.length;
+ } else {
+ long assetFileDescriptorLength = assetFileDescriptor.getLength();
+ // If the length is UNKNOWN_LENGTH then the asset extends to the end of the file.
+ bytesRemaining = assetFileDescriptorLength == AssetFileDescriptor.UNKNOWN_LENGTH
+ ? C.LENGTH_UNSET : (assetFileDescriptorLength - dataSpec.position);
+ }
+ } catch (IOException e) {
+ throw new RawResourceDataSourceException(e);
+ }
+
+ opened = true;
+ transferStarted(dataSpec);
+
+ return bytesRemaining;
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int readLength) throws RawResourceDataSourceException {
+ if (readLength == 0) {
+ return 0;
+ } else if (bytesRemaining == 0) {
+ return C.RESULT_END_OF_INPUT;
+ }
+
+ int bytesRead;
+ try {
+ int bytesToRead = bytesRemaining == C.LENGTH_UNSET ? readLength
+ : (int) Math.min(bytesRemaining, readLength);
+ bytesRead = castNonNull(inputStream).read(buffer, offset, bytesToRead);
+ } catch (IOException e) {
+ throw new RawResourceDataSourceException(e);
+ }
+
+ if (bytesRead == -1) {
+ if (bytesRemaining != C.LENGTH_UNSET) {
+ // End of stream reached having not read sufficient data.
+ throw new RawResourceDataSourceException(new EOFException());
+ }
+ return C.RESULT_END_OF_INPUT;
+ }
+ if (bytesRemaining != C.LENGTH_UNSET) {
+ bytesRemaining -= bytesRead;
+ }
+ bytesTransferred(bytesRead);
+ return bytesRead;
+ }
+
+ @Override
+ @Nullable
+ public Uri getUri() {
+ return uri;
+ }
+
+ @SuppressWarnings("Finally")
+ @Override
+ public void close() throws RawResourceDataSourceException {
+ uri = null;
+ try {
+ if (inputStream != null) {
+ inputStream.close();
+ }
+ } catch (IOException e) {
+ throw new RawResourceDataSourceException(e);
+ } finally {
+ inputStream = null;
+ try {
+ if (assetFileDescriptor != null) {
+ assetFileDescriptor.close();
+ }
+ } catch (IOException e) {
+ throw new RawResourceDataSourceException(e);
+ } finally {
+ assetFileDescriptor = null;
+ if (opened) {
+ opened = false;
+ transferEnded();
+ }
+ }
+ }
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ResolvingDataSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ResolvingDataSource.java
new file mode 100644
index 0000000000..80046e1757
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ResolvingDataSource.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream;
+
+import android.net.Uri;
+import androidx.annotation.Nullable;
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+/** {@link DataSource} wrapper allowing just-in-time resolution of {@link DataSpec DataSpecs}. */
+public final class ResolvingDataSource implements DataSource {
+
+ /** Resolves {@link DataSpec DataSpecs}. */
+ public interface Resolver {
+
+ /**
+ * Resolves a {@link DataSpec} before forwarding it to the wrapped {@link DataSource}. This
+ * method is allowed to block until the {@link DataSpec} has been resolved.
+ *
+ * <p>Note that this method is called for every new connection, so caching of results is
+ * recommended, especially if network operations are involved.
+ *
+ * @param dataSpec The original {@link DataSpec}.
+ * @return The resolved {@link DataSpec}.
+ * @throws IOException If an {@link IOException} occurred while resolving the {@link DataSpec}.
+ */
+ DataSpec resolveDataSpec(DataSpec dataSpec) throws IOException;
+
+ /**
+ * Resolves a URI reported by {@link DataSource#getUri()} for event reporting and caching
+ * purposes.
+ *
+ * <p>Implementations do not need to overwrite this method unless they want to change the
+ * reported URI.
+ *
+ * <p>This method is <em>not</em> allowed to block.
+ *
+ * @param uri The URI as reported by {@link DataSource#getUri()}.
+ * @return The resolved URI used for event reporting and caching.
+ */
+ default Uri resolveReportedUri(Uri uri) {
+ return uri;
+ }
+ }
+
+ /** {@link DataSource.Factory} for {@link ResolvingDataSource} instances. */
+ public static final class Factory implements DataSource.Factory {
+
+ private final DataSource.Factory upstreamFactory;
+ private final Resolver resolver;
+
+ /**
+ * @param upstreamFactory The wrapped {@link DataSource.Factory} for handling resolved {@link
+ * DataSpec DataSpecs}.
+ * @param resolver The {@link Resolver} to resolve the {@link DataSpec DataSpecs}.
+ */
+ public Factory(DataSource.Factory upstreamFactory, Resolver resolver) {
+ this.upstreamFactory = upstreamFactory;
+ this.resolver = resolver;
+ }
+
+ @Override
+ public ResolvingDataSource createDataSource() {
+ return new ResolvingDataSource(upstreamFactory.createDataSource(), resolver);
+ }
+ }
+
+ private final DataSource upstreamDataSource;
+ private final Resolver resolver;
+
+ private boolean upstreamOpened;
+
+ /**
+ * @param upstreamDataSource The wrapped {@link DataSource}.
+ * @param resolver The {@link Resolver} to resolve the {@link DataSpec DataSpecs}.
+ */
+ public ResolvingDataSource(DataSource upstreamDataSource, Resolver resolver) {
+ this.upstreamDataSource = upstreamDataSource;
+ this.resolver = resolver;
+ }
+
+ @Override
+ public void addTransferListener(TransferListener transferListener) {
+ upstreamDataSource.addTransferListener(transferListener);
+ }
+
+ @Override
+ public long open(DataSpec dataSpec) throws IOException {
+ DataSpec resolvedDataSpec = resolver.resolveDataSpec(dataSpec);
+ upstreamOpened = true;
+ return upstreamDataSource.open(resolvedDataSpec);
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int readLength) throws IOException {
+ return upstreamDataSource.read(buffer, offset, readLength);
+ }
+
+ @Nullable
+ @Override
+ public Uri getUri() {
+ Uri reportedUri = upstreamDataSource.getUri();
+ return reportedUri == null ? null : resolver.resolveReportedUri(reportedUri);
+ }
+
+ @Override
+ public Map<String, List<String>> getResponseHeaders() {
+ return upstreamDataSource.getResponseHeaders();
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (upstreamOpened) {
+ upstreamOpened = false;
+ upstreamDataSource.close();
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/StatsDataSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/StatsDataSource.java
new file mode 100644
index 0000000000..e2a179cc9d
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/StatsDataSource.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream;
+
+import android.net.Uri;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * {@link DataSource} wrapper which keeps track of bytes transferred, redirected uris, and response
+ * headers.
+ */
+public final class StatsDataSource implements DataSource {
+
+ private final DataSource dataSource;
+
+ private long bytesRead;
+ private Uri lastOpenedUri;
+ private Map<String, List<String>> lastResponseHeaders;
+
+ /**
+ * Creates the stats data source.
+ *
+ * @param dataSource The wrapped {@link DataSource}.
+ */
+ public StatsDataSource(DataSource dataSource) {
+ this.dataSource = Assertions.checkNotNull(dataSource);
+ lastOpenedUri = Uri.EMPTY;
+ lastResponseHeaders = Collections.emptyMap();
+ }
+
+ /** Resets the number of bytes read as returned from {@link #getBytesRead()} to zero. */
+ public void resetBytesRead() {
+ bytesRead = 0;
+ }
+
+ /** Returns the total number of bytes that have been read from the data source. */
+ public long getBytesRead() {
+ return bytesRead;
+ }
+
+ /**
+ * Returns the {@link Uri} associated with the last {@link #open(DataSpec)} call. If redirection
+ * occurred, this is the redirected uri.
+ */
+ public Uri getLastOpenedUri() {
+ return lastOpenedUri;
+ }
+
+ /** Returns the response headers associated with the last {@link #open(DataSpec)} call. */
+ public Map<String, List<String>> getLastResponseHeaders() {
+ return lastResponseHeaders;
+ }
+
+ @Override
+ public void addTransferListener(TransferListener transferListener) {
+ dataSource.addTransferListener(transferListener);
+ }
+
+ @Override
+ public long open(DataSpec dataSpec) throws IOException {
+ // Reassign defaults in case dataSource.open throws an exception.
+ lastOpenedUri = dataSpec.uri;
+ lastResponseHeaders = Collections.emptyMap();
+ long availableBytes = dataSource.open(dataSpec);
+ lastOpenedUri = Assertions.checkNotNull(getUri());
+ lastResponseHeaders = getResponseHeaders();
+ return availableBytes;
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int readLength) throws IOException {
+ int bytesRead = dataSource.read(buffer, offset, readLength);
+ if (bytesRead != C.RESULT_END_OF_INPUT) {
+ this.bytesRead += bytesRead;
+ }
+ return bytesRead;
+ }
+
+ @Override
+ @Nullable
+ public Uri getUri() {
+ return dataSource.getUri();
+ }
+
+ @Override
+ public Map<String, List<String>> getResponseHeaders() {
+ return dataSource.getResponseHeaders();
+ }
+
+ @Override
+ public void close() throws IOException {
+ dataSource.close();
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/TeeDataSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/TeeDataSource.java
new file mode 100644
index 0000000000..c6063b916f
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/TeeDataSource.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream;
+
+import android.net.Uri;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Tees data into a {@link DataSink} as the data is read.
+ */
+public final class TeeDataSource implements DataSource {
+
+ private final DataSource upstream;
+ private final DataSink dataSink;
+
+ private boolean dataSinkNeedsClosing;
+ private long bytesRemaining;
+
+ /**
+ * @param upstream The upstream {@link DataSource}.
+ * @param dataSink The {@link DataSink} into which data is written.
+ */
+ public TeeDataSource(DataSource upstream, DataSink dataSink) {
+ this.upstream = Assertions.checkNotNull(upstream);
+ this.dataSink = Assertions.checkNotNull(dataSink);
+ }
+
+ @Override
+ public void addTransferListener(TransferListener transferListener) {
+ upstream.addTransferListener(transferListener);
+ }
+
+ @Override
+ public long open(DataSpec dataSpec) throws IOException {
+ bytesRemaining = upstream.open(dataSpec);
+ if (bytesRemaining == 0) {
+ return 0;
+ }
+ if (dataSpec.length == C.LENGTH_UNSET && bytesRemaining != C.LENGTH_UNSET) {
+ // Reconstruct dataSpec in order to provide the resolved length to the sink.
+ dataSpec = dataSpec.subrange(0, bytesRemaining);
+ }
+ dataSinkNeedsClosing = true;
+ dataSink.open(dataSpec);
+ return bytesRemaining;
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int max) throws IOException {
+ if (bytesRemaining == 0) {
+ return C.RESULT_END_OF_INPUT;
+ }
+ int bytesRead = upstream.read(buffer, offset, max);
+ if (bytesRead > 0) {
+ // TODO: Consider continuing even if writes to the sink fail.
+ dataSink.write(buffer, offset, bytesRead);
+ if (bytesRemaining != C.LENGTH_UNSET) {
+ bytesRemaining -= bytesRead;
+ }
+ }
+ return bytesRead;
+ }
+
+ @Override
+ @Nullable
+ public Uri getUri() {
+ return upstream.getUri();
+ }
+
+ @Override
+ public Map<String, List<String>> getResponseHeaders() {
+ return upstream.getResponseHeaders();
+ }
+
+ @Override
+ public void close() throws IOException {
+ try {
+ upstream.close();
+ } finally {
+ if (dataSinkNeedsClosing) {
+ dataSinkNeedsClosing = false;
+ dataSink.close();
+ }
+ }
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/TransferListener.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/TransferListener.java
new file mode 100644
index 0000000000..f6574120ff
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/TransferListener.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream;
+
+/**
+ * A listener of data transfer events.
+ *
+ * <p>A transfer usually progresses through multiple steps:
+ *
+ * <ol>
+ * <li>Initializing the underlying resource (e.g. opening a HTTP connection). {@link
+ * #onTransferInitializing(DataSource, DataSpec, boolean)} is called before the initialization
+ * starts.
+ * <li>Starting the transfer after successfully initializing the resource. {@link
+ * #onTransferStart(DataSource, DataSpec, boolean)} is called. Note that this only happens if
+ * the initialization was successful.
+ * <li>Transferring data. {@link #onBytesTransferred(DataSource, DataSpec, boolean, int)} is
+ * called frequently during the transfer to indicate progress.
+ * <li>Closing the transfer and the underlying resource. {@link #onTransferEnd(DataSource,
+ * DataSpec, boolean)} is called. Note that each {@link #onTransferStart(DataSource, DataSpec,
+ * boolean)} will have exactly one corresponding call to {@link #onTransferEnd(DataSource,
+ * DataSpec, boolean)}.
+ * </ol>
+ */
+public interface TransferListener {
+
+ /**
+ * Called when a transfer is being initialized.
+ *
+ * @param source The source performing the transfer.
+ * @param dataSpec Describes the data for which the transfer is initialized.
+ * @param isNetwork Whether the data is transferred through a network.
+ */
+ void onTransferInitializing(DataSource source, DataSpec dataSpec, boolean isNetwork);
+
+ /**
+ * Called when a transfer starts.
+ *
+ * @param source The source performing the transfer.
+ * @param dataSpec Describes the data being transferred.
+ * @param isNetwork Whether the data is transferred through a network.
+ */
+ void onTransferStart(DataSource source, DataSpec dataSpec, boolean isNetwork);
+
+ /**
+ * Called incrementally during a transfer.
+ *
+ * @param source The source performing the transfer.
+ * @param dataSpec Describes the data being transferred.
+ * @param isNetwork Whether the data is transferred through a network.
+ * @param bytesTransferred The number of bytes transferred since the previous call to this method
+ */
+ void onBytesTransferred(
+ DataSource source, DataSpec dataSpec, boolean isNetwork, int bytesTransferred);
+
+ /**
+ * Called when a transfer ends.
+ *
+ * @param source The source performing the transfer.
+ * @param dataSpec Describes the data being transferred.
+ * @param isNetwork Whether the data is transferred through a network.
+ */
+ void onTransferEnd(DataSource source, DataSpec dataSpec, boolean isNetwork);
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/UdpDataSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/UdpDataSource.java
new file mode 100644
index 0000000000..8e9b44563c
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/UdpDataSource.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream;
+
+import android.net.Uri;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.MulticastSocket;
+import java.net.SocketException;
+
+/** A UDP {@link DataSource}. */
+public final class UdpDataSource extends BaseDataSource {
+
+ /**
+ * Thrown when an error is encountered when trying to read from a {@link UdpDataSource}.
+ */
+ public static final class UdpDataSourceException extends IOException {
+
+ public UdpDataSourceException(IOException cause) {
+ super(cause);
+ }
+
+ }
+
+ /**
+ * The default maximum datagram packet size, in bytes.
+ */
+ public static final int DEFAULT_MAX_PACKET_SIZE = 2000;
+
+ /** The default socket timeout, in milliseconds. */
+ public static final int DEFAULT_SOCKET_TIMEOUT_MILLIS = 8 * 1000;
+
+ private final int socketTimeoutMillis;
+ private final byte[] packetBuffer;
+ private final DatagramPacket packet;
+
+ @Nullable private Uri uri;
+ @Nullable private DatagramSocket socket;
+ @Nullable private MulticastSocket multicastSocket;
+ @Nullable private InetAddress address;
+ @Nullable private InetSocketAddress socketAddress;
+ private boolean opened;
+
+ private int packetRemaining;
+
+ public UdpDataSource() {
+ this(DEFAULT_MAX_PACKET_SIZE);
+ }
+
+ /**
+ * Constructs a new instance.
+ *
+ * @param maxPacketSize The maximum datagram packet size, in bytes.
+ */
+ public UdpDataSource(int maxPacketSize) {
+ this(maxPacketSize, DEFAULT_SOCKET_TIMEOUT_MILLIS);
+ }
+
+ /**
+ * Constructs a new instance.
+ *
+ * @param maxPacketSize The maximum datagram packet size, in bytes.
+ * @param socketTimeoutMillis The socket timeout in milliseconds. A timeout of zero is interpreted
+ * as an infinite timeout.
+ */
+ public UdpDataSource(int maxPacketSize, int socketTimeoutMillis) {
+ super(/* isNetwork= */ true);
+ this.socketTimeoutMillis = socketTimeoutMillis;
+ packetBuffer = new byte[maxPacketSize];
+ packet = new DatagramPacket(packetBuffer, 0, maxPacketSize);
+ }
+
+ @Override
+ public long open(DataSpec dataSpec) throws UdpDataSourceException {
+ uri = dataSpec.uri;
+ String host = uri.getHost();
+ int port = uri.getPort();
+ transferInitializing(dataSpec);
+ try {
+ address = InetAddress.getByName(host);
+ socketAddress = new InetSocketAddress(address, port);
+ if (address.isMulticastAddress()) {
+ multicastSocket = new MulticastSocket(socketAddress);
+ multicastSocket.joinGroup(address);
+ socket = multicastSocket;
+ } else {
+ socket = new DatagramSocket(socketAddress);
+ }
+ } catch (IOException e) {
+ throw new UdpDataSourceException(e);
+ }
+
+ try {
+ socket.setSoTimeout(socketTimeoutMillis);
+ } catch (SocketException e) {
+ throw new UdpDataSourceException(e);
+ }
+
+ opened = true;
+ transferStarted(dataSpec);
+ return C.LENGTH_UNSET;
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int readLength) throws UdpDataSourceException {
+ if (readLength == 0) {
+ return 0;
+ }
+
+ if (packetRemaining == 0) {
+ // We've read all of the data from the current packet. Get another.
+ try {
+ socket.receive(packet);
+ } catch (IOException e) {
+ throw new UdpDataSourceException(e);
+ }
+ packetRemaining = packet.getLength();
+ bytesTransferred(packetRemaining);
+ }
+
+ int packetOffset = packet.getLength() - packetRemaining;
+ int bytesToRead = Math.min(packetRemaining, readLength);
+ System.arraycopy(packetBuffer, packetOffset, buffer, offset, bytesToRead);
+ packetRemaining -= bytesToRead;
+ return bytesToRead;
+ }
+
+ @Override
+ @Nullable
+ public Uri getUri() {
+ return uri;
+ }
+
+ @Override
+ public void close() {
+ uri = null;
+ if (multicastSocket != null) {
+ try {
+ multicastSocket.leaveGroup(address);
+ } catch (IOException e) {
+ // Do nothing.
+ }
+ multicastSocket = null;
+ }
+ if (socket != null) {
+ socket.close();
+ socket = null;
+ }
+ address = null;
+ socketAddress = null;
+ packetRemaining = 0;
+ if (opened) {
+ opened = false;
+ transferEnded();
+ }
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/Cache.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/Cache.java
new file mode 100644
index 0000000000..cb90d95bb4
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/Cache.java
@@ -0,0 +1,286 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import java.io.File;
+import java.io.IOException;
+import java.util.NavigableSet;
+import java.util.Set;
+
+/**
+ * An interface for cache.
+ */
+public interface Cache {
+
+ /**
+ * Listener of {@link Cache} events.
+ */
+ interface Listener {
+
+ /**
+ * Called when a {@link CacheSpan} is added to the cache.
+ *
+ * @param cache The source of the event.
+ * @param span The added {@link CacheSpan}.
+ */
+ void onSpanAdded(Cache cache, CacheSpan span);
+
+ /**
+ * Called when a {@link CacheSpan} is removed from the cache.
+ *
+ * @param cache The source of the event.
+ * @param span The removed {@link CacheSpan}.
+ */
+ void onSpanRemoved(Cache cache, CacheSpan span);
+
+ /**
+ * Called when an existing {@link CacheSpan} is touched, causing it to be replaced. The new
+ * {@link CacheSpan} is guaranteed to represent the same data as the one it replaces, however
+ * {@link CacheSpan#file} and {@link CacheSpan#lastTouchTimestamp} may have changed.
+ *
+ * <p>Note that for span replacement, {@link #onSpanAdded(Cache, CacheSpan)} and {@link
+ * #onSpanRemoved(Cache, CacheSpan)} are not called in addition to this method.
+ *
+ * @param cache The source of the event.
+ * @param oldSpan The old {@link CacheSpan}, which has been removed from the cache.
+ * @param newSpan The new {@link CacheSpan}, which has been added to the cache.
+ */
+ void onSpanTouched(Cache cache, CacheSpan oldSpan, CacheSpan newSpan);
+ }
+
+ /**
+ * Thrown when an error is encountered when writing data.
+ */
+ class CacheException extends IOException {
+
+ public CacheException(String message) {
+ super(message);
+ }
+
+ public CacheException(Throwable cause) {
+ super(cause);
+ }
+
+ public CacheException(String message, Throwable cause) {
+ super(message, cause);
+ }
+ }
+
+ /**
+ * Returned by {@link #getUid()} if initialization failed before the unique identifier was read or
+ * generated.
+ */
+ long UID_UNSET = -1;
+
+ /**
+ * Returns a non-negative unique identifier for the cache, or {@link #UID_UNSET} if initialization
+ * failed before the unique identifier was determined.
+ *
+ * <p>Implementations are expected to generate and store the unique identifier alongside the
+ * cached content. If the location of the cache is deleted or swapped, it is expected that a new
+ * unique identifier will be generated when the cache is recreated.
+ */
+ long getUid();
+
+ /**
+ * Releases the cache. This method must be called when the cache is no longer required. The cache
+ * must not be used after calling this method.
+ *
+ * <p>This method may be slow and shouldn't normally be called on the main thread.
+ */
+ @WorkerThread
+ void release();
+
+ /**
+ * Registers a listener to listen for changes to a given key.
+ *
+ * <p>No guarantees are made about the thread or threads on which the listener is called, but it
+ * is guaranteed that listener methods will be called in a serial fashion (i.e. one at a time) and
+ * in the same order as events occurred.
+ *
+ * @param key The key to listen to.
+ * @param listener The listener to add.
+ * @return The current spans for the key.
+ */
+ NavigableSet<CacheSpan> addListener(String key, Listener listener);
+
+ /**
+ * Unregisters a listener.
+ *
+ * @param key The key to stop listening to.
+ * @param listener The listener to remove.
+ */
+ void removeListener(String key, Listener listener);
+
+ /**
+ * Returns the cached spans for a given cache key.
+ *
+ * @param key The key for which spans should be returned.
+ * @return The spans for the key.
+ */
+ NavigableSet<CacheSpan> getCachedSpans(String key);
+
+ /**
+ * Returns all keys in the cache.
+ *
+ * @return All the keys in the cache.
+ */
+ Set<String> getKeys();
+
+ /**
+ * Returns the total disk space in bytes used by the cache.
+ *
+ * @return The total disk space in bytes.
+ */
+ long getCacheSpace();
+
+ /**
+ * A caller should invoke this method when they require data from a given position for a given
+ * key.
+ *
+ * <p>If there is a cache entry that overlaps the position, then the returned {@link CacheSpan}
+ * defines the file in which the data is stored. {@link CacheSpan#isCached} is true. The caller
+ * may read from the cache file, but does not acquire any locks.
+ *
+ * <p>If there is no cache entry overlapping {@code offset}, then the returned {@link CacheSpan}
+ * defines a hole in the cache starting at {@code position} into which the caller may write as it
+ * obtains the data from some other source. The returned {@link CacheSpan} serves as a lock.
+ * Whilst the caller holds the lock it may write data into the hole. It may split data into
+ * multiple files. When the caller has finished writing a file it should commit it to the cache by
+ * calling {@link #commitFile(File, long)}. When the caller has finished writing, it must release
+ * the lock by calling {@link #releaseHoleSpan}.
+ *
+ * <p>This method may be slow and shouldn't normally be called on the main thread.
+ *
+ * @param key The key of the data being requested.
+ * @param position The position of the data being requested.
+ * @return The {@link CacheSpan}.
+ * @throws InterruptedException If the thread was interrupted.
+ * @throws CacheException If an error is encountered.
+ */
+ @WorkerThread
+ CacheSpan startReadWrite(String key, long position) throws InterruptedException, CacheException;
+
+ /**
+ * Same as {@link #startReadWrite(String, long)}. However, if the cache entry is locked, then
+ * instead of blocking, this method will return null as the {@link CacheSpan}.
+ *
+ * <p>This method may be slow and shouldn't normally be called on the main thread.
+ *
+ * @param key The key of the data being requested.
+ * @param position The position of the data being requested.
+ * @return The {@link CacheSpan}. Or null if the cache entry is locked.
+ * @throws CacheException If an error is encountered.
+ */
+ @WorkerThread
+ @Nullable
+ CacheSpan startReadWriteNonBlocking(String key, long position) throws CacheException;
+
+ /**
+ * Obtains a cache file into which data can be written. Must only be called when holding a
+ * corresponding hole {@link CacheSpan} obtained from {@link #startReadWrite(String, long)}.
+ *
+ * <p>This method may be slow and shouldn't normally be called on the main thread.
+ *
+ * @param key The cache key for the data.
+ * @param position The starting position of the data.
+ * @param length The length of the data being written, or {@link C#LENGTH_UNSET} if unknown. Used
+ * only to ensure that there is enough space in the cache.
+ * @return The file into which data should be written.
+ * @throws CacheException If an error is encountered.
+ */
+ @WorkerThread
+ File startFile(String key, long position, long length) throws CacheException;
+
+ /**
+ * Commits a file into the cache. Must only be called when holding a corresponding hole {@link
+ * CacheSpan} obtained from {@link #startReadWrite(String, long)}.
+ *
+ * <p>This method may be slow and shouldn't normally be called on the main thread.
+ *
+ * @param file A newly written cache file.
+ * @param length The length of the newly written cache file in bytes.
+ * @throws CacheException If an error is encountered.
+ */
+ @WorkerThread
+ void commitFile(File file, long length) throws CacheException;
+
+ /**
+ * Releases a {@link CacheSpan} obtained from {@link #startReadWrite(String, long)} which
+ * corresponded to a hole in the cache.
+ *
+ * @param holeSpan The {@link CacheSpan} being released.
+ */
+ void releaseHoleSpan(CacheSpan holeSpan);
+
+ /**
+ * Removes a cached {@link CacheSpan} from the cache, deleting the underlying file.
+ *
+ * <p>This method may be slow and shouldn't normally be called on the main thread.
+ *
+ * @param span The {@link CacheSpan} to remove.
+ * @throws CacheException If an error is encountered.
+ */
+ @WorkerThread
+ void removeSpan(CacheSpan span) throws CacheException;
+
+ /**
+ * Queries if a range is entirely available in the cache.
+ *
+ * @param key The cache key for the data.
+ * @param position The starting position of the data.
+ * @param length The length of the data.
+ * @return true if the data is available in the Cache otherwise false;
+ */
+ boolean isCached(String key, long position, long length);
+
+ /**
+ * Returns the length of the cached data block starting from the {@code position} to the block end
+ * up to {@code length} bytes. If the {@code position} isn't cached then -(the length of the gap
+ * to the next cached data up to {@code length} bytes) is returned.
+ *
+ * @param key The cache key for the data.
+ * @param position The starting position of the data.
+ * @param length The maximum length of the data to be returned.
+ * @return The length of the cached or not cached data block length.
+ */
+ long getCachedLength(String key, long position, long length);
+
+ /**
+ * Applies {@code mutations} to the {@link ContentMetadata} for the given key. A new {@link
+ * CachedContent} is added if there isn't one already with the given key.
+ *
+ * <p>This method may be slow and shouldn't normally be called on the main thread.
+ *
+ * @param key The cache key for the data.
+ * @param mutations Contains mutations to be applied to the metadata.
+ * @throws CacheException If an error is encountered.
+ */
+ @WorkerThread
+ void applyContentMetadataMutations(String key, ContentMetadataMutations mutations)
+ throws CacheException;
+
+ /**
+ * Returns a {@link ContentMetadata} for the given key.
+ *
+ * @param key The cache key for the data.
+ * @return A {@link ContentMetadata} for the given key.
+ */
+ ContentMetadata getContentMetadata(String key);
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java
new file mode 100644
index 0000000000..e372a02851
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSink;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache.Cache.CacheException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ReusableBufferedOutputStream;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * Writes data into a cache.
+ *
+ * <p>If the {@link DataSpec} passed to {@link #open(DataSpec)} has the {@code length} field set to
+ * {@link C#LENGTH_UNSET} and {@link DataSpec#FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN} set, then {@link
+ * #write(byte[], int, int)} calls are ignored.
+ */
+public final class CacheDataSink implements DataSink {
+
+ /** Default {@code fragmentSize} recommended for caching use cases. */
+ public static final long DEFAULT_FRAGMENT_SIZE = 5 * 1024 * 1024;
+ /** Default buffer size in bytes. */
+ public static final int DEFAULT_BUFFER_SIZE = 20 * 1024;
+
+ private static final long MIN_RECOMMENDED_FRAGMENT_SIZE = 2 * 1024 * 1024;
+ private static final String TAG = "CacheDataSink";
+
+ private final Cache cache;
+ private final long fragmentSize;
+ private final int bufferSize;
+
+ private DataSpec dataSpec;
+ private long dataSpecFragmentSize;
+ private File file;
+ private OutputStream outputStream;
+ private long outputStreamBytesWritten;
+ private long dataSpecBytesWritten;
+ private ReusableBufferedOutputStream bufferedOutputStream;
+
+ /**
+ * Thrown when IOException is encountered when writing data into sink.
+ */
+ public static class CacheDataSinkException extends CacheException {
+
+ public CacheDataSinkException(IOException cause) {
+ super(cause);
+ }
+
+ }
+
+ /**
+ * Constructs an instance using {@link #DEFAULT_BUFFER_SIZE}.
+ *
+ * @param cache The cache into which data should be written.
+ * @param fragmentSize For requests that should be fragmented into multiple cache files, this is
+ * the maximum size of a cache file in bytes. If set to {@link C#LENGTH_UNSET} then no
+ * fragmentation will occur. Using a small value allows for finer-grained cache eviction
+ * policies, at the cost of increased overhead both on the cache implementation and the file
+ * system. Values under {@code (2 * 1024 * 1024)} are not recommended.
+ */
+ public CacheDataSink(Cache cache, long fragmentSize) {
+ this(cache, fragmentSize, DEFAULT_BUFFER_SIZE);
+ }
+
+ /**
+ * @param cache The cache into which data should be written.
+ * @param fragmentSize For requests that should be fragmented into multiple cache files, this is
+ * the maximum size of a cache file in bytes. If set to {@link C#LENGTH_UNSET} then no
+ * fragmentation will occur. Using a small value allows for finer-grained cache eviction
+ * policies, at the cost of increased overhead both on the cache implementation and the file
+ * system. Values under {@code (2 * 1024 * 1024)} are not recommended.
+ * @param bufferSize The buffer size in bytes for writing to a cache file. A zero or negative
+ * value disables buffering.
+ */
+ public CacheDataSink(Cache cache, long fragmentSize, int bufferSize) {
+ Assertions.checkState(
+ fragmentSize > 0 || fragmentSize == C.LENGTH_UNSET,
+ "fragmentSize must be positive or C.LENGTH_UNSET.");
+ if (fragmentSize != C.LENGTH_UNSET && fragmentSize < MIN_RECOMMENDED_FRAGMENT_SIZE) {
+ Log.w(
+ TAG,
+ "fragmentSize is below the minimum recommended value of "
+ + MIN_RECOMMENDED_FRAGMENT_SIZE
+ + ". This may cause poor cache performance.");
+ }
+ this.cache = Assertions.checkNotNull(cache);
+ this.fragmentSize = fragmentSize == C.LENGTH_UNSET ? Long.MAX_VALUE : fragmentSize;
+ this.bufferSize = bufferSize;
+ }
+
+ @Override
+ public void open(DataSpec dataSpec) throws CacheDataSinkException {
+ if (dataSpec.length == C.LENGTH_UNSET
+ && dataSpec.isFlagSet(DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN)) {
+ this.dataSpec = null;
+ return;
+ }
+ this.dataSpec = dataSpec;
+ this.dataSpecFragmentSize =
+ dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION) ? fragmentSize : Long.MAX_VALUE;
+ dataSpecBytesWritten = 0;
+ try {
+ openNextOutputStream();
+ } catch (IOException e) {
+ throw new CacheDataSinkException(e);
+ }
+ }
+
+ @Override
+ public void write(byte[] buffer, int offset, int length) throws CacheDataSinkException {
+ if (dataSpec == null) {
+ return;
+ }
+ try {
+ int bytesWritten = 0;
+ while (bytesWritten < length) {
+ if (outputStreamBytesWritten == dataSpecFragmentSize) {
+ closeCurrentOutputStream();
+ openNextOutputStream();
+ }
+ int bytesToWrite =
+ (int) Math.min(length - bytesWritten, dataSpecFragmentSize - outputStreamBytesWritten);
+ outputStream.write(buffer, offset + bytesWritten, bytesToWrite);
+ bytesWritten += bytesToWrite;
+ outputStreamBytesWritten += bytesToWrite;
+ dataSpecBytesWritten += bytesToWrite;
+ }
+ } catch (IOException e) {
+ throw new CacheDataSinkException(e);
+ }
+ }
+
+ @Override
+ public void close() throws CacheDataSinkException {
+ if (dataSpec == null) {
+ return;
+ }
+ try {
+ closeCurrentOutputStream();
+ } catch (IOException e) {
+ throw new CacheDataSinkException(e);
+ }
+ }
+
+ private void openNextOutputStream() throws IOException {
+ long length =
+ dataSpec.length == C.LENGTH_UNSET
+ ? C.LENGTH_UNSET
+ : Math.min(dataSpec.length - dataSpecBytesWritten, dataSpecFragmentSize);
+ file =
+ cache.startFile(
+ dataSpec.key, dataSpec.absoluteStreamPosition + dataSpecBytesWritten, length);
+ FileOutputStream underlyingFileOutputStream = new FileOutputStream(file);
+ if (bufferSize > 0) {
+ if (bufferedOutputStream == null) {
+ bufferedOutputStream = new ReusableBufferedOutputStream(underlyingFileOutputStream,
+ bufferSize);
+ } else {
+ bufferedOutputStream.reset(underlyingFileOutputStream);
+ }
+ outputStream = bufferedOutputStream;
+ } else {
+ outputStream = underlyingFileOutputStream;
+ }
+ outputStreamBytesWritten = 0;
+ }
+
+ private void closeCurrentOutputStream() throws IOException {
+ if (outputStream == null) {
+ return;
+ }
+
+ boolean success = false;
+ try {
+ outputStream.flush();
+ success = true;
+ } finally {
+ Util.closeQuietly(outputStream);
+ outputStream = null;
+ File fileToCommit = file;
+ file = null;
+ if (success) {
+ cache.commitFile(fileToCommit, outputStreamBytesWritten);
+ } else {
+ fileToCommit.delete();
+ }
+ }
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java
new file mode 100644
index 0000000000..51ba6f4294
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSink;
+
+/**
+ * A {@link DataSink.Factory} that produces {@link CacheDataSink}.
+ */
+public final class CacheDataSinkFactory implements DataSink.Factory {
+
+ private final Cache cache;
+ private final long fragmentSize;
+ private final int bufferSize;
+
+ /** @see CacheDataSink#CacheDataSink(Cache, long) */
+ public CacheDataSinkFactory(Cache cache, long fragmentSize) {
+ this(cache, fragmentSize, CacheDataSink.DEFAULT_BUFFER_SIZE);
+ }
+
+ /** @see CacheDataSink#CacheDataSink(Cache, long, int) */
+ public CacheDataSinkFactory(Cache cache, long fragmentSize, int bufferSize) {
+ this.cache = cache;
+ this.fragmentSize = fragmentSize;
+ this.bufferSize = bufferSize;
+ }
+
+ @Override
+ public DataSink createDataSink() {
+ return new CacheDataSink(cache, fragmentSize, bufferSize);
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java
new file mode 100644
index 0000000000..19fb8191e4
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java
@@ -0,0 +1,580 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache;
+
+import android.net.Uri;
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSink;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSourceException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec.HttpMethod;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.FileDataSource;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.TeeDataSource;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.TransferListener;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache.Cache.CacheException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A {@link DataSource} that reads and writes a {@link Cache}. Requests are fulfilled from the cache
+ * when possible. When data is not cached it is requested from an upstream {@link DataSource} and
+ * written into the cache.
+ */
+public final class CacheDataSource implements DataSource {
+
+ /**
+ * Flags controlling the CacheDataSource's behavior. Possible flag values are {@link
+ * #FLAG_BLOCK_ON_CACHE}, {@link #FLAG_IGNORE_CACHE_ON_ERROR} and {@link
+ * #FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS}.
+ */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ flag = true,
+ value = {
+ FLAG_BLOCK_ON_CACHE,
+ FLAG_IGNORE_CACHE_ON_ERROR,
+ FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS
+ })
+ public @interface Flags {}
+ /**
+ * A flag indicating whether we will block reads if the cache key is locked. If unset then data is
+ * read from upstream if the cache key is locked, regardless of whether the data is cached.
+ */
+ public static final int FLAG_BLOCK_ON_CACHE = 1;
+
+ /**
+ * A flag indicating whether the cache is bypassed following any cache related error. If set
+ * then cache related exceptions may be thrown for one cycle of open, read and close calls.
+ * Subsequent cycles of these calls will then bypass the cache.
+ */
+ public static final int FLAG_IGNORE_CACHE_ON_ERROR = 1 << 1; // 2
+
+ /**
+ * A flag indicating that the cache should be bypassed for requests whose lengths are unset. This
+ * flag is provided for legacy reasons only.
+ */
+ public static final int FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS = 1 << 2; // 4
+
+ /**
+ * Reasons the cache may be ignored. One of {@link #CACHE_IGNORED_REASON_ERROR} or {@link
+ * #CACHE_IGNORED_REASON_UNSET_LENGTH}.
+ */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({CACHE_IGNORED_REASON_ERROR, CACHE_IGNORED_REASON_UNSET_LENGTH})
+ public @interface CacheIgnoredReason {}
+
+ /** Cache not ignored. */
+ private static final int CACHE_NOT_IGNORED = -1;
+
+ /** Cache ignored due to a cache related error. */
+ public static final int CACHE_IGNORED_REASON_ERROR = 0;
+
+ /** Cache ignored due to a request with an unset length. */
+ public static final int CACHE_IGNORED_REASON_UNSET_LENGTH = 1;
+
+ /**
+ * Listener of {@link CacheDataSource} events.
+ */
+ public interface EventListener {
+
+ /**
+ * Called when bytes have been read from the cache.
+ *
+ * @param cacheSizeBytes Current cache size in bytes.
+ * @param cachedBytesRead Total bytes read from the cache since this method was last called.
+ */
+ void onCachedBytesRead(long cacheSizeBytes, long cachedBytesRead);
+
+ /**
+ * Called when the current request ignores cache.
+ *
+ * @param reason Reason cache is bypassed.
+ */
+ void onCacheIgnored(@CacheIgnoredReason int reason);
+ }
+
+ /** Minimum number of bytes to read before checking cache for availability. */
+ private static final long MIN_READ_BEFORE_CHECKING_CACHE = 100 * 1024;
+
+ private final Cache cache;
+ private final DataSource cacheReadDataSource;
+ @Nullable private final DataSource cacheWriteDataSource;
+ private final DataSource upstreamDataSource;
+ private final CacheKeyFactory cacheKeyFactory;
+ @Nullable private final EventListener eventListener;
+
+ private final boolean blockOnCache;
+ private final boolean ignoreCacheOnError;
+ private final boolean ignoreCacheForUnsetLengthRequests;
+
+ @Nullable private DataSource currentDataSource;
+ private boolean currentDataSpecLengthUnset;
+ @Nullable private Uri uri;
+ @Nullable private Uri actualUri;
+ @HttpMethod private int httpMethod;
+ @Nullable private byte[] httpBody;
+ private Map<String, String> httpRequestHeaders = Collections.emptyMap();
+ @DataSpec.Flags private int flags;
+ @Nullable private String key;
+ private long readPosition;
+ private long bytesRemaining;
+ @Nullable private CacheSpan currentHoleSpan;
+ private boolean seenCacheError;
+ private boolean currentRequestIgnoresCache;
+ private long totalCachedBytesRead;
+ private long checkCachePosition;
+
+ /**
+ * Constructs an instance with default {@link DataSource} and {@link DataSink} instances for
+ * reading and writing the cache.
+ *
+ * @param cache The cache.
+ * @param upstream A {@link DataSource} for reading data not in the cache.
+ */
+ public CacheDataSource(Cache cache, DataSource upstream) {
+ this(cache, upstream, /* flags= */ 0);
+ }
+
+ /**
+ * Constructs an instance with default {@link DataSource} and {@link DataSink} instances for
+ * reading and writing the cache.
+ *
+ * @param cache The cache.
+ * @param upstream A {@link DataSource} for reading data not in the cache.
+ * @param flags A combination of {@link #FLAG_BLOCK_ON_CACHE}, {@link #FLAG_IGNORE_CACHE_ON_ERROR}
+ * and {@link #FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS}, or 0.
+ */
+ public CacheDataSource(Cache cache, DataSource upstream, @Flags int flags) {
+ this(
+ cache,
+ upstream,
+ new FileDataSource(),
+ new CacheDataSink(cache, CacheDataSink.DEFAULT_FRAGMENT_SIZE),
+ flags,
+ /* eventListener= */ null);
+ }
+
+ /**
+ * Constructs an instance with arbitrary {@link DataSource} and {@link DataSink} instances for
+ * reading and writing the cache. One use of this constructor is to allow data to be transformed
+ * before it is written to disk.
+ *
+ * @param cache The cache.
+ * @param upstream A {@link DataSource} for reading data not in the cache.
+ * @param cacheReadDataSource A {@link DataSource} for reading data from the cache.
+ * @param cacheWriteDataSink A {@link DataSink} for writing data to the cache. If null, cache is
+ * accessed read-only.
+ * @param flags A combination of {@link #FLAG_BLOCK_ON_CACHE}, {@link #FLAG_IGNORE_CACHE_ON_ERROR}
+ * and {@link #FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS}, or 0.
+ * @param eventListener An optional {@link EventListener} to receive events.
+ */
+ public CacheDataSource(
+ Cache cache,
+ DataSource upstream,
+ DataSource cacheReadDataSource,
+ @Nullable DataSink cacheWriteDataSink,
+ @Flags int flags,
+ @Nullable EventListener eventListener) {
+ this(
+ cache,
+ upstream,
+ cacheReadDataSource,
+ cacheWriteDataSink,
+ flags,
+ eventListener,
+ /* cacheKeyFactory= */ null);
+ }
+
+ /**
+ * Constructs an instance with arbitrary {@link DataSource} and {@link DataSink} instances for
+ * reading and writing the cache. One use of this constructor is to allow data to be transformed
+ * before it is written to disk.
+ *
+ * @param cache The cache.
+ * @param upstream A {@link DataSource} for reading data not in the cache.
+ * @param cacheReadDataSource A {@link DataSource} for reading data from the cache.
+ * @param cacheWriteDataSink A {@link DataSink} for writing data to the cache. If null, cache is
+ * accessed read-only.
+ * @param flags A combination of {@link #FLAG_BLOCK_ON_CACHE}, {@link #FLAG_IGNORE_CACHE_ON_ERROR}
+ * and {@link #FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS}, or 0.
+ * @param eventListener An optional {@link EventListener} to receive events.
+ * @param cacheKeyFactory An optional factory for cache keys.
+ */
+ public CacheDataSource(
+ Cache cache,
+ DataSource upstream,
+ DataSource cacheReadDataSource,
+ @Nullable DataSink cacheWriteDataSink,
+ @Flags int flags,
+ @Nullable EventListener eventListener,
+ @Nullable CacheKeyFactory cacheKeyFactory) {
+ this.cache = cache;
+ this.cacheReadDataSource = cacheReadDataSource;
+ this.cacheKeyFactory =
+ cacheKeyFactory != null ? cacheKeyFactory : CacheUtil.DEFAULT_CACHE_KEY_FACTORY;
+ this.blockOnCache = (flags & FLAG_BLOCK_ON_CACHE) != 0;
+ this.ignoreCacheOnError = (flags & FLAG_IGNORE_CACHE_ON_ERROR) != 0;
+ this.ignoreCacheForUnsetLengthRequests =
+ (flags & FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS) != 0;
+ this.upstreamDataSource = upstream;
+ if (cacheWriteDataSink != null) {
+ this.cacheWriteDataSource = new TeeDataSource(upstream, cacheWriteDataSink);
+ } else {
+ this.cacheWriteDataSource = null;
+ }
+ this.eventListener = eventListener;
+ }
+
+ @Override
+ public void addTransferListener(TransferListener transferListener) {
+ cacheReadDataSource.addTransferListener(transferListener);
+ upstreamDataSource.addTransferListener(transferListener);
+ }
+
+ @Override
+ public long open(DataSpec dataSpec) throws IOException {
+ try {
+ key = cacheKeyFactory.buildCacheKey(dataSpec);
+ uri = dataSpec.uri;
+ actualUri = getRedirectedUriOrDefault(cache, key, /* defaultUri= */ uri);
+ httpMethod = dataSpec.httpMethod;
+ httpBody = dataSpec.httpBody;
+ httpRequestHeaders = dataSpec.httpRequestHeaders;
+ flags = dataSpec.flags;
+ readPosition = dataSpec.position;
+
+ int reason = shouldIgnoreCacheForRequest(dataSpec);
+ currentRequestIgnoresCache = reason != CACHE_NOT_IGNORED;
+ if (currentRequestIgnoresCache) {
+ notifyCacheIgnored(reason);
+ }
+
+ if (dataSpec.length != C.LENGTH_UNSET || currentRequestIgnoresCache) {
+ bytesRemaining = dataSpec.length;
+ } else {
+ bytesRemaining = ContentMetadata.getContentLength(cache.getContentMetadata(key));
+ if (bytesRemaining != C.LENGTH_UNSET) {
+ bytesRemaining -= dataSpec.position;
+ if (bytesRemaining <= 0) {
+ throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE);
+ }
+ }
+ }
+ openNextSource(false);
+ return bytesRemaining;
+ } catch (Throwable e) {
+ handleBeforeThrow(e);
+ throw e;
+ }
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int readLength) throws IOException {
+ if (readLength == 0) {
+ return 0;
+ }
+ if (bytesRemaining == 0) {
+ return C.RESULT_END_OF_INPUT;
+ }
+ try {
+ if (readPosition >= checkCachePosition) {
+ openNextSource(true);
+ }
+ int bytesRead = currentDataSource.read(buffer, offset, readLength);
+ if (bytesRead != C.RESULT_END_OF_INPUT) {
+ if (isReadingFromCache()) {
+ totalCachedBytesRead += bytesRead;
+ }
+ readPosition += bytesRead;
+ if (bytesRemaining != C.LENGTH_UNSET) {
+ bytesRemaining -= bytesRead;
+ }
+ } else if (currentDataSpecLengthUnset) {
+ setNoBytesRemainingAndMaybeStoreLength();
+ } else if (bytesRemaining > 0 || bytesRemaining == C.LENGTH_UNSET) {
+ closeCurrentSource();
+ openNextSource(false);
+ return read(buffer, offset, readLength);
+ }
+ return bytesRead;
+ } catch (IOException e) {
+ if (currentDataSpecLengthUnset && CacheUtil.isCausedByPositionOutOfRange(e)) {
+ setNoBytesRemainingAndMaybeStoreLength();
+ return C.RESULT_END_OF_INPUT;
+ }
+ handleBeforeThrow(e);
+ throw e;
+ } catch (Throwable e) {
+ handleBeforeThrow(e);
+ throw e;
+ }
+ }
+
+ @Override
+ @Nullable
+ public Uri getUri() {
+ return actualUri;
+ }
+
+ @Override
+ public Map<String, List<String>> getResponseHeaders() {
+ // TODO: Implement.
+ return isReadingFromUpstream()
+ ? upstreamDataSource.getResponseHeaders()
+ : Collections.emptyMap();
+ }
+
+ @Override
+ public void close() throws IOException {
+ uri = null;
+ actualUri = null;
+ httpMethod = DataSpec.HTTP_METHOD_GET;
+ httpBody = null;
+ httpRequestHeaders = Collections.emptyMap();
+ flags = 0;
+ readPosition = 0;
+ key = null;
+ notifyBytesRead();
+ try {
+ closeCurrentSource();
+ } catch (Throwable e) {
+ handleBeforeThrow(e);
+ throw e;
+ }
+ }
+
+ /**
+ * Opens the next source. If the cache contains data spanning the current read position then
+ * {@link #cacheReadDataSource} is opened to read from it. Else {@link #upstreamDataSource} is
+ * opened to read from the upstream source and write into the cache.
+ *
+ * <p>There must not be a currently open source when this method is called, except in the case
+ * that {@code checkCache} is true. If {@code checkCache} is true then there must be a currently
+ * open source, and it must be {@link #upstreamDataSource}. It will be closed and a new source
+ * opened if it's possible to switch to reading from or writing to the cache. If a switch isn't
+ * possible then the current source is left unchanged.
+ *
+ * @param checkCache If true tries to switch to reading from or writing to cache instead of
+ * reading from {@link #upstreamDataSource}, which is the currently open source.
+ */
+ private void openNextSource(boolean checkCache) throws IOException {
+ CacheSpan nextSpan;
+ if (currentRequestIgnoresCache) {
+ nextSpan = null;
+ } else if (blockOnCache) {
+ try {
+ nextSpan = cache.startReadWrite(key, readPosition);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new InterruptedIOException();
+ }
+ } else {
+ nextSpan = cache.startReadWriteNonBlocking(key, readPosition);
+ }
+
+ DataSpec nextDataSpec;
+ DataSource nextDataSource;
+ if (nextSpan == null) {
+ // The data is locked in the cache, or we're ignoring the cache. Bypass the cache and read
+ // from upstream.
+ nextDataSource = upstreamDataSource;
+ nextDataSpec =
+ new DataSpec(
+ uri,
+ httpMethod,
+ httpBody,
+ readPosition,
+ readPosition,
+ bytesRemaining,
+ key,
+ flags,
+ httpRequestHeaders);
+ } else if (nextSpan.isCached) {
+ // Data is cached, read from cache.
+ Uri fileUri = Uri.fromFile(nextSpan.file);
+ long filePosition = readPosition - nextSpan.position;
+ long length = nextSpan.length - filePosition;
+ if (bytesRemaining != C.LENGTH_UNSET) {
+ length = Math.min(length, bytesRemaining);
+ }
+ // Deliberately skip the HTTP-related parameters since we're reading from the cache, not
+ // making an HTTP request.
+ nextDataSpec = new DataSpec(fileUri, readPosition, filePosition, length, key, flags);
+ nextDataSource = cacheReadDataSource;
+ } else {
+ // Data is not cached, and data is not locked, read from upstream with cache backing.
+ long length;
+ if (nextSpan.isOpenEnded()) {
+ length = bytesRemaining;
+ } else {
+ length = nextSpan.length;
+ if (bytesRemaining != C.LENGTH_UNSET) {
+ length = Math.min(length, bytesRemaining);
+ }
+ }
+ nextDataSpec =
+ new DataSpec(
+ uri,
+ httpMethod,
+ httpBody,
+ readPosition,
+ readPosition,
+ length,
+ key,
+ flags,
+ httpRequestHeaders);
+ if (cacheWriteDataSource != null) {
+ nextDataSource = cacheWriteDataSource;
+ } else {
+ nextDataSource = upstreamDataSource;
+ cache.releaseHoleSpan(nextSpan);
+ nextSpan = null;
+ }
+ }
+
+ checkCachePosition =
+ !currentRequestIgnoresCache && nextDataSource == upstreamDataSource
+ ? readPosition + MIN_READ_BEFORE_CHECKING_CACHE
+ : Long.MAX_VALUE;
+ if (checkCache) {
+ Assertions.checkState(isBypassingCache());
+ if (nextDataSource == upstreamDataSource) {
+ // Continue reading from upstream.
+ return;
+ }
+ // We're switching to reading from or writing to the cache.
+ try {
+ closeCurrentSource();
+ } catch (Throwable e) {
+ if (nextSpan.isHoleSpan()) {
+ // Release the hole span before throwing, else we'll hold it forever.
+ cache.releaseHoleSpan(nextSpan);
+ }
+ throw e;
+ }
+ }
+
+ if (nextSpan != null && nextSpan.isHoleSpan()) {
+ currentHoleSpan = nextSpan;
+ }
+ currentDataSource = nextDataSource;
+ currentDataSpecLengthUnset = nextDataSpec.length == C.LENGTH_UNSET;
+ long resolvedLength = nextDataSource.open(nextDataSpec);
+
+ // Update bytesRemaining, actualUri and (if writing to cache) the cache metadata.
+ ContentMetadataMutations mutations = new ContentMetadataMutations();
+ if (currentDataSpecLengthUnset && resolvedLength != C.LENGTH_UNSET) {
+ bytesRemaining = resolvedLength;
+ ContentMetadataMutations.setContentLength(mutations, readPosition + bytesRemaining);
+ }
+ if (isReadingFromUpstream()) {
+ actualUri = currentDataSource.getUri();
+ boolean isRedirected = !uri.equals(actualUri);
+ ContentMetadataMutations.setRedirectedUri(mutations, isRedirected ? actualUri : null);
+ }
+ if (isWritingToCache()) {
+ cache.applyContentMetadataMutations(key, mutations);
+ }
+ }
+
+ private void setNoBytesRemainingAndMaybeStoreLength() throws IOException {
+ bytesRemaining = 0;
+ if (isWritingToCache()) {
+ ContentMetadataMutations mutations = new ContentMetadataMutations();
+ ContentMetadataMutations.setContentLength(mutations, readPosition);
+ cache.applyContentMetadataMutations(key, mutations);
+ }
+ }
+
+ private static Uri getRedirectedUriOrDefault(Cache cache, String key, Uri defaultUri) {
+ Uri redirectedUri = ContentMetadata.getRedirectedUri(cache.getContentMetadata(key));
+ return redirectedUri != null ? redirectedUri : defaultUri;
+ }
+
+ private boolean isReadingFromUpstream() {
+ return !isReadingFromCache();
+ }
+
+ private boolean isBypassingCache() {
+ return currentDataSource == upstreamDataSource;
+ }
+
+ private boolean isReadingFromCache() {
+ return currentDataSource == cacheReadDataSource;
+ }
+
+ private boolean isWritingToCache() {
+ return currentDataSource == cacheWriteDataSource;
+ }
+
+ private void closeCurrentSource() throws IOException {
+ if (currentDataSource == null) {
+ return;
+ }
+ try {
+ currentDataSource.close();
+ } finally {
+ currentDataSource = null;
+ currentDataSpecLengthUnset = false;
+ if (currentHoleSpan != null) {
+ cache.releaseHoleSpan(currentHoleSpan);
+ currentHoleSpan = null;
+ }
+ }
+ }
+
+ private void handleBeforeThrow(Throwable exception) {
+ if (isReadingFromCache() || exception instanceof CacheException) {
+ seenCacheError = true;
+ }
+ }
+
+ private int shouldIgnoreCacheForRequest(DataSpec dataSpec) {
+ if (ignoreCacheOnError && seenCacheError) {
+ return CACHE_IGNORED_REASON_ERROR;
+ } else if (ignoreCacheForUnsetLengthRequests && dataSpec.length == C.LENGTH_UNSET) {
+ return CACHE_IGNORED_REASON_UNSET_LENGTH;
+ } else {
+ return CACHE_NOT_IGNORED;
+ }
+ }
+
+ private void notifyCacheIgnored(@CacheIgnoredReason int reason) {
+ if (eventListener != null) {
+ eventListener.onCacheIgnored(reason);
+ }
+ }
+
+ private void notifyBytesRead() {
+ if (eventListener != null && totalCachedBytesRead > 0) {
+ eventListener.onCachedBytesRead(cache.getCacheSpace(), totalCachedBytesRead);
+ totalCachedBytesRead = 0;
+ }
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java
new file mode 100644
index 0000000000..21aef3f93a
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache;
+
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSink;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.FileDataSource;
+
+/** A {@link DataSource.Factory} that produces {@link CacheDataSource}. */
+public final class CacheDataSourceFactory implements DataSource.Factory {
+
+ private final Cache cache;
+ private final DataSource.Factory upstreamFactory;
+ private final DataSource.Factory cacheReadDataSourceFactory;
+ @CacheDataSource.Flags private final int flags;
+ @Nullable private final DataSink.Factory cacheWriteDataSinkFactory;
+ @Nullable private final CacheDataSource.EventListener eventListener;
+ @Nullable private final CacheKeyFactory cacheKeyFactory;
+
+ /**
+ * Constructs a factory which creates {@link CacheDataSource} instances with default {@link
+ * DataSource} and {@link DataSink} instances for reading and writing the cache.
+ *
+ * @param cache The cache.
+ * @param upstreamFactory A {@link DataSource.Factory} for creating upstream {@link DataSource}s
+ * for reading data not in the cache.
+ */
+ public CacheDataSourceFactory(Cache cache, DataSource.Factory upstreamFactory) {
+ this(cache, upstreamFactory, /* flags= */ 0);
+ }
+
+ /** @see CacheDataSource#CacheDataSource(Cache, DataSource, int) */
+ public CacheDataSourceFactory(
+ Cache cache, DataSource.Factory upstreamFactory, @CacheDataSource.Flags int flags) {
+ this(
+ cache,
+ upstreamFactory,
+ new FileDataSource.Factory(),
+ new CacheDataSinkFactory(cache, CacheDataSink.DEFAULT_FRAGMENT_SIZE),
+ flags,
+ /* eventListener= */ null);
+ }
+
+ /**
+ * @see CacheDataSource#CacheDataSource(Cache, DataSource, DataSource, DataSink, int,
+ * CacheDataSource.EventListener)
+ */
+ public CacheDataSourceFactory(
+ Cache cache,
+ DataSource.Factory upstreamFactory,
+ DataSource.Factory cacheReadDataSourceFactory,
+ @Nullable DataSink.Factory cacheWriteDataSinkFactory,
+ @CacheDataSource.Flags int flags,
+ @Nullable CacheDataSource.EventListener eventListener) {
+ this(
+ cache,
+ upstreamFactory,
+ cacheReadDataSourceFactory,
+ cacheWriteDataSinkFactory,
+ flags,
+ eventListener,
+ /* cacheKeyFactory= */ null);
+ }
+
+ /**
+ * @see CacheDataSource#CacheDataSource(Cache, DataSource, DataSource, DataSink, int,
+ * CacheDataSource.EventListener, CacheKeyFactory)
+ */
+ public CacheDataSourceFactory(
+ Cache cache,
+ DataSource.Factory upstreamFactory,
+ DataSource.Factory cacheReadDataSourceFactory,
+ @Nullable DataSink.Factory cacheWriteDataSinkFactory,
+ @CacheDataSource.Flags int flags,
+ @Nullable CacheDataSource.EventListener eventListener,
+ @Nullable CacheKeyFactory cacheKeyFactory) {
+ this.cache = cache;
+ this.upstreamFactory = upstreamFactory;
+ this.cacheReadDataSourceFactory = cacheReadDataSourceFactory;
+ this.cacheWriteDataSinkFactory = cacheWriteDataSinkFactory;
+ this.flags = flags;
+ this.eventListener = eventListener;
+ this.cacheKeyFactory = cacheKeyFactory;
+ }
+
+ @Override
+ public CacheDataSource createDataSource() {
+ return new CacheDataSource(
+ cache,
+ upstreamFactory.createDataSource(),
+ cacheReadDataSourceFactory.createDataSource(),
+ cacheWriteDataSinkFactory == null ? null : cacheWriteDataSinkFactory.createDataSink(),
+ flags,
+ eventListener,
+ cacheKeyFactory);
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheEvictor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheEvictor.java
new file mode 100644
index 0000000000..017e84c8c8
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheEvictor.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+
+/**
+ * Evicts data from a {@link Cache}. Implementations should call {@link Cache#removeSpan(CacheSpan)}
+ * to evict cache entries based on their eviction policies.
+ */
+public interface CacheEvictor extends Cache.Listener {
+
+ /**
+ * Returns whether the evictor requires the {@link Cache} to touch {@link CacheSpan CacheSpans}
+ * when it accesses them. Implementations that do not use {@link CacheSpan#lastTouchTimestamp}
+ * should return {@code false}.
+ */
+ boolean requiresCacheSpanTouches();
+
+ /**
+ * Called when cache has been initialized.
+ */
+ void onCacheInitialized();
+
+ /**
+ * Called when a writer starts writing to the cache.
+ *
+ * @param cache The source of the event.
+ * @param key The key being written.
+ * @param position The starting position of the data being written.
+ * @param length The length of the data being written, or {@link C#LENGTH_UNSET} if unknown.
+ */
+ void onStartFile(Cache cache, String key, long position, long length);
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheFileMetadata.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheFileMetadata.java
new file mode 100644
index 0000000000..2618a3ef6a
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheFileMetadata.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache;
+
+/** Metadata associated with a cache file. */
+/* package */ final class CacheFileMetadata {
+
+ public final long length;
+ public final long lastTouchTimestamp;
+
+ public CacheFileMetadata(long length, long lastTouchTimestamp) {
+ this.length = length;
+ this.lastTouchTimestamp = lastTouchTimestamp;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java
new file mode 100644
index 0000000000..cd69336ff4
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.SQLException;
+import android.database.sqlite.SQLiteDatabase;
+import androidx.annotation.WorkerThread;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.database.DatabaseIOException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.database.DatabaseProvider;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.database.VersionTable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+
+/** Maintains an index of cache file metadata. */
+/* package */ final class CacheFileMetadataIndex {
+
+ private static final String TABLE_PREFIX = DatabaseProvider.TABLE_PREFIX + "CacheFileMetadata";
+ private static final int TABLE_VERSION = 1;
+
+ private static final String COLUMN_NAME = "name";
+ private static final String COLUMN_LENGTH = "length";
+ private static final String COLUMN_LAST_TOUCH_TIMESTAMP = "last_touch_timestamp";
+
+ private static final int COLUMN_INDEX_NAME = 0;
+ private static final int COLUMN_INDEX_LENGTH = 1;
+ private static final int COLUMN_INDEX_LAST_TOUCH_TIMESTAMP = 2;
+
+ private static final String WHERE_NAME_EQUALS = COLUMN_NAME + " = ?";
+
+ private static final String[] COLUMNS =
+ new String[] {
+ COLUMN_NAME, COLUMN_LENGTH, COLUMN_LAST_TOUCH_TIMESTAMP,
+ };
+ private static final String TABLE_SCHEMA =
+ "("
+ + COLUMN_NAME
+ + " TEXT PRIMARY KEY NOT NULL,"
+ + COLUMN_LENGTH
+ + " INTEGER NOT NULL,"
+ + COLUMN_LAST_TOUCH_TIMESTAMP
+ + " INTEGER NOT NULL)";
+
+ private final DatabaseProvider databaseProvider;
+
+ private @MonotonicNonNull String tableName;
+
+ /**
+ * Deletes index data for the specified cache.
+ *
+ * <p>This method may be slow and shouldn't normally be called on the main thread.
+ *
+ * @param databaseProvider Provides the database in which the index is stored.
+ * @param uid The cache UID.
+ * @throws DatabaseIOException If an error occurs deleting the index data.
+ */
+ @WorkerThread
+ public static void delete(DatabaseProvider databaseProvider, long uid)
+ throws DatabaseIOException {
+ String hexUid = Long.toHexString(uid);
+ try {
+ String tableName = getTableName(hexUid);
+ SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
+ writableDatabase.beginTransactionNonExclusive();
+ try {
+ VersionTable.removeVersion(
+ writableDatabase, VersionTable.FEATURE_CACHE_FILE_METADATA, hexUid);
+ dropTable(writableDatabase, tableName);
+ writableDatabase.setTransactionSuccessful();
+ } finally {
+ writableDatabase.endTransaction();
+ }
+ } catch (SQLException e) {
+ throw new DatabaseIOException(e);
+ }
+ }
+
+ /** @param databaseProvider Provides the database in which the index is stored. */
+ public CacheFileMetadataIndex(DatabaseProvider databaseProvider) {
+ this.databaseProvider = databaseProvider;
+ }
+
+ /**
+ * Initializes the index for the given cache UID.
+ *
+ * <p>This method may be slow and shouldn't normally be called on the main thread.
+ *
+ * @param uid The cache UID.
+ * @throws DatabaseIOException If an error occurs initializing the index.
+ */
+ @WorkerThread
+ public void initialize(long uid) throws DatabaseIOException {
+ try {
+ String hexUid = Long.toHexString(uid);
+ tableName = getTableName(hexUid);
+ SQLiteDatabase readableDatabase = databaseProvider.getReadableDatabase();
+ int version =
+ VersionTable.getVersion(
+ readableDatabase, VersionTable.FEATURE_CACHE_FILE_METADATA, hexUid);
+ if (version != TABLE_VERSION) {
+ SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
+ writableDatabase.beginTransactionNonExclusive();
+ try {
+ VersionTable.setVersion(
+ writableDatabase, VersionTable.FEATURE_CACHE_FILE_METADATA, hexUid, TABLE_VERSION);
+ dropTable(writableDatabase, tableName);
+ writableDatabase.execSQL("CREATE TABLE " + tableName + " " + TABLE_SCHEMA);
+ writableDatabase.setTransactionSuccessful();
+ } finally {
+ writableDatabase.endTransaction();
+ }
+ }
+ } catch (SQLException e) {
+ throw new DatabaseIOException(e);
+ }
+ }
+
+ /**
+ * Returns all file metadata keyed by file name. The returned map is mutable and may be modified
+ * by the caller.
+ *
+ * <p>This method may be slow and shouldn't normally be called on the main thread.
+ *
+ * @return The file metadata keyed by file name.
+ * @throws DatabaseIOException If an error occurs loading the metadata.
+ */
+ @WorkerThread
+ public Map<String, CacheFileMetadata> getAll() throws DatabaseIOException {
+ try (Cursor cursor = getCursor()) {
+ Map<String, CacheFileMetadata> fileMetadata = new HashMap<>(cursor.getCount());
+ while (cursor.moveToNext()) {
+ String name = cursor.getString(COLUMN_INDEX_NAME);
+ long length = cursor.getLong(COLUMN_INDEX_LENGTH);
+ long lastTouchTimestamp = cursor.getLong(COLUMN_INDEX_LAST_TOUCH_TIMESTAMP);
+ fileMetadata.put(name, new CacheFileMetadata(length, lastTouchTimestamp));
+ }
+ return fileMetadata;
+ } catch (SQLException e) {
+ throw new DatabaseIOException(e);
+ }
+ }
+
+ /**
+ * Sets metadata for a given file.
+ *
+ * <p>This method may be slow and shouldn't normally be called on the main thread.
+ *
+ * @param name The name of the file.
+ * @param length The file length.
+ * @param lastTouchTimestamp The file last touch timestamp.
+ * @throws DatabaseIOException If an error occurs setting the metadata.
+ */
+ @WorkerThread
+ public void set(String name, long length, long lastTouchTimestamp) throws DatabaseIOException {
+ Assertions.checkNotNull(tableName);
+ try {
+ SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
+ ContentValues values = new ContentValues();
+ values.put(COLUMN_NAME, name);
+ values.put(COLUMN_LENGTH, length);
+ values.put(COLUMN_LAST_TOUCH_TIMESTAMP, lastTouchTimestamp);
+ writableDatabase.replaceOrThrow(tableName, /* nullColumnHack= */ null, values);
+ } catch (SQLException e) {
+ throw new DatabaseIOException(e);
+ }
+ }
+
+ /**
+ * Removes metadata.
+ *
+ * <p>This method may be slow and shouldn't normally be called on the main thread.
+ *
+ * @param name The name of the file whose metadata is to be removed.
+ * @throws DatabaseIOException If an error occurs removing the metadata.
+ */
+ @WorkerThread
+ public void remove(String name) throws DatabaseIOException {
+ Assertions.checkNotNull(tableName);
+ try {
+ SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
+ writableDatabase.delete(tableName, WHERE_NAME_EQUALS, new String[] {name});
+ } catch (SQLException e) {
+ throw new DatabaseIOException(e);
+ }
+ }
+
+ /**
+ * Removes metadata.
+ *
+ * <p>This method may be slow and shouldn't normally be called on the main thread.
+ *
+ * @param names The names of the files whose metadata is to be removed.
+ * @throws DatabaseIOException If an error occurs removing the metadata.
+ */
+ @WorkerThread
+ public void removeAll(Set<String> names) throws DatabaseIOException {
+ Assertions.checkNotNull(tableName);
+ try {
+ SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
+ writableDatabase.beginTransactionNonExclusive();
+ try {
+ for (String name : names) {
+ writableDatabase.delete(tableName, WHERE_NAME_EQUALS, new String[] {name});
+ }
+ writableDatabase.setTransactionSuccessful();
+ } finally {
+ writableDatabase.endTransaction();
+ }
+ } catch (SQLException e) {
+ throw new DatabaseIOException(e);
+ }
+ }
+
+ private Cursor getCursor() {
+ Assertions.checkNotNull(tableName);
+ return databaseProvider
+ .getReadableDatabase()
+ .query(
+ tableName,
+ COLUMNS,
+ /* selection */ null,
+ /* selectionArgs= */ null,
+ /* groupBy= */ null,
+ /* having= */ null,
+ /* orderBy= */ null);
+ }
+
+ private static void dropTable(SQLiteDatabase writableDatabase, String tableName) {
+ writableDatabase.execSQL("DROP TABLE IF EXISTS " + tableName);
+ }
+
+ private static String getTableName(String hexUid) {
+ return TABLE_PREFIX + hexUid;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheKeyFactory.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheKeyFactory.java
new file mode 100644
index 0000000000..1c30a4b03e
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheKeyFactory.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec;
+
+/** Factory for cache keys. */
+public interface CacheKeyFactory {
+
+ /**
+ * Returns a cache key for the given {@link DataSpec}.
+ *
+ * @param dataSpec The data being cached.
+ */
+ String buildCacheKey(DataSpec dataSpec);
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheSpan.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheSpan.java
new file mode 100644
index 0000000000..f57544f12b
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheSpan.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import java.io.File;
+
+/**
+ * Defines a span of data that may or may not be cached (as indicated by {@link #isCached}).
+ */
+public class CacheSpan implements Comparable<CacheSpan> {
+
+ /**
+ * The cache key that uniquely identifies the original stream.
+ */
+ public final String key;
+ /**
+ * The position of the {@link CacheSpan} in the original stream.
+ */
+ public final long position;
+ /**
+ * The length of the {@link CacheSpan}, or {@link C#LENGTH_UNSET} if this is an open-ended hole.
+ */
+ public final long length;
+ /**
+ * Whether the {@link CacheSpan} is cached.
+ */
+ public final boolean isCached;
+ /** The file corresponding to this {@link CacheSpan}, or null if {@link #isCached} is false. */
+ @Nullable public final File file;
+ /** The last touch timestamp, or {@link C#TIME_UNSET} if {@link #isCached} is false. */
+ public final long lastTouchTimestamp;
+
+ /**
+ * Creates a hole CacheSpan which isn't cached, has no last touch timestamp and no file
+ * associated.
+ *
+ * @param key The cache key that uniquely identifies the original stream.
+ * @param position The position of the {@link CacheSpan} in the original stream.
+ * @param length The length of the {@link CacheSpan}, or {@link C#LENGTH_UNSET} if this is an
+ * open-ended hole.
+ */
+ public CacheSpan(String key, long position, long length) {
+ this(key, position, length, C.TIME_UNSET, null);
+ }
+
+ /**
+ * Creates a CacheSpan.
+ *
+ * @param key The cache key that uniquely identifies the original stream.
+ * @param position The position of the {@link CacheSpan} in the original stream.
+ * @param length The length of the {@link CacheSpan}, or {@link C#LENGTH_UNSET} if this is an
+ * open-ended hole.
+ * @param lastTouchTimestamp The last touch timestamp, or {@link C#TIME_UNSET} if {@link
+ * #isCached} is false.
+ * @param file The file corresponding to this {@link CacheSpan}, or null if it's a hole.
+ */
+ public CacheSpan(
+ String key, long position, long length, long lastTouchTimestamp, @Nullable File file) {
+ this.key = key;
+ this.position = position;
+ this.length = length;
+ this.isCached = file != null;
+ this.file = file;
+ this.lastTouchTimestamp = lastTouchTimestamp;
+ }
+
+ /**
+ * Returns whether this is an open-ended {@link CacheSpan}.
+ */
+ public boolean isOpenEnded() {
+ return length == C.LENGTH_UNSET;
+ }
+
+ /**
+ * Returns whether this is a hole {@link CacheSpan}.
+ */
+ public boolean isHoleSpan() {
+ return !isCached;
+ }
+
+ @Override
+ public int compareTo(@NonNull CacheSpan another) {
+ if (!key.equals(another.key)) {
+ return key.compareTo(another.key);
+ }
+ long startOffsetDiff = position - another.position;
+ return startOffsetDiff == 0 ? 0 : ((startOffsetDiff < 0) ? -1 : 1);
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheUtil.java
new file mode 100644
index 0000000000..01fef2b605
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheUtil.java
@@ -0,0 +1,434 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache;
+
+import android.net.Uri;
+import android.util.Pair;
+import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSourceException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.PriorityTaskManager;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.io.EOFException;
+import java.io.IOException;
+import java.util.NavigableSet;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Caching related utility methods.
+ */
+public final class CacheUtil {
+
+ /** Receives progress updates during cache operations. */
+ public interface ProgressListener {
+
+ /**
+ * Called when progress is made during a cache operation.
+ *
+ * @param requestLength The length of the content being cached in bytes, or {@link
+ * C#LENGTH_UNSET} if unknown.
+ * @param bytesCached The number of bytes that are cached.
+ * @param newBytesCached The number of bytes that have been newly cached since the last progress
+ * update.
+ */
+ void onProgress(long requestLength, long bytesCached, long newBytesCached);
+ }
+
+ /** Default buffer size to be used while caching. */
+ public static final int DEFAULT_BUFFER_SIZE_BYTES = 128 * 1024;
+
+ /** Default {@link CacheKeyFactory}. */
+ public static final CacheKeyFactory DEFAULT_CACHE_KEY_FACTORY =
+ (dataSpec) -> dataSpec.key != null ? dataSpec.key : generateKey(dataSpec.uri);
+
+ /**
+ * Generates a cache key out of the given {@link Uri}.
+ *
+ * @param uri Uri of a content which the requested key is for.
+ */
+ public static String generateKey(Uri uri) {
+ return uri.toString();
+ }
+
+ /**
+ * Queries the cache to obtain the request length and the number of bytes already cached for a
+ * given {@link DataSpec}.
+ *
+ * @param dataSpec Defines the data to be checked.
+ * @param cache A {@link Cache} which has the data.
+ * @param cacheKeyFactory An optional factory for cache keys.
+ * @return A pair containing the request length and the number of bytes that are already cached.
+ */
+ public static Pair<Long, Long> getCached(
+ DataSpec dataSpec, Cache cache, @Nullable CacheKeyFactory cacheKeyFactory) {
+ String key = buildCacheKey(dataSpec, cacheKeyFactory);
+ long position = dataSpec.absoluteStreamPosition;
+ long requestLength = getRequestLength(dataSpec, cache, key);
+ long bytesAlreadyCached = 0;
+ long bytesLeft = requestLength;
+ while (bytesLeft != 0) {
+ long blockLength =
+ cache.getCachedLength(
+ key, position, bytesLeft != C.LENGTH_UNSET ? bytesLeft : Long.MAX_VALUE);
+ if (blockLength > 0) {
+ bytesAlreadyCached += blockLength;
+ } else {
+ blockLength = -blockLength;
+ if (blockLength == Long.MAX_VALUE) {
+ break;
+ }
+ }
+ position += blockLength;
+ bytesLeft -= bytesLeft == C.LENGTH_UNSET ? 0 : blockLength;
+ }
+ return Pair.create(requestLength, bytesAlreadyCached);
+ }
+
+ /**
+ * Caches the data defined by {@code dataSpec}, skipping already cached data. Caching stops early
+ * if the end of the input is reached.
+ *
+ * <p>This method may be slow and shouldn't normally be called on the main thread.
+ *
+ * @param dataSpec Defines the data to be cached.
+ * @param cache A {@link Cache} to store the data.
+ * @param cacheKeyFactory An optional factory for cache keys.
+ * @param upstream A {@link DataSource} for reading data not in the cache.
+ * @param progressListener A listener to receive progress updates, or {@code null}.
+ * @param isCanceled An optional flag that will interrupt caching if set to true.
+ * @throws IOException If an error occurs reading from the source.
+ * @throws InterruptedException If the thread was interrupted directly or via {@code isCanceled}.
+ */
+ @WorkerThread
+ public static void cache(
+ DataSpec dataSpec,
+ Cache cache,
+ @Nullable CacheKeyFactory cacheKeyFactory,
+ DataSource upstream,
+ @Nullable ProgressListener progressListener,
+ @Nullable AtomicBoolean isCanceled)
+ throws IOException, InterruptedException {
+ cache(
+ dataSpec,
+ cache,
+ cacheKeyFactory,
+ new CacheDataSource(cache, upstream),
+ new byte[DEFAULT_BUFFER_SIZE_BYTES],
+ /* priorityTaskManager= */ null,
+ /* priority= */ 0,
+ progressListener,
+ isCanceled,
+ /* enableEOFException= */ false);
+ }
+
+ /**
+ * Caches the data defined by {@code dataSpec} while skipping already cached data. Caching stops
+ * early if end of input is reached and {@code enableEOFException} is false.
+ *
+ * <p>If a {@link PriorityTaskManager} is given, it's used to pause and resume caching depending
+ * on {@code priority} and the priority of other tasks registered to the PriorityTaskManager.
+ * Please note that it's the responsibility of the calling code to call {@link
+ * PriorityTaskManager#add} to register with the manager before calling this method, and to call
+ * {@link PriorityTaskManager#remove} afterwards to unregister.
+ *
+ * <p>This method may be slow and shouldn't normally be called on the main thread.
+ *
+ * @param dataSpec Defines the data to be cached.
+ * @param cache A {@link Cache} to store the data.
+ * @param cacheKeyFactory An optional factory for cache keys.
+ * @param dataSource A {@link CacheDataSource} that works on the {@code cache}.
+ * @param buffer The buffer to be used while caching.
+ * @param priorityTaskManager If not null it's used to check whether it is allowed to proceed with
+ * caching.
+ * @param priority The priority of this task. Used with {@code priorityTaskManager}.
+ * @param progressListener A listener to receive progress updates, or {@code null}.
+ * @param isCanceled An optional flag that will interrupt caching if set to true.
+ * @param enableEOFException Whether to throw an {@link EOFException} if end of input has been
+ * reached unexpectedly.
+ * @throws IOException If an error occurs reading from the source.
+ * @throws InterruptedException If the thread was interrupted directly or via {@code isCanceled}.
+ */
+ @WorkerThread
+ public static void cache(
+ DataSpec dataSpec,
+ Cache cache,
+ @Nullable CacheKeyFactory cacheKeyFactory,
+ CacheDataSource dataSource,
+ byte[] buffer,
+ @Nullable PriorityTaskManager priorityTaskManager,
+ int priority,
+ @Nullable ProgressListener progressListener,
+ @Nullable AtomicBoolean isCanceled,
+ boolean enableEOFException)
+ throws IOException, InterruptedException {
+ Assertions.checkNotNull(dataSource);
+ Assertions.checkNotNull(buffer);
+
+ String key = buildCacheKey(dataSpec, cacheKeyFactory);
+ long bytesLeft;
+ ProgressNotifier progressNotifier = null;
+ if (progressListener != null) {
+ progressNotifier = new ProgressNotifier(progressListener);
+ Pair<Long, Long> lengthAndBytesAlreadyCached = getCached(dataSpec, cache, cacheKeyFactory);
+ progressNotifier.init(lengthAndBytesAlreadyCached.first, lengthAndBytesAlreadyCached.second);
+ bytesLeft = lengthAndBytesAlreadyCached.first;
+ } else {
+ bytesLeft = getRequestLength(dataSpec, cache, key);
+ }
+
+ long position = dataSpec.absoluteStreamPosition;
+ boolean lengthUnset = bytesLeft == C.LENGTH_UNSET;
+ while (bytesLeft != 0) {
+ throwExceptionIfInterruptedOrCancelled(isCanceled);
+ long blockLength =
+ cache.getCachedLength(key, position, lengthUnset ? Long.MAX_VALUE : bytesLeft);
+ if (blockLength > 0) {
+ // Skip already cached data.
+ } else {
+ // There is a hole in the cache which is at least "-blockLength" long.
+ blockLength = -blockLength;
+ long length = blockLength == Long.MAX_VALUE ? C.LENGTH_UNSET : blockLength;
+ boolean isLastBlock = length == bytesLeft;
+ long read =
+ readAndDiscard(
+ dataSpec,
+ position,
+ length,
+ dataSource,
+ buffer,
+ priorityTaskManager,
+ priority,
+ progressNotifier,
+ isLastBlock,
+ isCanceled);
+ if (read < blockLength) {
+ // Reached to the end of the data.
+ if (enableEOFException && !lengthUnset) {
+ throw new EOFException();
+ }
+ break;
+ }
+ }
+ position += blockLength;
+ if (!lengthUnset) {
+ bytesLeft -= blockLength;
+ }
+ }
+ }
+
+ private static long getRequestLength(DataSpec dataSpec, Cache cache, String key) {
+ if (dataSpec.length != C.LENGTH_UNSET) {
+ return dataSpec.length;
+ } else {
+ long contentLength = ContentMetadata.getContentLength(cache.getContentMetadata(key));
+ return contentLength == C.LENGTH_UNSET
+ ? C.LENGTH_UNSET
+ : contentLength - dataSpec.absoluteStreamPosition;
+ }
+ }
+
+ /**
+ * Reads and discards all data specified by the {@code dataSpec}.
+ *
+ * @param dataSpec Defines the data to be read. {@code absoluteStreamPosition} and {@code length}
+ * fields are overwritten by the following parameters.
+ * @param absoluteStreamPosition The absolute position of the data to be read.
+ * @param length Length of the data to be read, or {@link C#LENGTH_UNSET} if it is unknown.
+ * @param dataSource The {@link DataSource} to read the data from.
+ * @param buffer The buffer to be used while downloading.
+ * @param priorityTaskManager If not null it's used to check whether it is allowed to proceed with
+ * caching.
+ * @param priority The priority of this task.
+ * @param progressNotifier A notifier through which to report progress updates, or {@code null}.
+ * @param isLastBlock Whether this read block is the last block of the content.
+ * @param isCanceled An optional flag that will interrupt caching if set to true.
+ * @return Number of read bytes, or 0 if no data is available because the end of the opened range
+ * has been reached.
+ */
+ private static long readAndDiscard(
+ DataSpec dataSpec,
+ long absoluteStreamPosition,
+ long length,
+ DataSource dataSource,
+ byte[] buffer,
+ @Nullable PriorityTaskManager priorityTaskManager,
+ int priority,
+ @Nullable ProgressNotifier progressNotifier,
+ boolean isLastBlock,
+ @Nullable AtomicBoolean isCanceled)
+ throws IOException, InterruptedException {
+ long positionOffset = absoluteStreamPosition - dataSpec.absoluteStreamPosition;
+ long initialPositionOffset = positionOffset;
+ long endOffset = length != C.LENGTH_UNSET ? positionOffset + length : C.POSITION_UNSET;
+ while (true) {
+ if (priorityTaskManager != null) {
+ // Wait for any other thread with higher priority to finish its job.
+ priorityTaskManager.proceed(priority);
+ }
+ throwExceptionIfInterruptedOrCancelled(isCanceled);
+ try {
+ long resolvedLength = C.LENGTH_UNSET;
+ boolean isDataSourceOpen = false;
+ if (endOffset != C.POSITION_UNSET) {
+ // If a specific length is given, first try to open the data source for that length to
+ // avoid more data then required to be requested. If the given length exceeds the end of
+ // input we will get a "position out of range" error. In that case try to open the source
+ // again with unset length.
+ try {
+ resolvedLength =
+ dataSource.open(dataSpec.subrange(positionOffset, endOffset - positionOffset));
+ isDataSourceOpen = true;
+ } catch (IOException exception) {
+ if (!isLastBlock || !isCausedByPositionOutOfRange(exception)) {
+ throw exception;
+ }
+ Util.closeQuietly(dataSource);
+ }
+ }
+ if (!isDataSourceOpen) {
+ resolvedLength = dataSource.open(dataSpec.subrange(positionOffset, C.LENGTH_UNSET));
+ }
+ if (isLastBlock && progressNotifier != null && resolvedLength != C.LENGTH_UNSET) {
+ progressNotifier.onRequestLengthResolved(positionOffset + resolvedLength);
+ }
+ while (positionOffset != endOffset) {
+ throwExceptionIfInterruptedOrCancelled(isCanceled);
+ int bytesRead =
+ dataSource.read(
+ buffer,
+ 0,
+ endOffset != C.POSITION_UNSET
+ ? (int) Math.min(buffer.length, endOffset - positionOffset)
+ : buffer.length);
+ if (bytesRead == C.RESULT_END_OF_INPUT) {
+ if (progressNotifier != null) {
+ progressNotifier.onRequestLengthResolved(positionOffset);
+ }
+ break;
+ }
+ positionOffset += bytesRead;
+ if (progressNotifier != null) {
+ progressNotifier.onBytesCached(bytesRead);
+ }
+ }
+ return positionOffset - initialPositionOffset;
+ } catch (PriorityTaskManager.PriorityTooLowException exception) {
+ // catch and try again
+ } finally {
+ Util.closeQuietly(dataSource);
+ }
+ }
+ }
+
+ /**
+ * Removes all of the data specified by the {@code dataSpec}.
+ *
+ * <p>This methods blocks until the operation is complete.
+ *
+ * @param dataSpec Defines the data to be removed.
+ * @param cache A {@link Cache} to store the data.
+ * @param cacheKeyFactory An optional factory for cache keys.
+ */
+ @WorkerThread
+ public static void remove(
+ DataSpec dataSpec, Cache cache, @Nullable CacheKeyFactory cacheKeyFactory) {
+ remove(cache, buildCacheKey(dataSpec, cacheKeyFactory));
+ }
+
+ /**
+ * Removes all of the data specified by the {@code key}.
+ *
+ * <p>This methods blocks until the operation is complete.
+ *
+ * @param cache A {@link Cache} to store the data.
+ * @param key The key whose data should be removed.
+ */
+ @WorkerThread
+ public static void remove(Cache cache, String key) {
+ NavigableSet<CacheSpan> cachedSpans = cache.getCachedSpans(key);
+ for (CacheSpan cachedSpan : cachedSpans) {
+ try {
+ cache.removeSpan(cachedSpan);
+ } catch (Cache.CacheException e) {
+ // Do nothing.
+ }
+ }
+ }
+
+ /* package */ static boolean isCausedByPositionOutOfRange(IOException e) {
+ Throwable cause = e;
+ while (cause != null) {
+ if (cause instanceof DataSourceException) {
+ int reason = ((DataSourceException) cause).reason;
+ if (reason == DataSourceException.POSITION_OUT_OF_RANGE) {
+ return true;
+ }
+ }
+ cause = cause.getCause();
+ }
+ return false;
+ }
+
+ private static String buildCacheKey(
+ DataSpec dataSpec, @Nullable CacheKeyFactory cacheKeyFactory) {
+ return (cacheKeyFactory != null ? cacheKeyFactory : DEFAULT_CACHE_KEY_FACTORY)
+ .buildCacheKey(dataSpec);
+ }
+
+ private static void throwExceptionIfInterruptedOrCancelled(@Nullable AtomicBoolean isCanceled)
+ throws InterruptedException {
+ if (Thread.interrupted() || (isCanceled != null && isCanceled.get())) {
+ throw new InterruptedException();
+ }
+ }
+
+ private CacheUtil() {}
+
+ private static final class ProgressNotifier {
+ /** The listener to notify when progress is made. */
+ private final ProgressListener listener;
+ /** The length of the content being cached in bytes, or {@link C#LENGTH_UNSET} if unknown. */
+ private long requestLength;
+ /** The number of bytes that are cached. */
+ private long bytesCached;
+
+ public ProgressNotifier(ProgressListener listener) {
+ this.listener = listener;
+ }
+
+ public void init(long requestLength, long bytesCached) {
+ this.requestLength = requestLength;
+ this.bytesCached = bytesCached;
+ listener.onProgress(requestLength, bytesCached, /* newBytesCached= */ 0);
+ }
+
+ public void onRequestLengthResolved(long requestLength) {
+ if (this.requestLength == C.LENGTH_UNSET && requestLength != C.LENGTH_UNSET) {
+ this.requestLength = requestLength;
+ listener.onProgress(requestLength, bytesCached, /* newBytesCached= */ 0);
+ }
+ }
+
+ public void onBytesCached(long newBytesCached) {
+ bytesCached += newBytesCached;
+ listener.onProgress(requestLength, bytesCached, newBytesCached);
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CachedContent.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CachedContent.java
new file mode 100644
index 0000000000..660a2a3cb3
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CachedContent.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache;
+
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log;
+import java.io.File;
+import java.util.TreeSet;
+
+/** Defines the cached content for a single stream. */
+/* package */ final class CachedContent {
+
+ private static final String TAG = "CachedContent";
+
+ /** The cache file id that uniquely identifies the original stream. */
+ public final int id;
+ /** The cache key that uniquely identifies the original stream. */
+ public final String key;
+ /** The cached spans of this content. */
+ private final TreeSet<SimpleCacheSpan> cachedSpans;
+ /** Metadata values. */
+ private DefaultContentMetadata metadata;
+ /** Whether the content is locked. */
+ private boolean locked;
+
+ /**
+ * Creates a CachedContent.
+ *
+ * @param id The cache file id.
+ * @param key The cache stream key.
+ */
+ public CachedContent(int id, String key) {
+ this(id, key, DefaultContentMetadata.EMPTY);
+ }
+
+ public CachedContent(int id, String key, DefaultContentMetadata metadata) {
+ this.id = id;
+ this.key = key;
+ this.metadata = metadata;
+ this.cachedSpans = new TreeSet<>();
+ }
+
+ /** Returns the metadata. */
+ public DefaultContentMetadata getMetadata() {
+ return metadata;
+ }
+
+ /**
+ * Applies {@code mutations} to the metadata.
+ *
+ * @return Whether {@code mutations} changed any metadata.
+ */
+ public boolean applyMetadataMutations(ContentMetadataMutations mutations) {
+ DefaultContentMetadata oldMetadata = metadata;
+ metadata = metadata.copyWithMutationsApplied(mutations);
+ return !metadata.equals(oldMetadata);
+ }
+
+ /** Returns whether the content is locked. */
+ public boolean isLocked() {
+ return locked;
+ }
+
+ /** Sets the locked state of the content. */
+ public void setLocked(boolean locked) {
+ this.locked = locked;
+ }
+
+ /** Adds the given {@link SimpleCacheSpan} which contains a part of the content. */
+ public void addSpan(SimpleCacheSpan span) {
+ cachedSpans.add(span);
+ }
+
+ /** Returns a set of all {@link SimpleCacheSpan}s. */
+ public TreeSet<SimpleCacheSpan> getSpans() {
+ return cachedSpans;
+ }
+
+ /**
+ * Returns the span containing the position. If there isn't one, it returns a hole span
+ * which defines the maximum extents of the hole in the cache.
+ */
+ public SimpleCacheSpan getSpan(long position) {
+ SimpleCacheSpan lookupSpan = SimpleCacheSpan.createLookup(key, position);
+ SimpleCacheSpan floorSpan = cachedSpans.floor(lookupSpan);
+ if (floorSpan != null && floorSpan.position + floorSpan.length > position) {
+ return floorSpan;
+ }
+ SimpleCacheSpan ceilSpan = cachedSpans.ceiling(lookupSpan);
+ return ceilSpan == null ? SimpleCacheSpan.createOpenHole(key, position)
+ : SimpleCacheSpan.createClosedHole(key, position, ceilSpan.position - position);
+ }
+
+ /**
+ * Returns the length of the cached data block starting from the {@code position} to the block end
+ * up to {@code length} bytes. If the {@code position} isn't cached then -(the length of the gap
+ * to the next cached data up to {@code length} bytes) is returned.
+ *
+ * @param position The starting position of the data.
+ * @param length The maximum length of the data to be returned.
+ * @return the length of the cached or not cached data block length.
+ */
+ public long getCachedBytesLength(long position, long length) {
+ SimpleCacheSpan span = getSpan(position);
+ if (span.isHoleSpan()) {
+ // We don't have a span covering the start of the queried region.
+ return -Math.min(span.isOpenEnded() ? Long.MAX_VALUE : span.length, length);
+ }
+ long queryEndPosition = position + length;
+ long currentEndPosition = span.position + span.length;
+ if (currentEndPosition < queryEndPosition) {
+ for (SimpleCacheSpan next : cachedSpans.tailSet(span, false)) {
+ if (next.position > currentEndPosition) {
+ // There's a hole in the cache within the queried region.
+ break;
+ }
+ // We expect currentEndPosition to always equal (next.position + next.length), but
+ // perform a max check anyway to guard against the existence of overlapping spans.
+ currentEndPosition = Math.max(currentEndPosition, next.position + next.length);
+ if (currentEndPosition >= queryEndPosition) {
+ // We've found spans covering the queried region.
+ break;
+ }
+ }
+ }
+ return Math.min(currentEndPosition - position, length);
+ }
+
+ /**
+ * Sets the given span's last touch timestamp. The passed span becomes invalid after this call.
+ *
+ * @param cacheSpan Span to be copied and updated.
+ * @param lastTouchTimestamp The new last touch timestamp.
+ * @param updateFile Whether the span file should be renamed to have its timestamp match the new
+ * last touch time.
+ * @return A span with the updated last touch timestamp.
+ */
+ public SimpleCacheSpan setLastTouchTimestamp(
+ SimpleCacheSpan cacheSpan, long lastTouchTimestamp, boolean updateFile) {
+ Assertions.checkState(cachedSpans.remove(cacheSpan));
+ File file = cacheSpan.file;
+ if (updateFile) {
+ File directory = file.getParentFile();
+ long position = cacheSpan.position;
+ File newFile = SimpleCacheSpan.getCacheFile(directory, id, position, lastTouchTimestamp);
+ if (file.renameTo(newFile)) {
+ file = newFile;
+ } else {
+ Log.w(TAG, "Failed to rename " + file + " to " + newFile);
+ }
+ }
+ SimpleCacheSpan newCacheSpan =
+ cacheSpan.copyWithFileAndLastTouchTimestamp(file, lastTouchTimestamp);
+ cachedSpans.add(newCacheSpan);
+ return newCacheSpan;
+ }
+
+ /** Returns whether there are any spans cached. */
+ public boolean isEmpty() {
+ return cachedSpans.isEmpty();
+ }
+
+ /** Removes the given span from cache. */
+ public boolean removeSpan(CacheSpan span) {
+ if (cachedSpans.remove(span)) {
+ span.file.delete();
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = id;
+ result = 31 * result + key.hashCode();
+ result = 31 * result + metadata.hashCode();
+ return result;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ CachedContent that = (CachedContent) o;
+ return id == that.id
+ && key.equals(that.key)
+ && cachedSpans.equals(that.cachedSpans)
+ && metadata.equals(that.metadata);
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java
new file mode 100644
index 0000000000..ac31e492a2
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java
@@ -0,0 +1,956 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache;
+
+import android.annotation.SuppressLint;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.SQLException;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.annotation.WorkerThread;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.database.DatabaseIOException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.database.DatabaseProvider;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.database.VersionTable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.AtomicFile;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ReusableBufferedOutputStream;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+import java.util.Set;
+import javax.crypto.Cipher;
+import javax.crypto.CipherInputStream;
+import javax.crypto.CipherOutputStream;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import org.checkerframework.checker.nullness.compatqual.NullableType;
+
+/** Maintains the index of cached content. */
+/* package */ class CachedContentIndex {
+
+ /* package */ static final String FILE_NAME_ATOMIC = "cached_content_index.exi";
+
+ private static final int INCREMENTAL_METADATA_READ_LENGTH = 10 * 1024 * 1024;
+
+ private final HashMap<String, CachedContent> keyToContent;
+ /**
+ * Maps assigned ids to their corresponding keys. Also contains (id -> null) entries for ids that
+ * have been removed from the index since it was last stored. This prevents reuse of these ids,
+ * which is necessary to avoid clashes that could otherwise occur as a result of the sequence:
+ *
+ * <p>[1] (key1, id1) is removed from the in-memory index ... the index is not stored to disk ...
+ * [2] id1 is reused for a different key2 ... the index is not stored to disk ... [3] A file for
+ * key2 is partially written using a path corresponding to id1 ... the process is killed before
+ * the index is stored to disk ... [4] The index is read from disk, causing the partially written
+ * file to be incorrectly associated to key1
+ *
+ * <p>By avoiding id reuse in step [2], a new id2 will be used instead. Step [4] will then delete
+ * the partially written file because the index does not contain an entry for id2.
+ *
+ * <p>When the index is next stored (id -> null) entries are removed, making the ids eligible for
+ * reuse.
+ */
+ private final SparseArray<@NullableType String> idToKey;
+ /**
+ * Tracks ids for which (id -> null) entries are present in idToKey, so that they can be removed
+ * efficiently when the index is next stored.
+ */
+ private final SparseBooleanArray removedIds;
+ /** Tracks ids that are new since the index was last stored. */
+ private final SparseBooleanArray newIds;
+
+ private Storage storage;
+ @Nullable private Storage previousStorage;
+
+ /** Returns whether the file is an index file. */
+ public static boolean isIndexFile(String fileName) {
+ // Atomic file backups add additional suffixes to the file name.
+ return fileName.startsWith(FILE_NAME_ATOMIC);
+ }
+
+ /**
+ * Deletes index data for the specified cache.
+ *
+ * <p>This method may be slow and shouldn't normally be called on the main thread.
+ *
+ * @param databaseProvider Provides the database in which the index is stored.
+ * @param uid The cache UID.
+ * @throws DatabaseIOException If an error occurs deleting the index data.
+ */
+ @WorkerThread
+ public static void delete(DatabaseProvider databaseProvider, long uid)
+ throws DatabaseIOException {
+ DatabaseStorage.delete(databaseProvider, uid);
+ }
+
+ /**
+ * Creates an instance supporting database storage only.
+ *
+ * @param databaseProvider Provides the database in which the index is stored.
+ */
+ public CachedContentIndex(DatabaseProvider databaseProvider) {
+ this(
+ databaseProvider,
+ /* legacyStorageDir= */ null,
+ /* legacyStorageSecretKey= */ null,
+ /* legacyStorageEncrypt= */ false,
+ /* preferLegacyStorage= */ false);
+ }
+
+ /**
+ * Creates an instance supporting either or both of database and legacy storage.
+ *
+ * @param databaseProvider Provides the database in which the index is stored, or {@code null} to
+ * use only legacy storage.
+ * @param legacyStorageDir The directory in which any legacy storage is stored, or {@code null} to
+ * use only database storage.
+ * @param legacyStorageSecretKey A 16 byte AES key for reading, and optionally writing, legacy
+ * storage.
+ * @param legacyStorageEncrypt Whether to encrypt when writing to legacy storage. Must be false if
+ * {@code legacyStorageSecretKey} is null.
+ * @param preferLegacyStorage Whether to use prefer legacy storage if both storage types are
+ * enabled. This option is only useful for downgrading from database storage back to legacy
+ * storage.
+ */
+ public CachedContentIndex(
+ @Nullable DatabaseProvider databaseProvider,
+ @Nullable File legacyStorageDir,
+ @Nullable byte[] legacyStorageSecretKey,
+ boolean legacyStorageEncrypt,
+ boolean preferLegacyStorage) {
+ Assertions.checkState(databaseProvider != null || legacyStorageDir != null);
+ keyToContent = new HashMap<>();
+ idToKey = new SparseArray<>();
+ removedIds = new SparseBooleanArray();
+ newIds = new SparseBooleanArray();
+ Storage databaseStorage =
+ databaseProvider != null ? new DatabaseStorage(databaseProvider) : null;
+ Storage legacyStorage =
+ legacyStorageDir != null
+ ? new LegacyStorage(
+ new File(legacyStorageDir, FILE_NAME_ATOMIC),
+ legacyStorageSecretKey,
+ legacyStorageEncrypt)
+ : null;
+ if (databaseStorage == null || (legacyStorage != null && preferLegacyStorage)) {
+ storage = legacyStorage;
+ previousStorage = databaseStorage;
+ } else {
+ storage = databaseStorage;
+ previousStorage = legacyStorage;
+ }
+ }
+
+ /**
+ * Loads the index data for the given cache UID.
+ *
+ * <p>This method may be slow and shouldn't normally be called on the main thread.
+ *
+ * @param uid The UID of the cache whose index is to be loaded.
+ * @throws IOException If an error occurs initializing the index data.
+ */
+ @WorkerThread
+ public void initialize(long uid) throws IOException {
+ storage.initialize(uid);
+ if (previousStorage != null) {
+ previousStorage.initialize(uid);
+ }
+ if (!storage.exists() && previousStorage != null && previousStorage.exists()) {
+ // Copy from previous storage into current storage.
+ previousStorage.load(keyToContent, idToKey);
+ storage.storeFully(keyToContent);
+ } else {
+ // Load from the current storage.
+ storage.load(keyToContent, idToKey);
+ }
+ if (previousStorage != null) {
+ previousStorage.delete();
+ previousStorage = null;
+ }
+ }
+
+ /**
+ * Stores the index data to index file if there is a change.
+ *
+ * <p>This method may be slow and shouldn't normally be called on the main thread.
+ *
+ * @throws IOException If an error occurs storing the index data.
+ */
+ @WorkerThread
+ public void store() throws IOException {
+ storage.storeIncremental(keyToContent);
+ // Make ids that were removed since the index was last stored eligible for re-use.
+ int removedIdCount = removedIds.size();
+ for (int i = 0; i < removedIdCount; i++) {
+ idToKey.remove(removedIds.keyAt(i));
+ }
+ removedIds.clear();
+ newIds.clear();
+ }
+
+ /**
+ * Adds the given key to the index if it isn't there already.
+ *
+ * @param key The cache key that uniquely identifies the original stream.
+ * @return A new or existing CachedContent instance with the given key.
+ */
+ public CachedContent getOrAdd(String key) {
+ CachedContent cachedContent = keyToContent.get(key);
+ return cachedContent == null ? addNew(key) : cachedContent;
+ }
+
+ /** Returns a CachedContent instance with the given key or null if there isn't one. */
+ public CachedContent get(String key) {
+ return keyToContent.get(key);
+ }
+
+ /**
+ * Returns a Collection of all CachedContent instances in the index. The collection is backed by
+ * the {@code keyToContent} map, so changes to the map are reflected in the collection, and
+ * vice-versa. If the map is modified while an iteration over the collection is in progress
+ * (except through the iterator's own remove operation), the results of the iteration are
+ * undefined.
+ */
+ public Collection<CachedContent> getAll() {
+ return keyToContent.values();
+ }
+
+ /** Returns an existing or new id assigned to the given key. */
+ public int assignIdForKey(String key) {
+ return getOrAdd(key).id;
+ }
+
+ /** Returns the key which has the given id assigned. */
+ public String getKeyForId(int id) {
+ return idToKey.get(id);
+ }
+
+ /** Removes {@link CachedContent} with the given key from index if it's empty and not locked. */
+ public void maybeRemove(String key) {
+ CachedContent cachedContent = keyToContent.get(key);
+ if (cachedContent != null && cachedContent.isEmpty() && !cachedContent.isLocked()) {
+ keyToContent.remove(key);
+ int id = cachedContent.id;
+ boolean neverStored = newIds.get(id);
+ storage.onRemove(cachedContent, neverStored);
+ if (neverStored) {
+ // The id can be reused immediately.
+ idToKey.remove(id);
+ newIds.delete(id);
+ } else {
+ // Keep an entry in idToKey to stop the id from being reused until the index is next stored,
+ // and add an entry to removedIds to track that it should be removed when this does happen.
+ idToKey.put(id, /* value= */ null);
+ removedIds.put(id, /* value= */ true);
+ }
+ }
+ }
+
+ /** Removes empty and not locked {@link CachedContent} instances from index. */
+ public void removeEmpty() {
+ String[] keys = new String[keyToContent.size()];
+ keyToContent.keySet().toArray(keys);
+ for (String key : keys) {
+ maybeRemove(key);
+ }
+ }
+
+ /**
+ * Returns a set of all content keys. The set is backed by the {@code keyToContent} map, so
+ * changes to the map are reflected in the set, and vice-versa. If the map is modified while an
+ * iteration over the set is in progress (except through the iterator's own remove operation), the
+ * results of the iteration are undefined.
+ */
+ public Set<String> getKeys() {
+ return keyToContent.keySet();
+ }
+
+ /**
+ * Applies {@code mutations} to the {@link ContentMetadata} for the given key. A new {@link
+ * CachedContent} is added if there isn't one already with the given key.
+ */
+ public void applyContentMetadataMutations(String key, ContentMetadataMutations mutations) {
+ CachedContent cachedContent = getOrAdd(key);
+ if (cachedContent.applyMetadataMutations(mutations)) {
+ storage.onUpdate(cachedContent);
+ }
+ }
+
+ /** Returns a {@link ContentMetadata} for the given key. */
+ public ContentMetadata getContentMetadata(String key) {
+ CachedContent cachedContent = get(key);
+ return cachedContent != null ? cachedContent.getMetadata() : DefaultContentMetadata.EMPTY;
+ }
+
+ private CachedContent addNew(String key) {
+ int id = getNewId(idToKey);
+ CachedContent cachedContent = new CachedContent(id, key);
+ keyToContent.put(key, cachedContent);
+ idToKey.put(id, key);
+ newIds.put(id, true);
+ storage.onUpdate(cachedContent);
+ return cachedContent;
+ }
+
+ @SuppressLint("GetInstance") // Suppress warning about specifying "BC" as an explicit provider.
+ private static Cipher getCipher() throws NoSuchPaddingException, NoSuchAlgorithmException {
+ // Workaround for https://issuetracker.google.com/issues/36976726
+ if (Util.SDK_INT == 18) {
+ try {
+ return Cipher.getInstance("AES/CBC/PKCS5PADDING", "BC");
+ } catch (Throwable ignored) {
+ // ignored
+ }
+ }
+ return Cipher.getInstance("AES/CBC/PKCS5PADDING");
+ }
+
+ /**
+ * Returns an id which isn't used in the given array. If the maximum id in the array is smaller
+ * than {@link java.lang.Integer#MAX_VALUE} it just returns the next bigger integer. Otherwise it
+ * returns the smallest unused non-negative integer.
+ */
+ @VisibleForTesting
+ /* package */ static int getNewId(SparseArray<String> idToKey) {
+ int size = idToKey.size();
+ int id = size == 0 ? 0 : (idToKey.keyAt(size - 1) + 1);
+ if (id < 0) { // In case if we pass max int value.
+ // TODO optimization: defragmentation or binary search?
+ for (id = 0; id < size; id++) {
+ if (id != idToKey.keyAt(id)) {
+ break;
+ }
+ }
+ }
+ return id;
+ }
+
+ /**
+ * Deserializes a {@link DefaultContentMetadata} from the given input stream.
+ *
+ * @param input Input stream to read from.
+ * @return a {@link DefaultContentMetadata} instance.
+ * @throws IOException If an error occurs during reading from the input.
+ */
+ private static DefaultContentMetadata readContentMetadata(DataInputStream input)
+ throws IOException {
+ int size = input.readInt();
+ HashMap<String, byte[]> metadata = new HashMap<>();
+ for (int i = 0; i < size; i++) {
+ String name = input.readUTF();
+ int valueSize = input.readInt();
+ if (valueSize < 0) {
+ throw new IOException("Invalid value size: " + valueSize);
+ }
+ // Grow the array incrementally to avoid OutOfMemoryError in the case that a corrupt (and very
+ // large) valueSize was read. In such cases the implementation below is expected to throw
+ // IOException from one of the readFully calls, due to the end of the input being reached.
+ int bytesRead = 0;
+ int nextBytesToRead = Math.min(valueSize, INCREMENTAL_METADATA_READ_LENGTH);
+ byte[] value = Util.EMPTY_BYTE_ARRAY;
+ while (bytesRead != valueSize) {
+ value = Arrays.copyOf(value, bytesRead + nextBytesToRead);
+ input.readFully(value, bytesRead, nextBytesToRead);
+ bytesRead += nextBytesToRead;
+ nextBytesToRead = Math.min(valueSize - bytesRead, INCREMENTAL_METADATA_READ_LENGTH);
+ }
+ metadata.put(name, value);
+ }
+ return new DefaultContentMetadata(metadata);
+ }
+
+ /**
+ * Serializes itself to a {@link DataOutputStream}.
+ *
+ * @param output Output stream to store the values.
+ * @throws IOException If an error occurs writing to the output.
+ */
+ private static void writeContentMetadata(DefaultContentMetadata metadata, DataOutputStream output)
+ throws IOException {
+ Set<Map.Entry<String, byte[]>> entrySet = metadata.entrySet();
+ output.writeInt(entrySet.size());
+ for (Map.Entry<String, byte[]> entry : entrySet) {
+ output.writeUTF(entry.getKey());
+ byte[] value = entry.getValue();
+ output.writeInt(value.length);
+ output.write(value);
+ }
+ }
+
+ /** Interface for the persistent index. */
+ private interface Storage {
+
+ /** Initializes the storage for the given cache UID. */
+ void initialize(long uid);
+
+ /**
+ * Returns whether the persisted index exists.
+ *
+ * @throws IOException If an error occurs determining whether the persisted index exists.
+ */
+ boolean exists() throws IOException;
+
+ /**
+ * Deletes the persisted index.
+ *
+ * @throws IOException If an error occurs deleting the index.
+ */
+ void delete() throws IOException;
+
+ /**
+ * Loads the persisted index into {@code content} and {@code idToKey}, creating it if it doesn't
+ * already exist.
+ *
+ * <p>If the persisted index is in a permanently bad state (i.e. all further attempts to load it
+ * are also expected to fail) then it will be deleted and the call will return successfully. For
+ * transient failures, {@link IOException} will be thrown.
+ *
+ * @param content The key to content map to populate with persisted data.
+ * @param idToKey The id to key map to populate with persisted data.
+ * @throws IOException If an error occurs loading the index.
+ */
+ void load(HashMap<String, CachedContent> content, SparseArray<@NullableType String> idToKey)
+ throws IOException;
+
+ /**
+ * Writes the persisted index, creating it if it doesn't already exist and replacing any
+ * existing content if it does.
+ *
+ * @param content The key to content map to persist.
+ * @throws IOException If an error occurs persisting the index.
+ */
+ void storeFully(HashMap<String, CachedContent> content) throws IOException;
+
+ /**
+ * Ensures incremental changes to the index since the initial {@link #initialize(long)} or last
+ * {@link #storeFully(HashMap)} are persisted. The storage will have been notified of all such
+ * changes via {@link #onUpdate(CachedContent)} and {@link #onRemove(CachedContent, boolean)}.
+ *
+ * @param content The key to content map to persist.
+ * @throws IOException If an error occurs persisting the index.
+ */
+ void storeIncremental(HashMap<String, CachedContent> content) throws IOException;
+
+ /**
+ * Called when a {@link CachedContent} is added or updated.
+ *
+ * @param cachedContent The updated {@link CachedContent}.
+ */
+ void onUpdate(CachedContent cachedContent);
+
+ /**
+ * Called when a {@link CachedContent} is removed.
+ *
+ * @param cachedContent The removed {@link CachedContent}.
+ * @param neverStored True if the {@link CachedContent} was added more recently than when the
+ * index was last stored.
+ */
+ void onRemove(CachedContent cachedContent, boolean neverStored);
+ }
+
+ /** {@link Storage} implementation that uses an {@link AtomicFile}. */
+ private static class LegacyStorage implements Storage {
+
+ private static final int VERSION = 2;
+ private static final int VERSION_METADATA_INTRODUCED = 2;
+ private static final int FLAG_ENCRYPTED_INDEX = 1;
+
+ private final boolean encrypt;
+ @Nullable private final Cipher cipher;
+ @Nullable private final SecretKeySpec secretKeySpec;
+ @Nullable private final Random random;
+ private final AtomicFile atomicFile;
+
+ private boolean changed;
+ @Nullable private ReusableBufferedOutputStream bufferedOutputStream;
+
+ public LegacyStorage(File file, @Nullable byte[] secretKey, boolean encrypt) {
+ Cipher cipher = null;
+ SecretKeySpec secretKeySpec = null;
+ if (secretKey != null) {
+ Assertions.checkArgument(secretKey.length == 16);
+ try {
+ cipher = getCipher();
+ secretKeySpec = new SecretKeySpec(secretKey, "AES");
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+ throw new IllegalStateException(e); // Should never happen.
+ }
+ } else {
+ Assertions.checkArgument(!encrypt);
+ }
+ this.encrypt = encrypt;
+ this.cipher = cipher;
+ this.secretKeySpec = secretKeySpec;
+ random = encrypt ? new Random() : null;
+ atomicFile = new AtomicFile(file);
+ }
+
+ @Override
+ public void initialize(long uid) {
+ // Do nothing. Legacy storage uses a separate file for each cache.
+ }
+
+ @Override
+ public boolean exists() {
+ return atomicFile.exists();
+ }
+
+ @Override
+ public void delete() {
+ atomicFile.delete();
+ }
+
+ @Override
+ public void load(
+ HashMap<String, CachedContent> content, SparseArray<@NullableType String> idToKey) {
+ Assertions.checkState(!changed);
+ if (!readFile(content, idToKey)) {
+ content.clear();
+ idToKey.clear();
+ atomicFile.delete();
+ }
+ }
+
+ @Override
+ public void storeFully(HashMap<String, CachedContent> content) throws IOException {
+ writeFile(content);
+ changed = false;
+ }
+
+ @Override
+ public void storeIncremental(HashMap<String, CachedContent> content) throws IOException {
+ if (!changed) {
+ return;
+ }
+ storeFully(content);
+ }
+
+ @Override
+ public void onUpdate(CachedContent cachedContent) {
+ changed = true;
+ }
+
+ @Override
+ public void onRemove(CachedContent cachedContent, boolean neverStored) {
+ changed = true;
+ }
+
+ private boolean readFile(
+ HashMap<String, CachedContent> content, SparseArray<@NullableType String> idToKey) {
+ if (!atomicFile.exists()) {
+ return true;
+ }
+
+ DataInputStream input = null;
+ try {
+ InputStream inputStream = new BufferedInputStream(atomicFile.openRead());
+ input = new DataInputStream(inputStream);
+ int version = input.readInt();
+ if (version < 0 || version > VERSION) {
+ return false;
+ }
+
+ int flags = input.readInt();
+ if ((flags & FLAG_ENCRYPTED_INDEX) != 0) {
+ if (cipher == null) {
+ return false;
+ }
+ byte[] initializationVector = new byte[16];
+ input.readFully(initializationVector);
+ IvParameterSpec ivParameterSpec = new IvParameterSpec(initializationVector);
+ try {
+ cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);
+ } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
+ throw new IllegalStateException(e);
+ }
+ input = new DataInputStream(new CipherInputStream(inputStream, cipher));
+ } else if (encrypt) {
+ changed = true; // Force index to be rewritten encrypted after read.
+ }
+
+ int count = input.readInt();
+ int hashCode = 0;
+ for (int i = 0; i < count; i++) {
+ CachedContent cachedContent = readCachedContent(version, input);
+ content.put(cachedContent.key, cachedContent);
+ idToKey.put(cachedContent.id, cachedContent.key);
+ hashCode += hashCachedContent(cachedContent, version);
+ }
+ int fileHashCode = input.readInt();
+ boolean isEOF = input.read() == -1;
+ if (fileHashCode != hashCode || !isEOF) {
+ return false;
+ }
+ } catch (IOException e) {
+ return false;
+ } finally {
+ if (input != null) {
+ Util.closeQuietly(input);
+ }
+ }
+ return true;
+ }
+
+ private void writeFile(HashMap<String, CachedContent> content) throws IOException {
+ DataOutputStream output = null;
+ try {
+ OutputStream outputStream = atomicFile.startWrite();
+ if (bufferedOutputStream == null) {
+ bufferedOutputStream = new ReusableBufferedOutputStream(outputStream);
+ } else {
+ bufferedOutputStream.reset(outputStream);
+ }
+ output = new DataOutputStream(bufferedOutputStream);
+ output.writeInt(VERSION);
+
+ int flags = encrypt ? FLAG_ENCRYPTED_INDEX : 0;
+ output.writeInt(flags);
+
+ if (encrypt) {
+ byte[] initializationVector = new byte[16];
+ random.nextBytes(initializationVector);
+ output.write(initializationVector);
+ IvParameterSpec ivParameterSpec = new IvParameterSpec(initializationVector);
+ try {
+ cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec);
+ } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
+ throw new IllegalStateException(e); // Should never happen.
+ }
+ output.flush();
+ output = new DataOutputStream(new CipherOutputStream(bufferedOutputStream, cipher));
+ }
+
+ output.writeInt(content.size());
+ int hashCode = 0;
+ for (CachedContent cachedContent : content.values()) {
+ writeCachedContent(cachedContent, output);
+ hashCode += hashCachedContent(cachedContent, VERSION);
+ }
+ output.writeInt(hashCode);
+ atomicFile.endWrite(output);
+ // Avoid calling close twice. Duplicate CipherOutputStream.close calls did
+ // not used to be no-ops: https://android-review.googlesource.com/#/c/272799/
+ output = null;
+ } finally {
+ Util.closeQuietly(output);
+ }
+ }
+
+ /**
+ * Calculates a hash code for a {@link CachedContent} which is compatible with a particular
+ * index version.
+ */
+ private int hashCachedContent(CachedContent cachedContent, int version) {
+ int result = cachedContent.id;
+ result = 31 * result + cachedContent.key.hashCode();
+ if (version < VERSION_METADATA_INTRODUCED) {
+ long length = ContentMetadata.getContentLength(cachedContent.getMetadata());
+ result = 31 * result + (int) (length ^ (length >>> 32));
+ } else {
+ result = 31 * result + cachedContent.getMetadata().hashCode();
+ }
+ return result;
+ }
+
+ /**
+ * Reads a {@link CachedContent} from a {@link DataInputStream}.
+ *
+ * @param version Version of the encoded data.
+ * @param input Input stream containing values needed to initialize CachedContent instance.
+ * @throws IOException If an error occurs during reading values.
+ */
+ private CachedContent readCachedContent(int version, DataInputStream input) throws IOException {
+ int id = input.readInt();
+ String key = input.readUTF();
+ DefaultContentMetadata metadata;
+ if (version < VERSION_METADATA_INTRODUCED) {
+ long length = input.readLong();
+ ContentMetadataMutations mutations = new ContentMetadataMutations();
+ ContentMetadataMutations.setContentLength(mutations, length);
+ metadata = DefaultContentMetadata.EMPTY.copyWithMutationsApplied(mutations);
+ } else {
+ metadata = readContentMetadata(input);
+ }
+ return new CachedContent(id, key, metadata);
+ }
+
+ /**
+ * Writes a {@link CachedContent} to a {@link DataOutputStream}.
+ *
+ * @param output Output stream to store the values.
+ * @throws IOException If an error occurs during writing values to output.
+ */
+ private void writeCachedContent(CachedContent cachedContent, DataOutputStream output)
+ throws IOException {
+ output.writeInt(cachedContent.id);
+ output.writeUTF(cachedContent.key);
+ writeContentMetadata(cachedContent.getMetadata(), output);
+ }
+ }
+
+ /** {@link Storage} implementation that uses an SQL database. */
+ private static final class DatabaseStorage implements Storage {
+
+ private static final String TABLE_PREFIX = DatabaseProvider.TABLE_PREFIX + "CacheIndex";
+ private static final int TABLE_VERSION = 1;
+
+ private static final String COLUMN_ID = "id";
+ private static final String COLUMN_KEY = "key";
+ private static final String COLUMN_METADATA = "metadata";
+
+ private static final int COLUMN_INDEX_ID = 0;
+ private static final int COLUMN_INDEX_KEY = 1;
+ private static final int COLUMN_INDEX_METADATA = 2;
+
+ private static final String WHERE_ID_EQUALS = COLUMN_ID + " = ?";
+
+ private static final String[] COLUMNS = new String[] {COLUMN_ID, COLUMN_KEY, COLUMN_METADATA};
+ private static final String TABLE_SCHEMA =
+ "("
+ + COLUMN_ID
+ + " INTEGER PRIMARY KEY NOT NULL,"
+ + COLUMN_KEY
+ + " TEXT NOT NULL,"
+ + COLUMN_METADATA
+ + " BLOB NOT NULL)";
+
+ private final DatabaseProvider databaseProvider;
+ private final SparseArray<CachedContent> pendingUpdates;
+
+ private String hexUid;
+ private String tableName;
+
+ public static void delete(DatabaseProvider databaseProvider, long uid)
+ throws DatabaseIOException {
+ delete(databaseProvider, Long.toHexString(uid));
+ }
+
+ public DatabaseStorage(DatabaseProvider databaseProvider) {
+ this.databaseProvider = databaseProvider;
+ pendingUpdates = new SparseArray<>();
+ }
+
+ @Override
+ public void initialize(long uid) {
+ hexUid = Long.toHexString(uid);
+ tableName = getTableName(hexUid);
+ }
+
+ @Override
+ public boolean exists() throws DatabaseIOException {
+ return VersionTable.getVersion(
+ databaseProvider.getReadableDatabase(),
+ VersionTable.FEATURE_CACHE_CONTENT_METADATA,
+ hexUid)
+ != VersionTable.VERSION_UNSET;
+ }
+
+ @Override
+ public void delete() throws DatabaseIOException {
+ delete(databaseProvider, hexUid);
+ }
+
+ @Override
+ public void load(
+ HashMap<String, CachedContent> content, SparseArray<@NullableType String> idToKey)
+ throws IOException {
+ Assertions.checkState(pendingUpdates.size() == 0);
+ try {
+ int version =
+ VersionTable.getVersion(
+ databaseProvider.getReadableDatabase(),
+ VersionTable.FEATURE_CACHE_CONTENT_METADATA,
+ hexUid);
+ if (version != TABLE_VERSION) {
+ SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
+ writableDatabase.beginTransactionNonExclusive();
+ try {
+ initializeTable(writableDatabase);
+ writableDatabase.setTransactionSuccessful();
+ } finally {
+ writableDatabase.endTransaction();
+ }
+ }
+
+ try (Cursor cursor = getCursor()) {
+ while (cursor.moveToNext()) {
+ int id = cursor.getInt(COLUMN_INDEX_ID);
+ String key = cursor.getString(COLUMN_INDEX_KEY);
+ byte[] metadataBytes = cursor.getBlob(COLUMN_INDEX_METADATA);
+
+ ByteArrayInputStream inputStream = new ByteArrayInputStream(metadataBytes);
+ DataInputStream input = new DataInputStream(inputStream);
+ DefaultContentMetadata metadata = readContentMetadata(input);
+
+ CachedContent cachedContent = new CachedContent(id, key, metadata);
+ content.put(cachedContent.key, cachedContent);
+ idToKey.put(cachedContent.id, cachedContent.key);
+ }
+ }
+ } catch (SQLiteException e) {
+ content.clear();
+ idToKey.clear();
+ throw new DatabaseIOException(e);
+ }
+ }
+
+ @Override
+ public void storeFully(HashMap<String, CachedContent> content) throws IOException {
+ try {
+ SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
+ writableDatabase.beginTransactionNonExclusive();
+ try {
+ initializeTable(writableDatabase);
+ for (CachedContent cachedContent : content.values()) {
+ addOrUpdateRow(writableDatabase, cachedContent);
+ }
+ writableDatabase.setTransactionSuccessful();
+ pendingUpdates.clear();
+ } finally {
+ writableDatabase.endTransaction();
+ }
+ } catch (SQLException e) {
+ throw new DatabaseIOException(e);
+ }
+ }
+
+ @Override
+ public void storeIncremental(HashMap<String, CachedContent> content) throws IOException {
+ if (pendingUpdates.size() == 0) {
+ return;
+ }
+ try {
+ SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
+ writableDatabase.beginTransactionNonExclusive();
+ try {
+ for (int i = 0; i < pendingUpdates.size(); i++) {
+ CachedContent cachedContent = pendingUpdates.valueAt(i);
+ if (cachedContent == null) {
+ deleteRow(writableDatabase, pendingUpdates.keyAt(i));
+ } else {
+ addOrUpdateRow(writableDatabase, cachedContent);
+ }
+ }
+ writableDatabase.setTransactionSuccessful();
+ pendingUpdates.clear();
+ } finally {
+ writableDatabase.endTransaction();
+ }
+ } catch (SQLException e) {
+ throw new DatabaseIOException(e);
+ }
+ }
+
+ @Override
+ public void onUpdate(CachedContent cachedContent) {
+ pendingUpdates.put(cachedContent.id, cachedContent);
+ }
+
+ @Override
+ public void onRemove(CachedContent cachedContent, boolean neverStored) {
+ if (neverStored) {
+ pendingUpdates.delete(cachedContent.id);
+ } else {
+ pendingUpdates.put(cachedContent.id, null);
+ }
+ }
+
+ private Cursor getCursor() {
+ return databaseProvider
+ .getReadableDatabase()
+ .query(
+ tableName,
+ COLUMNS,
+ /* selection= */ null,
+ /* selectionArgs= */ null,
+ /* groupBy= */ null,
+ /* having= */ null,
+ /* orderBy= */ null);
+ }
+
+ private void initializeTable(SQLiteDatabase writableDatabase) throws DatabaseIOException {
+ VersionTable.setVersion(
+ writableDatabase, VersionTable.FEATURE_CACHE_CONTENT_METADATA, hexUid, TABLE_VERSION);
+ dropTable(writableDatabase, tableName);
+ writableDatabase.execSQL("CREATE TABLE " + tableName + " " + TABLE_SCHEMA);
+ }
+
+ private void deleteRow(SQLiteDatabase writableDatabase, int key) {
+ writableDatabase.delete(tableName, WHERE_ID_EQUALS, new String[] {Integer.toString(key)});
+ }
+
+ private void addOrUpdateRow(SQLiteDatabase writableDatabase, CachedContent cachedContent)
+ throws IOException {
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ writeContentMetadata(cachedContent.getMetadata(), new DataOutputStream(outputStream));
+ byte[] data = outputStream.toByteArray();
+
+ ContentValues values = new ContentValues();
+ values.put(COLUMN_ID, cachedContent.id);
+ values.put(COLUMN_KEY, cachedContent.key);
+ values.put(COLUMN_METADATA, data);
+ writableDatabase.replaceOrThrow(tableName, /* nullColumnHack= */ null, values);
+ }
+
+ private static void delete(DatabaseProvider databaseProvider, String hexUid)
+ throws DatabaseIOException {
+ try {
+ String tableName = getTableName(hexUid);
+ SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
+ writableDatabase.beginTransactionNonExclusive();
+ try {
+ VersionTable.removeVersion(
+ writableDatabase, VersionTable.FEATURE_CACHE_CONTENT_METADATA, hexUid);
+ dropTable(writableDatabase, tableName);
+ writableDatabase.setTransactionSuccessful();
+ } finally {
+ writableDatabase.endTransaction();
+ }
+ } catch (SQLException e) {
+ throw new DatabaseIOException(e);
+ }
+ }
+
+ private static void dropTable(SQLiteDatabase writableDatabase, String tableName) {
+ writableDatabase.execSQL("DROP TABLE IF EXISTS " + tableName);
+ }
+
+ private static String getTableName(String hexUid) {
+ return TABLE_PREFIX + hexUid;
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CachedRegionTracker.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CachedRegionTracker.java
new file mode 100644
index 0000000000..9b08301ab8
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CachedRegionTracker.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache;
+
+import androidx.annotation.NonNull;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ChunkIndex;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.NavigableSet;
+import java.util.TreeSet;
+
+/**
+ * Utility class for efficiently tracking regions of data that are stored in a {@link Cache}
+ * for a given cache key.
+ */
+public final class CachedRegionTracker implements Cache.Listener {
+
+ private static final String TAG = "CachedRegionTracker";
+
+ public static final int NOT_CACHED = -1;
+ public static final int CACHED_TO_END = -2;
+
+ private final Cache cache;
+ private final String cacheKey;
+ private final ChunkIndex chunkIndex;
+
+ private final TreeSet<Region> regions;
+ private final Region lookupRegion;
+
+ public CachedRegionTracker(Cache cache, String cacheKey, ChunkIndex chunkIndex) {
+ this.cache = cache;
+ this.cacheKey = cacheKey;
+ this.chunkIndex = chunkIndex;
+ this.regions = new TreeSet<>();
+ this.lookupRegion = new Region(0, 0);
+
+ synchronized (this) {
+ NavigableSet<CacheSpan> cacheSpans = cache.addListener(cacheKey, this);
+ // Merge the spans into regions. mergeSpan is more efficient when merging from high to low,
+ // which is why a descending iterator is used here.
+ Iterator<CacheSpan> spanIterator = cacheSpans.descendingIterator();
+ while (spanIterator.hasNext()) {
+ CacheSpan span = spanIterator.next();
+ mergeSpan(span);
+ }
+ }
+ }
+
+ public void release() {
+ cache.removeListener(cacheKey, this);
+ }
+
+ /**
+ * When provided with a byte offset, this method locates the cached region within which the
+ * offset falls, and returns the approximate end position in milliseconds of that region. If the
+ * byte offset does not fall within a cached region then {@link #NOT_CACHED} is returned.
+ * If the cached region extends to the end of the stream, {@link #CACHED_TO_END} is returned.
+ *
+ * @param byteOffset The byte offset in the underlying stream.
+ * @return The end position of the corresponding cache region, {@link #NOT_CACHED}, or
+ * {@link #CACHED_TO_END}.
+ */
+ public synchronized int getRegionEndTimeMs(long byteOffset) {
+ lookupRegion.startOffset = byteOffset;
+ Region floorRegion = regions.floor(lookupRegion);
+ if (floorRegion == null || byteOffset > floorRegion.endOffset
+ || floorRegion.endOffsetIndex == -1) {
+ return NOT_CACHED;
+ }
+ int index = floorRegion.endOffsetIndex;
+ if (index == chunkIndex.length - 1
+ && floorRegion.endOffset == (chunkIndex.offsets[index] + chunkIndex.sizes[index])) {
+ return CACHED_TO_END;
+ }
+ long segmentFractionUs = (chunkIndex.durationsUs[index]
+ * (floorRegion.endOffset - chunkIndex.offsets[index])) / chunkIndex.sizes[index];
+ return (int) ((chunkIndex.timesUs[index] + segmentFractionUs) / 1000);
+ }
+
+ @Override
+ public synchronized void onSpanAdded(Cache cache, CacheSpan span) {
+ mergeSpan(span);
+ }
+
+ @Override
+ public synchronized void onSpanRemoved(Cache cache, CacheSpan span) {
+ Region removedRegion = new Region(span.position, span.position + span.length);
+
+ // Look up a region this span falls into.
+ Region floorRegion = regions.floor(removedRegion);
+ if (floorRegion == null) {
+ Log.e(TAG, "Removed a span we were not aware of");
+ return;
+ }
+
+ // Remove it.
+ regions.remove(floorRegion);
+
+ // Add new floor and ceiling regions, if necessary.
+ if (floorRegion.startOffset < removedRegion.startOffset) {
+ Region newFloorRegion = new Region(floorRegion.startOffset, removedRegion.startOffset);
+
+ int index = Arrays.binarySearch(chunkIndex.offsets, newFloorRegion.endOffset);
+ newFloorRegion.endOffsetIndex = index < 0 ? -index - 2 : index;
+ regions.add(newFloorRegion);
+ }
+
+ if (floorRegion.endOffset > removedRegion.endOffset) {
+ Region newCeilingRegion = new Region(removedRegion.endOffset + 1, floorRegion.endOffset);
+ newCeilingRegion.endOffsetIndex = floorRegion.endOffsetIndex;
+ regions.add(newCeilingRegion);
+ }
+ }
+
+ @Override
+ public void onSpanTouched(Cache cache, CacheSpan oldSpan, CacheSpan newSpan) {
+ // Do nothing.
+ }
+
+ private void mergeSpan(CacheSpan span) {
+ Region newRegion = new Region(span.position, span.position + span.length);
+ Region floorRegion = regions.floor(newRegion);
+ Region ceilingRegion = regions.ceiling(newRegion);
+ boolean floorConnects = regionsConnect(floorRegion, newRegion);
+ boolean ceilingConnects = regionsConnect(newRegion, ceilingRegion);
+
+ if (ceilingConnects) {
+ if (floorConnects) {
+ // Extend floorRegion to cover both newRegion and ceilingRegion.
+ floorRegion.endOffset = ceilingRegion.endOffset;
+ floorRegion.endOffsetIndex = ceilingRegion.endOffsetIndex;
+ } else {
+ // Extend newRegion to cover ceilingRegion. Add it.
+ newRegion.endOffset = ceilingRegion.endOffset;
+ newRegion.endOffsetIndex = ceilingRegion.endOffsetIndex;
+ regions.add(newRegion);
+ }
+ regions.remove(ceilingRegion);
+ } else if (floorConnects) {
+ // Extend floorRegion to the right to cover newRegion.
+ floorRegion.endOffset = newRegion.endOffset;
+ int index = floorRegion.endOffsetIndex;
+ while (index < chunkIndex.length - 1
+ && (chunkIndex.offsets[index + 1] <= floorRegion.endOffset)) {
+ index++;
+ }
+ floorRegion.endOffsetIndex = index;
+ } else {
+ // This is a new region.
+ int index = Arrays.binarySearch(chunkIndex.offsets, newRegion.endOffset);
+ newRegion.endOffsetIndex = index < 0 ? -index - 2 : index;
+ regions.add(newRegion);
+ }
+ }
+
+ private boolean regionsConnect(Region lower, Region upper) {
+ return lower != null && upper != null && lower.endOffset == upper.startOffset;
+ }
+
+ private static class Region implements Comparable<Region> {
+
+ /**
+ * The first byte of the region (inclusive).
+ */
+ public long startOffset;
+ /**
+ * End offset of the region (exclusive).
+ */
+ public long endOffset;
+ /**
+ * The index in chunkIndex that contains the end offset. May be -1 if the end offset comes
+ * before the start of the first media chunk (i.e. if the end offset is within the stream
+ * header).
+ */
+ public int endOffsetIndex;
+
+ public Region(long position, long endOffset) {
+ this.startOffset = position;
+ this.endOffset = endOffset;
+ }
+
+ @Override
+ public int compareTo(@NonNull Region another) {
+ return Util.compareLong(startOffset, another.startOffset);
+ }
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/ContentMetadata.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/ContentMetadata.java
new file mode 100644
index 0000000000..aa34823043
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/ContentMetadata.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache;
+
+import android.net.Uri;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+
+/**
+ * Interface for an immutable snapshot of keyed metadata.
+ */
+public interface ContentMetadata {
+
+ /**
+ * Prefix for custom metadata keys. Applications can use keys starting with this prefix without
+ * any risk of their keys colliding with ones defined by the ExoPlayer library.
+ */
+ @SuppressWarnings("unused")
+ String KEY_CUSTOM_PREFIX = "custom_";
+ /** Key for redirected uri (type: String). */
+ String KEY_REDIRECTED_URI = "exo_redir";
+ /** Key for content length in bytes (type: long). */
+ String KEY_CONTENT_LENGTH = "exo_len";
+
+ /**
+ * Returns a metadata value.
+ *
+ * @param key Key of the metadata to be returned.
+ * @param defaultValue Value to return if the metadata doesn't exist.
+ * @return The metadata value.
+ */
+ @Nullable
+ byte[] get(String key, @Nullable byte[] defaultValue);
+
+ /**
+ * Returns a metadata value.
+ *
+ * @param key Key of the metadata to be returned.
+ * @param defaultValue Value to return if the metadata doesn't exist.
+ * @return The metadata value.
+ */
+ @Nullable
+ String get(String key, @Nullable String defaultValue);
+
+ /**
+ * Returns a metadata value.
+ *
+ * @param key Key of the metadata to be returned.
+ * @param defaultValue Value to return if the metadata doesn't exist.
+ * @return The metadata value.
+ */
+ long get(String key, long defaultValue);
+
+ /** Returns whether the metadata is available. */
+ boolean contains(String key);
+
+ /**
+ * Returns the value stored under {@link #KEY_CONTENT_LENGTH}, or {@link C#LENGTH_UNSET} if not
+ * set.
+ */
+ static long getContentLength(ContentMetadata contentMetadata) {
+ return contentMetadata.get(KEY_CONTENT_LENGTH, C.LENGTH_UNSET);
+ }
+
+ /**
+ * Returns the value stored under {@link #KEY_REDIRECTED_URI} as a {@link Uri}, or {code null} if
+ * not set.
+ */
+ @Nullable
+ static Uri getRedirectedUri(ContentMetadata contentMetadata) {
+ String redirectedUri = contentMetadata.get(KEY_REDIRECTED_URI, (String) null);
+ return redirectedUri == null ? null : Uri.parse(redirectedUri);
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/ContentMetadataMutations.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/ContentMetadataMutations.java
new file mode 100644
index 0000000000..c7a8d9f711
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/ContentMetadataMutations.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache;
+
+import android.net.Uri;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * Defines multiple mutations on metadata value which are applied atomically. This class isn't
+ * thread safe.
+ */
+public class ContentMetadataMutations {
+
+ /**
+ * Adds a mutation to set the {@link ContentMetadata#KEY_CONTENT_LENGTH} value, or to remove any
+ * existing value if {@link C#LENGTH_UNSET} is passed.
+ *
+ * @param mutations The mutations to modify.
+ * @param length The length value, or {@link C#LENGTH_UNSET} to remove any existing entry.
+ * @return The mutations instance, for convenience.
+ */
+ public static ContentMetadataMutations setContentLength(
+ ContentMetadataMutations mutations, long length) {
+ return mutations.set(ContentMetadata.KEY_CONTENT_LENGTH, length);
+ }
+
+ /**
+ * Adds a mutation to set the {@link ContentMetadata#KEY_REDIRECTED_URI} value, or to remove any
+ * existing entry if {@code null} is passed.
+ *
+ * @param mutations The mutations to modify.
+ * @param uri The {@link Uri} value, or {@code null} to remove any existing entry.
+ * @return The mutations instance, for convenience.
+ */
+ public static ContentMetadataMutations setRedirectedUri(
+ ContentMetadataMutations mutations, @Nullable Uri uri) {
+ if (uri == null) {
+ return mutations.remove(ContentMetadata.KEY_REDIRECTED_URI);
+ } else {
+ return mutations.set(ContentMetadata.KEY_REDIRECTED_URI, uri.toString());
+ }
+ }
+
+ private final Map<String, Object> editedValues;
+ private final List<String> removedValues;
+
+ /** Constructs a DefaultMetadataMutations. */
+ public ContentMetadataMutations() {
+ editedValues = new HashMap<>();
+ removedValues = new ArrayList<>();
+ }
+
+ /**
+ * Adds a mutation to set a metadata value. Passing {@code null} as {@code name} or {@code value}
+ * isn't allowed.
+ *
+ * @param name The name of the metadata value.
+ * @param value The value to be set.
+ * @return This instance, for convenience.
+ */
+ public ContentMetadataMutations set(String name, String value) {
+ return checkAndSet(name, value);
+ }
+
+ /**
+ * Adds a mutation to set a metadata value. Passing {@code null} as {@code name} isn't allowed.
+ *
+ * @param name The name of the metadata value.
+ * @param value The value to be set.
+ * @return This instance, for convenience.
+ */
+ public ContentMetadataMutations set(String name, long value) {
+ return checkAndSet(name, value);
+ }
+
+ /**
+ * Adds a mutation to set a metadata value. Passing {@code null} as {@code name} or {@code value}
+ * isn't allowed.
+ *
+ * @param name The name of the metadata value.
+ * @param value The value to be set.
+ * @return This instance, for convenience.
+ */
+ public ContentMetadataMutations set(String name, byte[] value) {
+ return checkAndSet(name, Arrays.copyOf(value, value.length));
+ }
+
+ /**
+ * Adds a mutation to remove a metadata value.
+ *
+ * @param name The name of the metadata value.
+ * @return This instance, for convenience.
+ */
+ public ContentMetadataMutations remove(String name) {
+ removedValues.add(name);
+ editedValues.remove(name);
+ return this;
+ }
+
+ /** Returns a list of names of metadata values to be removed. */
+ public List<String> getRemovedValues() {
+ return Collections.unmodifiableList(new ArrayList<>(removedValues));
+ }
+
+ /** Returns a map of metadata name, value pairs to be set. Values are copied. */
+ public Map<String, Object> getEditedValues() {
+ HashMap<String, Object> hashMap = new HashMap<>(editedValues);
+ for (Entry<String, Object> entry : hashMap.entrySet()) {
+ Object value = entry.getValue();
+ if (value instanceof byte[]) {
+ byte[] bytes = (byte[]) value;
+ entry.setValue(Arrays.copyOf(bytes, bytes.length));
+ }
+ }
+ return Collections.unmodifiableMap(hashMap);
+ }
+
+ private ContentMetadataMutations checkAndSet(String name, Object value) {
+ editedValues.put(Assertions.checkNotNull(name), Assertions.checkNotNull(value));
+ removedValues.remove(name);
+ return this;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadata.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadata.java
new file mode 100644
index 0000000000..2602f834e7
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadata.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache;
+
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+/** Default implementation of {@link ContentMetadata}. Values are stored as byte arrays. */
+public final class DefaultContentMetadata implements ContentMetadata {
+
+ /** An empty DefaultContentMetadata. */
+ public static final DefaultContentMetadata EMPTY =
+ new DefaultContentMetadata(Collections.emptyMap());
+
+ private int hashCode;
+
+ private final Map<String, byte[]> metadata;
+
+ public DefaultContentMetadata() {
+ this(Collections.emptyMap());
+ }
+
+ /** @param metadata The metadata entries in their raw byte array form. */
+ public DefaultContentMetadata(Map<String, byte[]> metadata) {
+ this.metadata = Collections.unmodifiableMap(metadata);
+ }
+
+ /**
+ * Returns a copy {@link DefaultContentMetadata} with {@code mutations} applied. If {@code
+ * mutations} don't change anything, returns this instance.
+ */
+ public DefaultContentMetadata copyWithMutationsApplied(ContentMetadataMutations mutations) {
+ Map<String, byte[]> mutatedMetadata = applyMutations(metadata, mutations);
+ if (isMetadataEqual(metadata, mutatedMetadata)) {
+ return this;
+ }
+ return new DefaultContentMetadata(mutatedMetadata);
+ }
+
+ /** Returns the set of metadata entries in their raw byte array form. */
+ public Set<Entry<String, byte[]>> entrySet() {
+ return metadata.entrySet();
+ }
+
+ @Override
+ @Nullable
+ public final byte[] get(String name, @Nullable byte[] defaultValue) {
+ if (metadata.containsKey(name)) {
+ byte[] bytes = metadata.get(name);
+ return Arrays.copyOf(bytes, bytes.length);
+ } else {
+ return defaultValue;
+ }
+ }
+
+ @Override
+ @Nullable
+ public final String get(String name, @Nullable String defaultValue) {
+ if (metadata.containsKey(name)) {
+ byte[] bytes = metadata.get(name);
+ return new String(bytes, Charset.forName(C.UTF8_NAME));
+ } else {
+ return defaultValue;
+ }
+ }
+
+ @Override
+ public final long get(String name, long defaultValue) {
+ if (metadata.containsKey(name)) {
+ byte[] bytes = metadata.get(name);
+ return ByteBuffer.wrap(bytes).getLong();
+ } else {
+ return defaultValue;
+ }
+ }
+
+ @Override
+ public final boolean contains(String name) {
+ return metadata.containsKey(name);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ return isMetadataEqual(metadata, ((DefaultContentMetadata) o).metadata);
+ }
+
+ @Override
+ public int hashCode() {
+ if (hashCode == 0) {
+ int result = 0;
+ for (Entry<String, byte[]> entry : metadata.entrySet()) {
+ result += entry.getKey().hashCode() ^ Arrays.hashCode(entry.getValue());
+ }
+ hashCode = result;
+ }
+ return hashCode;
+ }
+
+ private static boolean isMetadataEqual(Map<String, byte[]> first, Map<String, byte[]> second) {
+ if (first.size() != second.size()) {
+ return false;
+ }
+ for (Entry<String, byte[]> entry : first.entrySet()) {
+ byte[] value = entry.getValue();
+ byte[] otherValue = second.get(entry.getKey());
+ if (!Arrays.equals(value, otherValue)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static Map<String, byte[]> applyMutations(
+ Map<String, byte[]> otherMetadata, ContentMetadataMutations mutations) {
+ HashMap<String, byte[]> metadata = new HashMap<>(otherMetadata);
+ removeValues(metadata, mutations.getRemovedValues());
+ addValues(metadata, mutations.getEditedValues());
+ return metadata;
+ }
+
+ private static void removeValues(HashMap<String, byte[]> metadata, List<String> names) {
+ for (int i = 0; i < names.size(); i++) {
+ metadata.remove(names.get(i));
+ }
+ }
+
+ private static void addValues(HashMap<String, byte[]> metadata, Map<String, Object> values) {
+ for (String name : values.keySet()) {
+ metadata.put(name, getBytes(values.get(name)));
+ }
+ }
+
+ private static byte[] getBytes(Object value) {
+ if (value instanceof Long) {
+ return ByteBuffer.allocate(8).putLong((Long) value).array();
+ } else if (value instanceof String) {
+ return ((String) value).getBytes(Charset.forName(C.UTF8_NAME));
+ } else if (value instanceof byte[]) {
+ return (byte[]) value;
+ } else {
+ throw new IllegalArgumentException();
+ }
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictor.java
new file mode 100644
index 0000000000..56eff06b25
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictor.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache.Cache.CacheException;
+import java.util.TreeSet;
+
+/** Evicts least recently used cache files first. */
+public final class LeastRecentlyUsedCacheEvictor implements CacheEvictor {
+
+ private final long maxBytes;
+ private final TreeSet<CacheSpan> leastRecentlyUsed;
+
+ private long currentSize;
+
+ public LeastRecentlyUsedCacheEvictor(long maxBytes) {
+ this.maxBytes = maxBytes;
+ this.leastRecentlyUsed = new TreeSet<>(LeastRecentlyUsedCacheEvictor::compare);
+ }
+
+ @Override
+ public boolean requiresCacheSpanTouches() {
+ return true;
+ }
+
+ @Override
+ public void onCacheInitialized() {
+ // Do nothing.
+ }
+
+ @Override
+ public void onStartFile(Cache cache, String key, long position, long length) {
+ if (length != C.LENGTH_UNSET) {
+ evictCache(cache, length);
+ }
+ }
+
+ @Override
+ public void onSpanAdded(Cache cache, CacheSpan span) {
+ leastRecentlyUsed.add(span);
+ currentSize += span.length;
+ evictCache(cache, 0);
+ }
+
+ @Override
+ public void onSpanRemoved(Cache cache, CacheSpan span) {
+ leastRecentlyUsed.remove(span);
+ currentSize -= span.length;
+ }
+
+ @Override
+ public void onSpanTouched(Cache cache, CacheSpan oldSpan, CacheSpan newSpan) {
+ onSpanRemoved(cache, oldSpan);
+ onSpanAdded(cache, newSpan);
+ }
+
+ private void evictCache(Cache cache, long requiredSpace) {
+ while (currentSize + requiredSpace > maxBytes && !leastRecentlyUsed.isEmpty()) {
+ try {
+ cache.removeSpan(leastRecentlyUsed.first());
+ } catch (CacheException e) {
+ // do nothing.
+ }
+ }
+ }
+
+ private static int compare(CacheSpan lhs, CacheSpan rhs) {
+ long lastTouchTimestampDelta = lhs.lastTouchTimestamp - rhs.lastTouchTimestamp;
+ if (lastTouchTimestampDelta == 0) {
+ // Use the standard compareTo method as a tie-break.
+ return lhs.compareTo(rhs);
+ }
+ return lhs.lastTouchTimestamp < rhs.lastTouchTimestamp ? -1 : 1;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/NoOpCacheEvictor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/NoOpCacheEvictor.java
new file mode 100644
index 0000000000..75c1ad0a09
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/NoOpCacheEvictor.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache;
+
+
+/**
+ * Evictor that doesn't ever evict cache files.
+ *
+ * Warning: Using this evictor might have unforeseeable consequences if cache
+ * size is not managed elsewhere.
+ */
+public final class NoOpCacheEvictor implements CacheEvictor {
+
+ @Override
+ public boolean requiresCacheSpanTouches() {
+ return false;
+ }
+
+ @Override
+ public void onCacheInitialized() {
+ // Do nothing.
+ }
+
+ @Override
+ public void onStartFile(Cache cache, String key, long position, long maxLength) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onSpanAdded(Cache cache, CacheSpan span) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onSpanRemoved(Cache cache, CacheSpan span) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onSpanTouched(Cache cache, CacheSpan oldSpan, CacheSpan newSpan) {
+ // Do nothing.
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/SimpleCache.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/SimpleCache.java
new file mode 100644
index 0000000000..9e36c48d88
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/SimpleCache.java
@@ -0,0 +1,812 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache;
+
+import android.os.ConditionVariable;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.database.DatabaseIOException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.database.DatabaseProvider;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.io.File;
+import java.io.IOException;
+import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.NavigableSet;
+import java.util.Random;
+import java.util.Set;
+import java.util.TreeSet;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+
+/**
+ * A {@link Cache} implementation that maintains an in-memory representation.
+ *
+ * <p>Only one instance of SimpleCache is allowed for a given directory at a given time.
+ *
+ * <p>To delete a SimpleCache, use {@link #delete(File, DatabaseProvider)} rather than deleting the
+ * directory and its contents directly. This is necessary to ensure that associated index data is
+ * also removed.
+ */
+public final class SimpleCache implements Cache {
+
+ private static final String TAG = "SimpleCache";
+ /**
+ * Cache files are distributed between a number of subdirectories. This helps to avoid poor
+ * performance in cases where the performance of the underlying file system (e.g. FAT32) scales
+ * badly with the number of files per directory. See
+ * https://github.com/google/ExoPlayer/issues/4253.
+ */
+ private static final int SUBDIRECTORY_COUNT = 10;
+
+ private static final String UID_FILE_SUFFIX = ".uid";
+
+ private static final HashSet<File> lockedCacheDirs = new HashSet<>();
+
+ private final File cacheDir;
+ private final CacheEvictor evictor;
+ private final CachedContentIndex contentIndex;
+ @Nullable private final CacheFileMetadataIndex fileIndex;
+ private final HashMap<String, ArrayList<Listener>> listeners;
+ private final Random random;
+ private final boolean touchCacheSpans;
+
+ private long uid;
+ private long totalSpace;
+ private boolean released;
+ private @MonotonicNonNull CacheException initializationException;
+
+ /**
+ * Returns whether {@code cacheFolder} is locked by a {@link SimpleCache} instance. To unlock the
+ * folder the {@link SimpleCache} instance should be released.
+ */
+ public static synchronized boolean isCacheFolderLocked(File cacheFolder) {
+ return lockedCacheDirs.contains(cacheFolder.getAbsoluteFile());
+ }
+
+ /**
+ * Deletes all content belonging to a cache instance.
+ *
+ * <p>This method may be slow and shouldn't normally be called on the main thread.
+ *
+ * @param cacheDir The cache directory.
+ * @param databaseProvider The database in which index data is stored, or {@code null} if the
+ * cache used a legacy index.
+ */
+ @WorkerThread
+ public static void delete(File cacheDir, @Nullable DatabaseProvider databaseProvider) {
+ if (!cacheDir.exists()) {
+ return;
+ }
+
+ File[] files = cacheDir.listFiles();
+ if (files == null) {
+ cacheDir.delete();
+ return;
+ }
+
+ if (databaseProvider != null) {
+ // Make a best effort to read the cache UID and delete associated index data before deleting
+ // cache directory itself.
+ long uid = loadUid(files);
+ if (uid != UID_UNSET) {
+ try {
+ CacheFileMetadataIndex.delete(databaseProvider, uid);
+ } catch (DatabaseIOException e) {
+ Log.w(TAG, "Failed to delete file metadata: " + uid);
+ }
+ try {
+ CachedContentIndex.delete(databaseProvider, uid);
+ } catch (DatabaseIOException e) {
+ Log.w(TAG, "Failed to delete file metadata: " + uid);
+ }
+ }
+ }
+
+ Util.recursiveDelete(cacheDir);
+ }
+
+ /**
+ * Constructs the cache. The cache will delete any unrecognized files from the directory. Hence
+ * the directory cannot be used to store other files.
+ *
+ * @param cacheDir A dedicated cache directory.
+ * @param evictor The evictor to be used. For download use cases where cache eviction should not
+ * occur, use {@link NoOpCacheEvictor}.
+ * @deprecated Use a constructor that takes a {@link DatabaseProvider} for improved performance.
+ */
+ @Deprecated
+ public SimpleCache(File cacheDir, CacheEvictor evictor) {
+ this(cacheDir, evictor, null, false);
+ }
+
+ /**
+ * Constructs the cache. The cache will delete any unrecognized files from the directory. Hence
+ * the directory cannot be used to store other files.
+ *
+ * @param cacheDir A dedicated cache directory.
+ * @param evictor The evictor to be used. For download use cases where cache eviction should not
+ * occur, use {@link NoOpCacheEvictor}.
+ * @param secretKey If not null, cache keys will be stored encrypted on filesystem using AES/CBC.
+ * The key must be 16 bytes long.
+ * @deprecated Use a constructor that takes a {@link DatabaseProvider} for improved performance.
+ */
+ @Deprecated
+ @SuppressWarnings("deprecation")
+ public SimpleCache(File cacheDir, CacheEvictor evictor, @Nullable byte[] secretKey) {
+ this(cacheDir, evictor, secretKey, secretKey != null);
+ }
+
+ /**
+ * Constructs the cache. The cache will delete any unrecognized files from the directory. Hence
+ * the directory cannot be used to store other files.
+ *
+ * @param cacheDir A dedicated cache directory.
+ * @param evictor The evictor to be used. For download use cases where cache eviction should not
+ * occur, use {@link NoOpCacheEvictor}.
+ * @param secretKey If not null, cache keys will be stored encrypted on filesystem using AES/CBC.
+ * The key must be 16 bytes long.
+ * @param encrypt Whether the index will be encrypted when written. Must be false if {@code
+ * secretKey} is null.
+ * @deprecated Use a constructor that takes a {@link DatabaseProvider} for improved performance.
+ */
+ @Deprecated
+ public SimpleCache(
+ File cacheDir, CacheEvictor evictor, @Nullable byte[] secretKey, boolean encrypt) {
+ this(
+ cacheDir,
+ evictor,
+ /* databaseProvider= */ null,
+ secretKey,
+ encrypt,
+ /* preferLegacyIndex= */ true);
+ }
+
+ /**
+ * Constructs the cache. The cache will delete any unrecognized files from the directory. Hence
+ * the directory cannot be used to store other files.
+ *
+ * @param cacheDir A dedicated cache directory.
+ * @param evictor The evictor to be used. For download use cases where cache eviction should not
+ * occur, use {@link NoOpCacheEvictor}.
+ * @param databaseProvider Provides the database in which the cache index is stored.
+ */
+ public SimpleCache(File cacheDir, CacheEvictor evictor, DatabaseProvider databaseProvider) {
+ this(
+ cacheDir,
+ evictor,
+ databaseProvider,
+ /* legacyIndexSecretKey= */ null,
+ /* legacyIndexEncrypt= */ false,
+ /* preferLegacyIndex= */ false);
+ }
+
+ /**
+ * Constructs the cache. The cache will delete any unrecognized files from the cache directory.
+ * Hence the directory cannot be used to store other files.
+ *
+ * @param cacheDir A dedicated cache directory.
+ * @param evictor The evictor to be used. For download use cases where cache eviction should not
+ * occur, use {@link NoOpCacheEvictor}.
+ * @param databaseProvider Provides the database in which the cache index is stored, or {@code
+ * null} to use a legacy index. Using a database index is highly recommended for performance
+ * reasons.
+ * @param legacyIndexSecretKey A 16 byte AES key for reading, and optionally writing, the legacy
+ * index. Not used by the database index, however should still be provided when using the
+ * database index in cases where upgrading from the legacy index may be necessary.
+ * @param legacyIndexEncrypt Whether to encrypt when writing to the legacy index. Must be {@code
+ * false} if {@code legacyIndexSecretKey} is {@code null}. Not used by the database index.
+ * @param preferLegacyIndex Whether to use the legacy index even if a {@code databaseProvider} is
+ * provided. Should be {@code false} in nearly all cases. Setting this to {@code true} is only
+ * useful for downgrading from the database index back to the legacy index.
+ */
+ public SimpleCache(
+ File cacheDir,
+ CacheEvictor evictor,
+ @Nullable DatabaseProvider databaseProvider,
+ @Nullable byte[] legacyIndexSecretKey,
+ boolean legacyIndexEncrypt,
+ boolean preferLegacyIndex) {
+ this(
+ cacheDir,
+ evictor,
+ new CachedContentIndex(
+ databaseProvider,
+ cacheDir,
+ legacyIndexSecretKey,
+ legacyIndexEncrypt,
+ preferLegacyIndex),
+ databaseProvider != null && !preferLegacyIndex
+ ? new CacheFileMetadataIndex(databaseProvider)
+ : null);
+ }
+
+ /* package */ SimpleCache(
+ File cacheDir,
+ CacheEvictor evictor,
+ CachedContentIndex contentIndex,
+ @Nullable CacheFileMetadataIndex fileIndex) {
+ if (!lockFolder(cacheDir)) {
+ throw new IllegalStateException("Another SimpleCache instance uses the folder: " + cacheDir);
+ }
+
+ this.cacheDir = cacheDir;
+ this.evictor = evictor;
+ this.contentIndex = contentIndex;
+ this.fileIndex = fileIndex;
+ listeners = new HashMap<>();
+ random = new Random();
+ touchCacheSpans = evictor.requiresCacheSpanTouches();
+ uid = UID_UNSET;
+
+ // Start cache initialization.
+ final ConditionVariable conditionVariable = new ConditionVariable();
+ new Thread("SimpleCache.initialize()") {
+ @Override
+ public void run() {
+ synchronized (SimpleCache.this) {
+ conditionVariable.open();
+ initialize();
+ SimpleCache.this.evictor.onCacheInitialized();
+ }
+ }
+ }.start();
+ conditionVariable.block();
+ }
+
+ /**
+ * Checks whether the cache was initialized successfully.
+ *
+ * @throws CacheException If an error occurred during initialization.
+ */
+ public synchronized void checkInitialization() throws CacheException {
+ if (initializationException != null) {
+ throw initializationException;
+ }
+ }
+
+ @Override
+ public synchronized long getUid() {
+ return uid;
+ }
+
+ @Override
+ public synchronized void release() {
+ if (released) {
+ return;
+ }
+ listeners.clear();
+ removeStaleSpans();
+ try {
+ contentIndex.store();
+ } catch (IOException e) {
+ Log.e(TAG, "Storing index file failed", e);
+ } finally {
+ unlockFolder(cacheDir);
+ released = true;
+ }
+ }
+
+ @Override
+ public synchronized NavigableSet<CacheSpan> addListener(String key, Listener listener) {
+ Assertions.checkState(!released);
+ ArrayList<Listener> listenersForKey = listeners.get(key);
+ if (listenersForKey == null) {
+ listenersForKey = new ArrayList<>();
+ listeners.put(key, listenersForKey);
+ }
+ listenersForKey.add(listener);
+ return getCachedSpans(key);
+ }
+
+ @Override
+ public synchronized void removeListener(String key, Listener listener) {
+ if (released) {
+ return;
+ }
+ ArrayList<Listener> listenersForKey = listeners.get(key);
+ if (listenersForKey != null) {
+ listenersForKey.remove(listener);
+ if (listenersForKey.isEmpty()) {
+ listeners.remove(key);
+ }
+ }
+ }
+
+ @NonNull
+ @Override
+ public synchronized NavigableSet<CacheSpan> getCachedSpans(String key) {
+ Assertions.checkState(!released);
+ CachedContent cachedContent = contentIndex.get(key);
+ return cachedContent == null || cachedContent.isEmpty()
+ ? new TreeSet<>()
+ : new TreeSet<CacheSpan>(cachedContent.getSpans());
+ }
+
+ @Override
+ public synchronized Set<String> getKeys() {
+ Assertions.checkState(!released);
+ return new HashSet<>(contentIndex.getKeys());
+ }
+
+ @Override
+ public synchronized long getCacheSpace() {
+ Assertions.checkState(!released);
+ return totalSpace;
+ }
+
+ @Override
+ public synchronized CacheSpan startReadWrite(String key, long position)
+ throws InterruptedException, CacheException {
+ Assertions.checkState(!released);
+ checkInitialization();
+
+ while (true) {
+ CacheSpan span = startReadWriteNonBlocking(key, position);
+ if (span != null) {
+ return span;
+ } else {
+ // Lock not available. We'll be woken up when a span is added, or when a locked span is
+ // released. We'll be able to make progress when either:
+ // 1. A span is added for the requested key that covers the requested position, in which
+ // case a read can be started.
+ // 2. The lock for the requested key is released, in which case a write can be started.
+ wait();
+ }
+ }
+ }
+
+ @Override
+ @Nullable
+ public synchronized CacheSpan startReadWriteNonBlocking(String key, long position)
+ throws CacheException {
+ Assertions.checkState(!released);
+ checkInitialization();
+
+ SimpleCacheSpan span = getSpan(key, position);
+
+ if (span.isCached) {
+ // Read case.
+ return touchSpan(key, span);
+ }
+
+ CachedContent cachedContent = contentIndex.getOrAdd(key);
+ if (!cachedContent.isLocked()) {
+ // Write case.
+ cachedContent.setLocked(true);
+ return span;
+ }
+
+ // Lock not available.
+ return null;
+ }
+
+ @Override
+ public synchronized File startFile(String key, long position, long length) throws CacheException {
+ Assertions.checkState(!released);
+ checkInitialization();
+
+ CachedContent cachedContent = contentIndex.get(key);
+ Assertions.checkNotNull(cachedContent);
+ Assertions.checkState(cachedContent.isLocked());
+ if (!cacheDir.exists()) {
+ // For some reason the cache directory doesn't exist. Make a best effort to create it.
+ cacheDir.mkdirs();
+ removeStaleSpans();
+ }
+ evictor.onStartFile(this, key, position, length);
+ // Randomly distribute files into subdirectories with a uniform distribution.
+ File fileDir = new File(cacheDir, Integer.toString(random.nextInt(SUBDIRECTORY_COUNT)));
+ if (!fileDir.exists()) {
+ fileDir.mkdir();
+ }
+ long lastTouchTimestamp = System.currentTimeMillis();
+ return SimpleCacheSpan.getCacheFile(fileDir, cachedContent.id, position, lastTouchTimestamp);
+ }
+
+ @Override
+ public synchronized void commitFile(File file, long length) throws CacheException {
+ Assertions.checkState(!released);
+ if (!file.exists()) {
+ return;
+ }
+ if (length == 0) {
+ file.delete();
+ return;
+ }
+
+ SimpleCacheSpan span =
+ Assertions.checkNotNull(SimpleCacheSpan.createCacheEntry(file, length, contentIndex));
+ CachedContent cachedContent = Assertions.checkNotNull(contentIndex.get(span.key));
+ Assertions.checkState(cachedContent.isLocked());
+
+ // Check if the span conflicts with the set content length
+ long contentLength = ContentMetadata.getContentLength(cachedContent.getMetadata());
+ if (contentLength != C.LENGTH_UNSET) {
+ Assertions.checkState((span.position + span.length) <= contentLength);
+ }
+
+ if (fileIndex != null) {
+ String fileName = file.getName();
+ try {
+ fileIndex.set(fileName, span.length, span.lastTouchTimestamp);
+ } catch (IOException e) {
+ throw new CacheException(e);
+ }
+ }
+ addSpan(span);
+ try {
+ contentIndex.store();
+ } catch (IOException e) {
+ throw new CacheException(e);
+ }
+ notifyAll();
+ }
+
+ @Override
+ public synchronized void releaseHoleSpan(CacheSpan holeSpan) {
+ Assertions.checkState(!released);
+ CachedContent cachedContent = contentIndex.get(holeSpan.key);
+ Assertions.checkNotNull(cachedContent);
+ Assertions.checkState(cachedContent.isLocked());
+ cachedContent.setLocked(false);
+ contentIndex.maybeRemove(cachedContent.key);
+ notifyAll();
+ }
+
+ @Override
+ public synchronized void removeSpan(CacheSpan span) {
+ Assertions.checkState(!released);
+ removeSpanInternal(span);
+ }
+
+ @Override
+ public synchronized boolean isCached(String key, long position, long length) {
+ Assertions.checkState(!released);
+ CachedContent cachedContent = contentIndex.get(key);
+ return cachedContent != null && cachedContent.getCachedBytesLength(position, length) >= length;
+ }
+
+ @Override
+ public synchronized long getCachedLength(String key, long position, long length) {
+ Assertions.checkState(!released);
+ CachedContent cachedContent = contentIndex.get(key);
+ return cachedContent != null ? cachedContent.getCachedBytesLength(position, length) : -length;
+ }
+
+ @Override
+ public synchronized void applyContentMetadataMutations(
+ String key, ContentMetadataMutations mutations) throws CacheException {
+ Assertions.checkState(!released);
+ checkInitialization();
+
+ contentIndex.applyContentMetadataMutations(key, mutations);
+ try {
+ contentIndex.store();
+ } catch (IOException e) {
+ throw new CacheException(e);
+ }
+ }
+
+ @Override
+ public synchronized ContentMetadata getContentMetadata(String key) {
+ Assertions.checkState(!released);
+ return contentIndex.getContentMetadata(key);
+ }
+
+ /** Ensures that the cache's in-memory representation has been initialized. */
+ private void initialize() {
+ if (!cacheDir.exists()) {
+ if (!cacheDir.mkdirs()) {
+ String message = "Failed to create cache directory: " + cacheDir;
+ Log.e(TAG, message);
+ initializationException = new CacheException(message);
+ return;
+ }
+ }
+
+ File[] files = cacheDir.listFiles();
+ if (files == null) {
+ String message = "Failed to list cache directory files: " + cacheDir;
+ Log.e(TAG, message);
+ initializationException = new CacheException(message);
+ return;
+ }
+
+ uid = loadUid(files);
+ if (uid == UID_UNSET) {
+ try {
+ uid = createUid(cacheDir);
+ } catch (IOException e) {
+ String message = "Failed to create cache UID: " + cacheDir;
+ Log.e(TAG, message, e);
+ initializationException = new CacheException(message, e);
+ return;
+ }
+ }
+
+ try {
+ contentIndex.initialize(uid);
+ if (fileIndex != null) {
+ fileIndex.initialize(uid);
+ Map<String, CacheFileMetadata> fileMetadata = fileIndex.getAll();
+ loadDirectory(cacheDir, /* isRoot= */ true, files, fileMetadata);
+ fileIndex.removeAll(fileMetadata.keySet());
+ } else {
+ loadDirectory(cacheDir, /* isRoot= */ true, files, /* fileMetadata= */ null);
+ }
+ } catch (IOException e) {
+ String message = "Failed to initialize cache indices: " + cacheDir;
+ Log.e(TAG, message, e);
+ initializationException = new CacheException(message, e);
+ return;
+ }
+
+ contentIndex.removeEmpty();
+ try {
+ contentIndex.store();
+ } catch (IOException e) {
+ Log.e(TAG, "Storing index file failed", e);
+ }
+ }
+
+ /**
+ * Loads a cache directory. If the root directory is passed, also loads any subdirectories.
+ *
+ * @param directory The directory.
+ * @param isRoot Whether the directory is the root directory.
+ * @param files The files belonging to the directory.
+ * @param fileMetadata A mutable map containing cache file metadata, keyed by file name. The map
+ * is modified by removing entries for all loaded files. When the method call returns, the map
+ * will contain only metadata that was unused. May be null if no file metadata is available.
+ */
+ private void loadDirectory(
+ File directory,
+ boolean isRoot,
+ @Nullable File[] files,
+ @Nullable Map<String, CacheFileMetadata> fileMetadata) {
+ if (files == null || files.length == 0) {
+ // Either (a) directory isn't really a directory (b) it's empty, or (c) listing files failed.
+ if (!isRoot) {
+ // For (a) and (b) deletion is the desired result. For (c) it will be a no-op if the
+ // directory is non-empty, so there's no harm in trying.
+ directory.delete();
+ }
+ return;
+ }
+ for (File file : files) {
+ String fileName = file.getName();
+ if (isRoot && fileName.indexOf('.') == -1) {
+ loadDirectory(file, /* isRoot= */ false, file.listFiles(), fileMetadata);
+ } else {
+ if (isRoot
+ && (CachedContentIndex.isIndexFile(fileName) || fileName.endsWith(UID_FILE_SUFFIX))) {
+ // Skip expected UID and index files in the root directory.
+ continue;
+ }
+ long length = C.LENGTH_UNSET;
+ long lastTouchTimestamp = C.TIME_UNSET;
+ CacheFileMetadata metadata = fileMetadata != null ? fileMetadata.remove(fileName) : null;
+ if (metadata != null) {
+ length = metadata.length;
+ lastTouchTimestamp = metadata.lastTouchTimestamp;
+ }
+ SimpleCacheSpan span =
+ SimpleCacheSpan.createCacheEntry(file, length, lastTouchTimestamp, contentIndex);
+ if (span != null) {
+ addSpan(span);
+ } else {
+ file.delete();
+ }
+ }
+ }
+ }
+
+ /**
+ * Touches a cache span, returning the updated result. If the evictor does not require cache spans
+ * to be touched, then this method does nothing and the span is returned without modification.
+ *
+ * @param key The key of the span being touched.
+ * @param span The span being touched.
+ * @return The updated span.
+ */
+ private SimpleCacheSpan touchSpan(String key, SimpleCacheSpan span) {
+ if (!touchCacheSpans) {
+ return span;
+ }
+ String fileName = Assertions.checkNotNull(span.file).getName();
+ long length = span.length;
+ long lastTouchTimestamp = System.currentTimeMillis();
+ boolean updateFile = false;
+ if (fileIndex != null) {
+ try {
+ fileIndex.set(fileName, length, lastTouchTimestamp);
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to update index with new touch timestamp.");
+ }
+ } else {
+ // Updating the file itself to incorporate the new last touch timestamp is much slower than
+ // updating the file index. Hence we only update the file if we don't have a file index.
+ updateFile = true;
+ }
+ SimpleCacheSpan newSpan =
+ contentIndex.get(key).setLastTouchTimestamp(span, lastTouchTimestamp, updateFile);
+ notifySpanTouched(span, newSpan);
+ return newSpan;
+ }
+
+ /**
+ * Returns the cache span corresponding to the provided lookup span.
+ *
+ * <p>If the lookup position is contained by an existing entry in the cache, then the returned
+ * span defines the file in which the data is stored. If the lookup position is not contained by
+ * an existing entry, then the returned span defines the maximum extents of the hole in the cache.
+ *
+ * @param key The key of the span being requested.
+ * @param position The position of the span being requested.
+ * @return The corresponding cache {@link SimpleCacheSpan}.
+ */
+ private SimpleCacheSpan getSpan(String key, long position) {
+ CachedContent cachedContent = contentIndex.get(key);
+ if (cachedContent == null) {
+ return SimpleCacheSpan.createOpenHole(key, position);
+ }
+ while (true) {
+ SimpleCacheSpan span = cachedContent.getSpan(position);
+ if (span.isCached && span.file.length() != span.length) {
+ // The file has been modified or deleted underneath us. It's likely that other files will
+ // have been modified too, so scan the whole in-memory representation.
+ removeStaleSpans();
+ continue;
+ }
+ return span;
+ }
+ }
+
+ /**
+ * Adds a cached span to the in-memory representation.
+ *
+ * @param span The span to be added.
+ */
+ private void addSpan(SimpleCacheSpan span) {
+ contentIndex.getOrAdd(span.key).addSpan(span);
+ totalSpace += span.length;
+ notifySpanAdded(span);
+ }
+
+ private void removeSpanInternal(CacheSpan span) {
+ CachedContent cachedContent = contentIndex.get(span.key);
+ if (cachedContent == null || !cachedContent.removeSpan(span)) {
+ return;
+ }
+ totalSpace -= span.length;
+ if (fileIndex != null) {
+ String fileName = span.file.getName();
+ try {
+ fileIndex.remove(fileName);
+ } catch (IOException e) {
+ // This will leave a stale entry in the file index. It will be removed next time the cache
+ // is initialized.
+ Log.w(TAG, "Failed to remove file index entry for: " + fileName);
+ }
+ }
+ contentIndex.maybeRemove(cachedContent.key);
+ notifySpanRemoved(span);
+ }
+
+ /**
+ * Scans all of the cached spans in the in-memory representation, removing any for which the
+ * underlying file lengths no longer match.
+ */
+ private void removeStaleSpans() {
+ ArrayList<CacheSpan> spansToBeRemoved = new ArrayList<>();
+ for (CachedContent cachedContent : contentIndex.getAll()) {
+ for (CacheSpan span : cachedContent.getSpans()) {
+ if (span.file.length() != span.length) {
+ spansToBeRemoved.add(span);
+ }
+ }
+ }
+ for (int i = 0; i < spansToBeRemoved.size(); i++) {
+ removeSpanInternal(spansToBeRemoved.get(i));
+ }
+ }
+
+ private void notifySpanRemoved(CacheSpan span) {
+ ArrayList<Listener> keyListeners = listeners.get(span.key);
+ if (keyListeners != null) {
+ for (int i = keyListeners.size() - 1; i >= 0; i--) {
+ keyListeners.get(i).onSpanRemoved(this, span);
+ }
+ }
+ evictor.onSpanRemoved(this, span);
+ }
+
+ private void notifySpanAdded(SimpleCacheSpan span) {
+ ArrayList<Listener> keyListeners = listeners.get(span.key);
+ if (keyListeners != null) {
+ for (int i = keyListeners.size() - 1; i >= 0; i--) {
+ keyListeners.get(i).onSpanAdded(this, span);
+ }
+ }
+ evictor.onSpanAdded(this, span);
+ }
+
+ private void notifySpanTouched(SimpleCacheSpan oldSpan, CacheSpan newSpan) {
+ ArrayList<Listener> keyListeners = listeners.get(oldSpan.key);
+ if (keyListeners != null) {
+ for (int i = keyListeners.size() - 1; i >= 0; i--) {
+ keyListeners.get(i).onSpanTouched(this, oldSpan, newSpan);
+ }
+ }
+ evictor.onSpanTouched(this, oldSpan, newSpan);
+ }
+
+ /**
+ * Loads the cache UID from the files belonging to the root directory.
+ *
+ * @param files The files belonging to the root directory.
+ * @return The loaded UID, or {@link #UID_UNSET} if a UID has not yet been created.
+ */
+ private static long loadUid(File[] files) {
+ for (File file : files) {
+ String fileName = file.getName();
+ if (fileName.endsWith(UID_FILE_SUFFIX)) {
+ try {
+ return parseUid(fileName);
+ } catch (NumberFormatException e) {
+ // This should never happen, but if it does delete the malformed UID file and continue.
+ Log.e(TAG, "Malformed UID file: " + file);
+ file.delete();
+ }
+ }
+ }
+ return UID_UNSET;
+ }
+
+ @SuppressWarnings("TrulyRandom")
+ private static long createUid(File directory) throws IOException {
+ // Generate a non-negative UID.
+ long uid = new SecureRandom().nextLong();
+ uid = uid == Long.MIN_VALUE ? 0 : Math.abs(uid);
+ // Persist it as a file.
+ String hexUid = Long.toString(uid, /* radix= */ 16);
+ File hexUidFile = new File(directory, hexUid + UID_FILE_SUFFIX);
+ if (!hexUidFile.createNewFile()) {
+ // False means that the file already exists, so this should never happen.
+ throw new IOException("Failed to create UID file: " + hexUidFile);
+ }
+ return uid;
+ }
+
+ private static long parseUid(String fileName) {
+ return Long.parseLong(fileName.substring(0, fileName.indexOf('.')), /* radix= */ 16);
+ }
+
+ private static synchronized boolean lockFolder(File cacheDir) {
+ return lockedCacheDirs.add(cacheDir.getAbsoluteFile());
+ }
+
+ private static synchronized void unlockFolder(File cacheDir) {
+ lockedCacheDirs.remove(cacheDir.getAbsoluteFile());
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java
new file mode 100644
index 0000000000..6e7bec301f
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache;
+
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.io.File;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/** This class stores span metadata in filename. */
+/* package */ final class SimpleCacheSpan extends CacheSpan {
+
+ /* package */ static final String COMMON_SUFFIX = ".exo";
+
+ private static final String SUFFIX = ".v3" + COMMON_SUFFIX;
+ private static final Pattern CACHE_FILE_PATTERN_V1 = Pattern.compile(
+ "^(.+)\\.(\\d+)\\.(\\d+)\\.v1\\.exo$", Pattern.DOTALL);
+ private static final Pattern CACHE_FILE_PATTERN_V2 = Pattern.compile(
+ "^(.+)\\.(\\d+)\\.(\\d+)\\.v2\\.exo$", Pattern.DOTALL);
+ private static final Pattern CACHE_FILE_PATTERN_V3 = Pattern.compile(
+ "^(\\d+)\\.(\\d+)\\.(\\d+)\\.v3\\.exo$", Pattern.DOTALL);
+
+ /**
+ * Returns a new {@link File} instance from {@code cacheDir}, {@code id}, {@code position}, {@code
+ * timestamp}.
+ *
+ * @param cacheDir The parent abstract pathname.
+ * @param id The cache file id.
+ * @param position The position of the stored data in the original stream.
+ * @param timestamp The file timestamp.
+ * @return The cache file.
+ */
+ public static File getCacheFile(File cacheDir, int id, long position, long timestamp) {
+ return new File(cacheDir, id + "." + position + "." + timestamp + SUFFIX);
+ }
+
+ /**
+ * Creates a lookup span.
+ *
+ * @param key The cache key.
+ * @param position The position of the {@link CacheSpan} in the original stream.
+ * @return The span.
+ */
+ public static SimpleCacheSpan createLookup(String key, long position) {
+ return new SimpleCacheSpan(key, position, C.LENGTH_UNSET, C.TIME_UNSET, null);
+ }
+
+ /**
+ * Creates an open hole span.
+ *
+ * @param key The cache key.
+ * @param position The position of the {@link CacheSpan} in the original stream.
+ * @return The span.
+ */
+ public static SimpleCacheSpan createOpenHole(String key, long position) {
+ return new SimpleCacheSpan(key, position, C.LENGTH_UNSET, C.TIME_UNSET, null);
+ }
+
+ /**
+ * Creates a closed hole span.
+ *
+ * @param key The cache key.
+ * @param position The position of the {@link CacheSpan} in the original stream.
+ * @param length The length of the {@link CacheSpan}.
+ * @return The span.
+ */
+ public static SimpleCacheSpan createClosedHole(String key, long position, long length) {
+ return new SimpleCacheSpan(key, position, length, C.TIME_UNSET, null);
+ }
+
+ /**
+ * Creates a cache span from an underlying cache file. Upgrades the file if necessary.
+ *
+ * @param file The cache file.
+ * @param length The length of the cache file in bytes, or {@link C#LENGTH_UNSET} to query the
+ * underlying file system. Querying the underlying file system can be expensive, so callers
+ * that already know the length of the file should pass it explicitly.
+ * @return The span, or null if the file name is not correctly formatted, or if the id is not
+ * present in the content index, or if the length is 0.
+ */
+ @Nullable
+ public static SimpleCacheSpan createCacheEntry(File file, long length, CachedContentIndex index) {
+ return createCacheEntry(file, length, /* lastTouchTimestamp= */ C.TIME_UNSET, index);
+ }
+
+ /**
+ * Creates a cache span from an underlying cache file. Upgrades the file if necessary.
+ *
+ * @param file The cache file.
+ * @param length The length of the cache file in bytes, or {@link C#LENGTH_UNSET} to query the
+ * underlying file system. Querying the underlying file system can be expensive, so callers
+ * that already know the length of the file should pass it explicitly.
+ * @param lastTouchTimestamp The last touch timestamp, or {@link C#TIME_UNSET} to use the file
+ * timestamp.
+ * @return The span, or null if the file name is not correctly formatted, or if the id is not
+ * present in the content index, or if the length is 0.
+ */
+ @Nullable
+ public static SimpleCacheSpan createCacheEntry(
+ File file, long length, long lastTouchTimestamp, CachedContentIndex index) {
+ String name = file.getName();
+ if (!name.endsWith(SUFFIX)) {
+ @Nullable File upgradedFile = upgradeFile(file, index);
+ if (upgradedFile == null) {
+ return null;
+ }
+ file = upgradedFile;
+ name = file.getName();
+ }
+
+ Matcher matcher = CACHE_FILE_PATTERN_V3.matcher(name);
+ if (!matcher.matches()) {
+ return null;
+ }
+
+ int id = Integer.parseInt(matcher.group(1));
+ String key = index.getKeyForId(id);
+ if (key == null) {
+ return null;
+ }
+
+ if (length == C.LENGTH_UNSET) {
+ length = file.length();
+ }
+ if (length == 0) {
+ return null;
+ }
+
+ long position = Long.parseLong(matcher.group(2));
+ if (lastTouchTimestamp == C.TIME_UNSET) {
+ lastTouchTimestamp = Long.parseLong(matcher.group(3));
+ }
+ return new SimpleCacheSpan(key, position, length, lastTouchTimestamp, file);
+ }
+
+ /**
+ * Upgrades the cache file if it is created by an earlier version of {@link SimpleCache}.
+ *
+ * @param file The cache file.
+ * @param index Cached content index.
+ * @return Upgraded cache file or {@code null} if the file name is not correctly formatted or the
+ * file can not be renamed.
+ */
+ @Nullable
+ private static File upgradeFile(File file, CachedContentIndex index) {
+ String key;
+ String filename = file.getName();
+ Matcher matcher = CACHE_FILE_PATTERN_V2.matcher(filename);
+ if (matcher.matches()) {
+ key = Util.unescapeFileName(matcher.group(1));
+ if (key == null) {
+ return null;
+ }
+ } else {
+ matcher = CACHE_FILE_PATTERN_V1.matcher(filename);
+ if (!matcher.matches()) {
+ return null;
+ }
+ key = matcher.group(1); // Keys were not escaped in version 1.
+ }
+
+ File newCacheFile =
+ getCacheFile(
+ Assertions.checkStateNotNull(file.getParentFile()),
+ index.assignIdForKey(key),
+ Long.parseLong(matcher.group(2)),
+ Long.parseLong(matcher.group(3)));
+ if (!file.renameTo(newCacheFile)) {
+ return null;
+ }
+ return newCacheFile;
+ }
+
+ /**
+ * @param key The cache key.
+ * @param position The position of the {@link CacheSpan} in the original stream.
+ * @param length The length of the {@link CacheSpan}, or {@link C#LENGTH_UNSET} if this is an
+ * open-ended hole.
+ * @param lastTouchTimestamp The last touch timestamp, or {@link C#TIME_UNSET} if {@link
+ * #isCached} is false.
+ * @param file The file corresponding to this {@link CacheSpan}, or null if it's a hole.
+ */
+ private SimpleCacheSpan(
+ String key, long position, long length, long lastTouchTimestamp, @Nullable File file) {
+ super(key, position, length, lastTouchTimestamp, file);
+ }
+
+ /**
+ * Returns a copy of this CacheSpan with a new file and last touch timestamp.
+ *
+ * @param file The new file.
+ * @param lastTouchTimestamp The new last touch time.
+ * @return A copy with the new file and last touch timestamp.
+ * @throws IllegalStateException If called on a non-cached span (i.e. {@link #isCached} is false).
+ */
+ public SimpleCacheSpan copyWithFileAndLastTouchTimestamp(File file, long lastTouchTimestamp) {
+ Assertions.checkState(isCached);
+ return new SimpleCacheSpan(key, position, length, lastTouchTimestamp, file);
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSink.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSink.java
new file mode 100644
index 0000000000..4c6be98157
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSink.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.crypto;
+
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull;
+
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSink;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec;
+import java.io.IOException;
+import javax.crypto.Cipher;
+
+/**
+ * A wrapping {@link DataSink} that encrypts the data being consumed.
+ */
+public final class AesCipherDataSink implements DataSink {
+
+ private final DataSink wrappedDataSink;
+ private final byte[] secretKey;
+ @Nullable private final byte[] scratch;
+
+ @Nullable private AesFlushingCipher cipher;
+
+ /**
+ * Create an instance whose {@code write} methods have the side effect of overwriting the input
+ * {@code data}. Use this constructor for maximum efficiency in the case that there is no
+ * requirement for the input data arrays to remain unchanged.
+ *
+ * @param secretKey The key data.
+ * @param wrappedDataSink The wrapped {@link DataSink}.
+ */
+ public AesCipherDataSink(byte[] secretKey, DataSink wrappedDataSink) {
+ this(secretKey, wrappedDataSink, null);
+ }
+
+ /**
+ * Create an instance whose {@code write} methods are free of side effects. Use this constructor
+ * when the input data arrays are required to remain unchanged.
+ *
+ * @param secretKey The key data.
+ * @param wrappedDataSink The wrapped {@link DataSink}.
+ * @param scratch Scratch space. Data is encrypted into this array before being written to the
+ * wrapped {@link DataSink}. It should be of appropriate size for the expected writes. If a
+ * write is larger than the size of this array the write will still succeed, but multiple
+ * cipher calls will be required to complete the operation. If {@code null} then encryption
+ * will overwrite the input {@code data}.
+ */
+ public AesCipherDataSink(byte[] secretKey, DataSink wrappedDataSink, @Nullable byte[] scratch) {
+ this.wrappedDataSink = wrappedDataSink;
+ this.secretKey = secretKey;
+ this.scratch = scratch;
+ }
+
+ @Override
+ public void open(DataSpec dataSpec) throws IOException {
+ wrappedDataSink.open(dataSpec);
+ long nonce = CryptoUtil.getFNV64Hash(dataSpec.key);
+ cipher = new AesFlushingCipher(Cipher.ENCRYPT_MODE, secretKey, nonce,
+ dataSpec.absoluteStreamPosition);
+ }
+
+ @Override
+ public void write(byte[] data, int offset, int length) throws IOException {
+ if (scratch == null) {
+ // In-place mode. Writes over the input data.
+ castNonNull(cipher).updateInPlace(data, offset, length);
+ wrappedDataSink.write(data, offset, length);
+ } else {
+ // Use scratch space. The original data remains intact.
+ int bytesProcessed = 0;
+ while (bytesProcessed < length) {
+ int bytesToProcess = Math.min(length - bytesProcessed, scratch.length);
+ castNonNull(cipher)
+ .update(data, offset + bytesProcessed, bytesToProcess, scratch, /* outOffset= */ 0);
+ wrappedDataSink.write(scratch, /* offset= */ 0, bytesToProcess);
+ bytesProcessed += bytesToProcess;
+ }
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ cipher = null;
+ wrappedDataSink.close();
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSource.java
new file mode 100644
index 0000000000..0b0687b57e
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSource.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.crypto;
+
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull;
+
+import android.net.Uri;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.TransferListener;
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import javax.crypto.Cipher;
+
+/**
+ * A {@link DataSource} that decrypts the data read from an upstream source.
+ */
+public final class AesCipherDataSource implements DataSource {
+
+ private final DataSource upstream;
+ private final byte[] secretKey;
+
+ @Nullable private AesFlushingCipher cipher;
+
+ public AesCipherDataSource(byte[] secretKey, DataSource upstream) {
+ this.upstream = upstream;
+ this.secretKey = secretKey;
+ }
+
+ @Override
+ public void addTransferListener(TransferListener transferListener) {
+ upstream.addTransferListener(transferListener);
+ }
+
+ @Override
+ public long open(DataSpec dataSpec) throws IOException {
+ long dataLength = upstream.open(dataSpec);
+ long nonce = CryptoUtil.getFNV64Hash(dataSpec.key);
+ cipher = new AesFlushingCipher(Cipher.DECRYPT_MODE, secretKey, nonce,
+ dataSpec.absoluteStreamPosition);
+ return dataLength;
+ }
+
+ @Override
+ public int read(byte[] data, int offset, int readLength) throws IOException {
+ if (readLength == 0) {
+ return 0;
+ }
+ int read = upstream.read(data, offset, readLength);
+ if (read == C.RESULT_END_OF_INPUT) {
+ return C.RESULT_END_OF_INPUT;
+ }
+ castNonNull(cipher).updateInPlace(data, offset, read);
+ return read;
+ }
+
+ @Override
+ @Nullable
+ public Uri getUri() {
+ return upstream.getUri();
+ }
+
+ @Override
+ public Map<String, List<String>> getResponseHeaders() {
+ return upstream.getResponseHeaders();
+ }
+
+ @Override
+ public void close() throws IOException {
+ cipher = null;
+ upstream.close();
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipher.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipher.java
new file mode 100644
index 0000000000..985a6dcf24
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipher.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.crypto;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.nio.ByteBuffer;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import javax.crypto.Cipher;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.ShortBufferException;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * A flushing variant of a AES/CTR/NoPadding {@link Cipher}.
+ *
+ * Unlike a regular {@link Cipher}, the update methods of this class are guaranteed to process all
+ * of the bytes input (and hence output the same number of bytes).
+ */
+public final class AesFlushingCipher {
+
+ private final Cipher cipher;
+ private final int blockSize;
+ private final byte[] zerosBlock;
+ private final byte[] flushedBlock;
+
+ private int pendingXorBytes;
+
+ public AesFlushingCipher(int mode, byte[] secretKey, long nonce, long offset) {
+ try {
+ cipher = Cipher.getInstance("AES/CTR/NoPadding");
+ blockSize = cipher.getBlockSize();
+ zerosBlock = new byte[blockSize];
+ flushedBlock = new byte[blockSize];
+ long counter = offset / blockSize;
+ int startPadding = (int) (offset % blockSize);
+ cipher.init(
+ mode,
+ new SecretKeySpec(secretKey, Util.splitAtFirst(cipher.getAlgorithm(), "/")[0]),
+ new IvParameterSpec(getInitializationVector(nonce, counter)));
+ if (startPadding != 0) {
+ updateInPlace(new byte[startPadding], 0, startPadding);
+ }
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
+ | InvalidAlgorithmParameterException e) {
+ // Should never happen.
+ throw new RuntimeException(e);
+ }
+ }
+
+ public void updateInPlace(byte[] data, int offset, int length) {
+ update(data, offset, length, data, offset);
+ }
+
+ public void update(byte[] in, int inOffset, int length, byte[] out, int outOffset) {
+ // If we previously flushed the cipher by inputting zeros up to a block boundary, then we need
+ // to manually transform the data that actually ended the block. See the comment below for more
+ // details.
+ while (pendingXorBytes > 0) {
+ out[outOffset] = (byte) (in[inOffset] ^ flushedBlock[blockSize - pendingXorBytes]);
+ outOffset++;
+ inOffset++;
+ pendingXorBytes--;
+ length--;
+ if (length == 0) {
+ return;
+ }
+ }
+
+ // Do the bulk of the update.
+ int written = nonFlushingUpdate(in, inOffset, length, out, outOffset);
+ if (length == written) {
+ return;
+ }
+
+ // We need to finish the block to flush out the remaining bytes. We do so by inputting zeros,
+ // so that the corresponding bytes output by the cipher are those that would have been XORed
+ // against the real end-of-block data to transform it. We store these bytes so that we can
+ // perform the transformation manually in the case of a subsequent call to this method with
+ // the real data.
+ int bytesToFlush = length - written;
+ Assertions.checkState(bytesToFlush < blockSize);
+ outOffset += written;
+ pendingXorBytes = blockSize - bytesToFlush;
+ written = nonFlushingUpdate(zerosBlock, 0, pendingXorBytes, flushedBlock, 0);
+ Assertions.checkState(written == blockSize);
+ // The first part of xorBytes contains the flushed data, which we copy out. The remainder
+ // contains the bytes that will be needed for manual transformation in a subsequent call.
+ for (int i = 0; i < bytesToFlush; i++) {
+ out[outOffset++] = flushedBlock[i];
+ }
+ }
+
+ private int nonFlushingUpdate(byte[] in, int inOffset, int length, byte[] out, int outOffset) {
+ try {
+ return cipher.update(in, inOffset, length, out, outOffset);
+ } catch (ShortBufferException e) {
+ // Should never happen.
+ throw new RuntimeException(e);
+ }
+ }
+
+ private byte[] getInitializationVector(long nonce, long counter) {
+ return ByteBuffer.allocate(16).putLong(nonce).putLong(counter).array();
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/crypto/CryptoUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/crypto/CryptoUtil.java
new file mode 100644
index 0000000000..a4904b9285
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/crypto/CryptoUtil.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.crypto;
+
+import androidx.annotation.Nullable;
+
+/**
+ * Utility functions for the crypto package.
+ */
+/* package */ final class CryptoUtil {
+
+ private CryptoUtil() {}
+
+ /**
+ * Returns the hash value of the input as a long using the 64 bit FNV-1a hash function. The hash
+ * values produced by this function are less likely to collide than those produced by {@link
+ * #hashCode()}.
+ */
+ public static long getFNV64Hash(@Nullable String input) {
+ if (input == null) {
+ return 0;
+ }
+
+ long hash = 0;
+ for (int i = 0; i < input.length(); i++) {
+ hash ^= input.charAt(i);
+ // This is equivalent to hash *= 0x100000001b3 (the FNV magic prime number).
+ hash += (hash << 1) + (hash << 4) + (hash << 5) + (hash << 7) + (hash << 8) + (hash << 40);
+ }
+ return hash;
+ }
+
+}