blob: d8a0671469b4e0d7a6c3191e97a3c3b38adebb0a [file] [log] [blame]
/*
* 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 com.google.android.exoplayer2.upstream.cache;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Assertions;
import 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.
* @param index The cached content index.
* @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.
* @param index The cached content index.
* @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(Assertions.checkNotNull(matcher.group(1)));
@Nullable 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(Assertions.checkNotNull(matcher.group(2)));
if (lastTouchTimestamp == C.TIME_UNSET) {
lastTouchTimestamp = Long.parseLong(Assertions.checkNotNull(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 The 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) {
@Nullable String key = null;
String filename = file.getName();
Matcher matcher = CACHE_FILE_PATTERN_V2.matcher(filename);
if (matcher.matches()) {
key = Util.unescapeFileName(Assertions.checkNotNull(matcher.group(1)));
} else {
matcher = CACHE_FILE_PATTERN_V1.matcher(filename);
if (matcher.matches()) {
key = Assertions.checkNotNull(matcher.group(1)); // Keys were not escaped in version 1.
}
}
if (key == null) {
return null;
}
File newCacheFile =
getCacheFile(
Assertions.checkStateNotNull(file.getParentFile()),
index.assignIdForKey(key),
Long.parseLong(Assertions.checkNotNull(matcher.group(2))),
Long.parseLong(Assertions.checkNotNull(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);
}
}