/*
* 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 androidx.media3.datasource.cache;
import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState;
import static java.lang.Math.max;
import static java.lang.Math.min;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.util.Log;
import java.io.File;
import java.util.ArrayList;
import java.util.TreeSet;
/** Defines the cached content for a single resource. */
/* package */ final class CachedContent {
private static final String TAG = "CachedContent";
/** The cache id that uniquely identifies the resource. */
public final int id;
/** The cache key that uniquely identifies the resource. */
public final String key;
/** The cached spans of this content. */
private final TreeSet<SimpleCacheSpan> cachedSpans;
/** Currently locked ranges. */
private final ArrayList<Range> lockedRanges;
/** Metadata values. */
private DefaultContentMetadata metadata;
/**
* Creates a CachedContent.
*
* @param id The cache id of the resource.
* @param key The cache key of the resource.
*/
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;
cachedSpans = new TreeSet<>();
lockedRanges = new ArrayList<>();
}
/** 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 entire resource is fully unlocked. */
public boolean isFullyUnlocked() {
return lockedRanges.isEmpty();
}
/**
* Returns whether the specified range of the resource is fully locked by a single lock.
*
* @param position The position of the range.
* @param length The length of the range, or {@link C#LENGTH_UNSET} if unbounded.
* @return Whether the range is fully locked by a single lock.
*/
public boolean isFullyLocked(long position, long length) {
for (int i = 0; i < lockedRanges.size(); i++) {
if (lockedRanges.get(i).contains(position, length)) {
return true;
}
}
return false;
}
/**
* Attempts to lock the specified range of the resource.
*
* @param position The position of the range.
* @param length The length of the range, or {@link C#LENGTH_UNSET} if unbounded.
* @return Whether the range was successfully locked.
*/
public boolean lockRange(long position, long length) {
for (int i = 0; i < lockedRanges.size(); i++) {
if (lockedRanges.get(i).intersects(position, length)) {
return false;
}
}
lockedRanges.add(new Range(position, length));
return true;
}
/**
* Unlocks the currently locked range starting at the specified position.
*
* @param position The starting position of the locked range.
* @throws IllegalStateException If there was no locked range starting at the specified position.
*/
public void unlockRange(long position) {
for (int i = 0; i < lockedRanges.size(); i++) {
if (lockedRanges.get(i).position == position) {
lockedRanges.remove(i);
return;
}
}
throw new IllegalStateException();
}
/** 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 cache span corresponding to the provided range. See {@link
* Cache#startReadWrite(String, long, long)} for detailed descriptions of the returned spans.
*
* @param position The position of the span being requested.
* @param length The length of the span, or {@link C#LENGTH_UNSET} if unbounded.
* @return The corresponding cache {@link SimpleCacheSpan}.
*/
public SimpleCacheSpan getSpan(long position, long length) {
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);
if (ceilSpan != null) {
long holeLength = ceilSpan.position - position;
length = length == C.LENGTH_UNSET ? holeLength : min(holeLength, length);
}
return SimpleCacheSpan.createHole(key, position, length);
}
/**
* Returns the length of continuously cached data starting from {@code position}, up to a maximum
* of {@code maxLength}. If {@code position} isn't cached, then {@code -holeLength} is returned,
* where {@code holeLength} is the length of continuously un-cached data starting from {@code
* position}, up to a maximum of {@code maxLength}.
*
* @param position The starting position of the data.
* @param length The maximum length of the data or hole to be returned.
* @return The length of continuously cached data, or {@code -holeLength} if {@code position}
* isn't cached.
*/
public long getCachedBytesLength(long position, long length) {
checkArgument(position >= 0);
checkArgument(length >= 0);
SimpleCacheSpan span = getSpan(position, length);
if (span.isHoleSpan()) {
// We don't have a span covering the start of the queried region.
return -min(span.isOpenEnded() ? Long.MAX_VALUE : span.length, length);
}
long queryEndPosition = position + length;
if (queryEndPosition < 0) {
// The calculation rolled over (length is probably Long.MAX_VALUE).
queryEndPosition = Long.MAX_VALUE;
}
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 = max(currentEndPosition, next.position + next.length);
if (currentEndPosition >= queryEndPosition) {
// We've found spans covering the queried region.
break;
}
}
}
return 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) {
checkState(cachedSpans.remove(cacheSpan));
File file = checkNotNull(cacheSpan.file);
if (updateFile) {
File directory = checkNotNull(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)) {
if (span.file != null) {
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);
}
private static final class Range {
/** The starting position of the range. */
public final long position;
/** The length of the range, or {@link C#LENGTH_UNSET} if unbounded. */
public final long length;
public Range(long position, long length) {
this.position = position;
this.length = length;
}
/**
* Returns whether this range fully contains the range specified by {@code otherPosition} and
* {@code otherLength}.
*
* @param otherPosition The position of the range to check.
* @param otherLength The length of the range to check, or {@link C#LENGTH_UNSET} if unbounded.
* @return Whether this range fully contains the specified range.
*/
public boolean contains(long otherPosition, long otherLength) {
if (length == C.LENGTH_UNSET) {
return otherPosition >= position;
} else if (otherLength == C.LENGTH_UNSET) {
return false;
} else {
return position <= otherPosition && (otherPosition + otherLength) <= (position + length);
}
}
/**
* Returns whether this range intersects with the range specified by {@code otherPosition} and
* {@code otherLength}.
*
* @param otherPosition The position of the range to check.
* @param otherLength The length of the range to check, or {@link C#LENGTH_UNSET} if unbounded.
* @return Whether this range intersects with the specified range.
*/
public boolean intersects(long otherPosition, long otherLength) {
if (position <= otherPosition) {
return length == C.LENGTH_UNSET || position + length > otherPosition;
} else {
return otherLength == C.LENGTH_UNSET || otherPosition + otherLength > position;
}
}
}
}