summaryrefslogtreecommitdiffstats
path: root/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheUtil.java
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheUtil.java')
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheUtil.java434
1 files changed, 434 insertions, 0 deletions
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);
+ }
+ }
+}