add disk usage limits (total cache size, total files, unlimited) #5

This commit is contained in:
Alexey Danilov
2015-10-23 15:05:18 +03:00
parent 55988e278d
commit d32e88f641
28 changed files with 793 additions and 72 deletions

View File

@@ -12,7 +12,7 @@ repositories {
jcenter()
}
dependencies {
compile 'com.danikula:videocache:2.2.0'
compile 'com.danikula:videocache:2.3.0'
}
```

View File

@@ -26,7 +26,7 @@ publish {
userOrg = 'alexeydanilov'
groupId = 'com.danikula'
artifactId = 'videocache'
publishVersion = '2.2.0'
publishVersion = '2.3.0'
description = 'Cache support for android VideoView'
website = 'https://github.com/danikula/AndroidVideoCache'
}

View File

@@ -0,0 +1,30 @@
package com.danikula.videocache;
import com.danikula.videocache.file.DiskUsage;
import com.danikula.videocache.file.FileNameGenerator;
import java.io.File;
/**
* Configuration for proxy cache.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
class Config {
public final File cacheRoot;
public final FileNameGenerator fileNameGenerator;
public final DiskUsage diskUsage;
Config(File cacheRoot, FileNameGenerator fileNameGenerator, DiskUsage diskUsage) {
this.cacheRoot = cacheRoot;
this.fileNameGenerator = fileNameGenerator;
this.diskUsage = diskUsage;
}
File generateCacheFile(String url) {
String name = fileNameGenerator.generate(url);
return new File(cacheRoot, name);
}
}

View File

@@ -2,6 +2,8 @@ package com.danikula.videocache;
import android.text.TextUtils;
import com.danikula.videocache.file.FileCache;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.OutputStream;

View File

@@ -1,8 +1,16 @@
package com.danikula.videocache;
import android.content.Context;
import android.os.SystemClock;
import android.util.Log;
import com.danikula.videocache.file.DiskUsage;
import com.danikula.videocache.file.FileNameGenerator;
import com.danikula.videocache.file.Md5FileNameGenerator;
import com.danikula.videocache.file.TotalCountLruDiskUsage;
import com.danikula.videocache.file.TotalSizeLruDiskUsage;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetAddress;
@@ -56,11 +64,15 @@ public class HttpProxyCacheServer {
private final ServerSocket serverSocket;
private final int port;
private final Thread waitConnectionThread;
private final FileNameGenerator fileNameGenerator;
private final Config config;
private boolean pinged;
public HttpProxyCacheServer(FileNameGenerator fileNameGenerator) {
this.fileNameGenerator = checkNotNull(fileNameGenerator);
public HttpProxyCacheServer(Context context) {
this(new Builder(context).buildConfig());
}
private HttpProxyCacheServer(Config config) {
this.config = checkNotNull(config);
try {
InetAddress inetAddress = InetAddress.getByName(PROXY_HOST);
this.serverSocket = new ServerSocket(0, 8, inetAddress);
@@ -231,7 +243,7 @@ public class HttpProxyCacheServer {
synchronized (clientsLock) {
HttpProxyCacheServerClients clients = clientsMap.get(url);
if (clients == null) {
clients = new HttpProxyCacheServerClients(url, fileNameGenerator);
clients = new HttpProxyCacheServerClients(url, config);
clientsMap.put(url, clients);
}
return clients;
@@ -328,4 +340,94 @@ public class HttpProxyCacheServer {
return pingServer();
}
}
/**
* Builder for {@link HttpProxyCacheServer}.
*/
public static final class Builder {
private static final long DEFAULT_MAX_SIZE = 512 * 104 * 1024;
private File cacheRoot;
private FileNameGenerator fileNameGenerator;
private DiskUsage diskUsage;
public Builder(Context context) {
this.cacheRoot = StorageUtils.getIndividualCacheDirectory(context);
this.diskUsage = new TotalSizeLruDiskUsage(DEFAULT_MAX_SIZE);
this.fileNameGenerator = new Md5FileNameGenerator();
}
/**
* Overrides default cache folder to be used for caching files.
* <p/>
* By default AndroidVideoCache uses
* '/Android/data/[app_package_name]/cache/video-cache/' if card is mounted and app has appropriate permission
* or 'video-cache' subdirectory in default application's cache directory otherwise.
* <p/>
* <b>Note</b> directory must be used <b>only</b> for AndroidVideoCache files.
*
* @param file a cache directory, can't be null.
* @return a builder.
*/
public Builder cacheDirectory(File file) {
this.cacheRoot = checkNotNull(file);
return this;
}
/**
* Overrides default cache file name generator {@link Md5FileNameGenerator} .
*
* @param fileNameGenerator a new file name generator.
* @return a builder.
*/
public Builder fileNameGenerator(FileNameGenerator fileNameGenerator) {
this.fileNameGenerator = checkNotNull(fileNameGenerator);
return this;
}
/**
* Sets max cache size in bytes.
* All files that exceeds limit will be deleted using LRU strategy.
* Default value is 512 Mb.
* <p/>
* Note this method overrides result of calling {@link #maxCacheFilesCount(int)}
*
* @param maxSize max cache size in bytes.
* @return a builder.
*/
public Builder maxCacheSize(long maxSize) {
this.diskUsage = new TotalSizeLruDiskUsage(maxSize);
return this;
}
/**
* Sets max cache files count.
* All files that exceeds limit will be deleted using LRU strategy.
* <p/>
* Note this method overrides result of calling {@link #maxCacheSize(long)}
*
* @param count max cache files count.
* @return a builder.
*/
public Builder maxCacheFilesCount(int count) {
this.diskUsage = new TotalCountLruDiskUsage(count);
return this;
}
/**
* Builds new instance of {@link HttpProxyCacheServer}.
*
* @return proxy cache. Only single instance should be used across whole app.
*/
public HttpProxyCacheServer build() {
Config config = buildConfig();
return new HttpProxyCacheServer(config);
}
private Config buildConfig() {
return new Config(cacheRoot, fileNameGenerator, diskUsage);
}
}
}

View File

@@ -4,6 +4,8 @@ import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import com.danikula.videocache.file.FileCache;
import java.io.File;
import java.io.IOException;
import java.net.Socket;
@@ -24,12 +26,12 @@ final class HttpProxyCacheServerClients {
private final String url;
private volatile HttpProxyCache proxyCache;
private final List<CacheListener> listeners = new CopyOnWriteArrayList<>();
private final FileNameGenerator fileNameGenerator;
private final CacheListener uiCacheListener;
private final Config config;
public HttpProxyCacheServerClients(String url, FileNameGenerator fileNameGenerator) {
public HttpProxyCacheServerClients(String url, Config config) {
this.url = checkNotNull(url);
this.fileNameGenerator = checkNotNull(fileNameGenerator);
this.config = checkNotNull(config);
this.uiCacheListener = new UiListenerHandler(url, listeners);
}
@@ -78,7 +80,7 @@ final class HttpProxyCacheServerClients {
private HttpProxyCache newHttpProxyCache() throws ProxyCacheException {
HttpUrlSource source = new HttpUrlSource(url);
FileCache cache = new FileCache(fileNameGenerator.generate(url));
FileCache cache = new FileCache(config.generateCacheFile(url), config.diskUsage);
HttpProxyCache httpProxyCache = new HttpProxyCache(source, cache);
httpProxyCache.registerCacheListener(uiCacheListener);
return httpProxyCache;

View File

@@ -5,7 +5,6 @@ import android.util.Log;
import android.webkit.MimeTypeMap;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
@@ -22,7 +21,7 @@ import static com.danikula.videocache.Preconditions.checkNotNull;
*
* @author Alexey Danilov (danikula@gmail.com).
*/
class ProxyCacheUtils {
public class ProxyCacheUtils {
static final String LOG_TAG = "ProxyCache";
static final int DEFAULT_BUFFER_SIZE = 8 * 1024;
@@ -50,19 +49,6 @@ class ProxyCacheUtils {
return preview;
}
static void createDirectory(File directory) throws IOException {
checkNotNull(directory, "File must be not null!");
if (directory.exists()) {
checkArgument(directory.isDirectory(), "File is not directory!");
} else {
boolean isCreated = directory.mkdirs();
if (!isCreated) {
String error = String.format("Directory %s can't be created", directory.getAbsolutePath());
throw new IOException(error);
}
}
}
static String encode(String url) {
try {
return URLEncoder.encode(url, "utf-8");
@@ -89,7 +75,7 @@ class ProxyCacheUtils {
}
}
static String computeMD5(String string) {
public static String computeMD5(String string) {
try {
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
byte[] digestBytes = messageDigest.digest(string.getBytes());
@@ -99,12 +85,11 @@ class ProxyCacheUtils {
}
}
static String bytesToHexString(byte[] bytes) {
private static String bytesToHexString(byte[] bytes) {
StringBuffer sb = new StringBuffer();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
}

View File

@@ -0,0 +1,81 @@
package com.danikula.videocache;
import android.content.Context;
import android.os.Environment;
import android.util.Log;
import java.io.File;
import static android.os.Environment.MEDIA_MOUNTED;
import static com.danikula.videocache.ProxyCacheUtils.LOG_TAG;
/**
* Provides application storage paths
* <p/>
* See https://github.com/nostra13/Android-Universal-Image-Loader
*
* @author Sergey Tarasevich (nostra13[at]gmail[dot]com)
* @since 1.0.0
*/
final class StorageUtils {
private static final String INDIVIDUAL_DIR_NAME = "video-cache";
/**
* Returns individual application cache directory (for only video caching from Proxy). Cache directory will be
* created on SD card <i>("/Android/data/[app_package_name]/cache/video-cache")</i> if card is mounted .
* Else - Android defines cache directory on device's file system.
*
* @param context Application context
* @return Cache {@link File directory}
*/
public static File getIndividualCacheDirectory(Context context) {
File cacheDir = getCacheDirectory(context, true);
return new File(cacheDir, INDIVIDUAL_DIR_NAME);
}
/**
* Returns application cache directory. Cache directory will be created on SD card
* <i>("/Android/data/[app_package_name]/cache")</i> (if card is mounted and app has appropriate permission) or
* on device's file system depending incoming parameters.
*
* @param context Application context
* @param preferExternal Whether prefer external location for cache
* @return Cache {@link File directory}.<br />
* <b>NOTE:</b> Can be null in some unpredictable cases (if SD card is unmounted and
* {@link android.content.Context#getCacheDir() Context.getCacheDir()} returns null).
*/
private static File getCacheDirectory(Context context, boolean preferExternal) {
File appCacheDir = null;
String externalStorageState;
try {
externalStorageState = Environment.getExternalStorageState();
} catch (NullPointerException e) { // (sh)it happens
externalStorageState = "";
}
if (preferExternal && MEDIA_MOUNTED.equals(externalStorageState)) {
appCacheDir = getExternalCacheDir(context);
}
if (appCacheDir == null) {
appCacheDir = context.getCacheDir();
}
if (appCacheDir == null) {
String cacheDirPath = "/data/data/" + context.getPackageName() + "/cache/";
Log.w(LOG_TAG, "Can't define system cache directory! '" + cacheDirPath + "%s' will be used.");
appCacheDir = new File(cacheDirPath);
}
return appCacheDir;
}
private static File getExternalCacheDir(Context context) {
File dataDir = new File(new File(Environment.getExternalStorageDirectory(), "Android"), "data");
File appCacheDir = new File(new File(dataDir, context.getPackageName()), "cache");
if (!appCacheDir.exists()) {
if (!appCacheDir.mkdirs()) {
Log.w(LOG_TAG, "Unable to create external cache directory");
return null;
}
}
return appCacheDir;
}
}

View File

@@ -0,0 +1,15 @@
package com.danikula.videocache.file;
import java.io.File;
import java.io.IOException;
/**
* Declares how {@link FileCache} will use disc space.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
public interface DiskUsage {
void touch(File file) throws IOException;
}

View File

@@ -1,11 +1,12 @@
package com.danikula.videocache;
package com.danikula.videocache.file;
import com.danikula.videocache.Cache;
import com.danikula.videocache.ProxyCacheException;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import static com.danikula.videocache.Preconditions.checkNotNull;
/**
* {@link Cache} that uses file for storing data.
*
@@ -15,13 +16,22 @@ public class FileCache implements Cache {
private static final String TEMP_POSTFIX = ".download";
private final DiskUsage diskUsage;
public File file;
private RandomAccessFile dataFile;
public FileCache(File file) throws ProxyCacheException {
this(file, new UnlimitedDiskUsage());
}
public FileCache(File file, DiskUsage diskUsage) throws ProxyCacheException {
try {
checkNotNull(file);
ProxyCacheUtils.createDirectory(file.getParentFile());
if (diskUsage == null) {
throw new NullPointerException();
}
this.diskUsage = diskUsage;
File directory = file.getParentFile();
Files.makeDir(directory);
boolean completed = file.exists();
this.file = completed ? file : new File(file.getParentFile(), file.getName() + TEMP_POSTFIX);
this.dataFile = new RandomAccessFile(this.file, completed ? "r" : "rw");
@@ -68,6 +78,7 @@ public class FileCache implements Cache {
public synchronized void close() throws ProxyCacheException {
try {
dataFile.close();
diskUsage.touch(file);
} catch (IOException e) {
throw new ProxyCacheException("Error closing file " + file, e);
}

View File

@@ -1,6 +1,4 @@
package com.danikula.videocache;
import java.io.File;
package com.danikula.videocache.file;
/**
* Generator for files to be used for caching.
@@ -9,6 +7,6 @@ import java.io.File;
*/
public interface FileNameGenerator {
File generate(String url);
String generate(String url);
}

View File

@@ -0,0 +1,88 @@
package com.danikula.videocache.file;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
/**
* Utils for work with files.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
class Files {
static void makeDir(File directory) throws IOException {
if (directory.exists()) {
if (!directory.isDirectory()) {
throw new IOException("File " + directory + " is not directory!");
}
} else {
boolean isCreated = directory.mkdirs();
if (!isCreated) {
throw new IOException(String.format("Directory %s can't be created", directory.getAbsolutePath()));
}
}
}
static List<File> getLruListFiles(File directory) {
List<File> result = new LinkedList<>();
File[] files = directory.listFiles();
if (files != null) {
result = Arrays.asList(files);
Collections.sort(result, new LastModifiedComparator());
}
return result;
}
static void setLastModifiedNow(File file) throws IOException {
if (file.exists()) {
long now = System.currentTimeMillis();
boolean modified = file.setLastModified(now); // on some devices (e.g. Nexus 5) doesn't work
if (!modified) {
modify(file);
if (file.lastModified() < now) {
throw new IOException("Error set last modified date to " + file);
}
}
}
}
static void modify(File file) throws IOException {
long size = file.length();
if (size == 0) {
recreateZeroSizeFile(file);
return;
}
RandomAccessFile accessFile = new RandomAccessFile(file, "rwd");
accessFile.seek(size - 1);
byte lastByte = accessFile.readByte();
accessFile.seek(size - 1);
accessFile.write(lastByte);
accessFile.close();
}
private static void recreateZeroSizeFile(File file) throws IOException {
if (!file.delete() || !file.createNewFile()) {
throw new IOException("Error recreate zero-size file " + file);
}
}
private static final class LastModifiedComparator implements Comparator<File> {
@Override
public int compare(File lhs, File rhs) {
return compareLong(lhs.lastModified(), rhs.lastModified());
}
private int compareLong(long first, long second) {
return (first < second) ? -1 : ((first == second) ? 0 : 1);
}
}
}

View File

@@ -0,0 +1,76 @@
package com.danikula.videocache.file;
import android.util.Log;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* {@link DiskUsage} that uses LRU (Least Recently Used) strategy to trim cache.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
abstract class LruDiskUsage implements DiskUsage {
private static final String LOG_TAG = "ProxyCache";
private final ExecutorService workerThread = Executors.newSingleThreadExecutor();
@Override
public void touch(File file) throws IOException {
workerThread.submit(new TouchCallable(file));
}
private void touchInBackground(File file) throws IOException {
Files.setLastModifiedNow(file);
List<File> files = Files.getLruListFiles(file.getParentFile());
trim(files);
}
protected abstract boolean accept(File file, long totalSize, int totalCount);
private void trim(List<File> files) {
long totalSize = countTotalSize(files);
int totalCount = files.size();
for (File file : files) {
boolean accepted = accept(file, totalSize, totalCount);
if (!accepted) {
long fileSize = file.length();
boolean deleted = file.delete();
if (deleted) {
totalCount--;
totalSize -= fileSize;
Log.i(LOG_TAG, "Cache file " + file + " is deleted because it exceeds cache limit");
} else {
Log.e(LOG_TAG, "Error deleting file " + file + " for trimming cache");
}
}
}
}
private long countTotalSize(List<File> files) {
long totalSize = 0;
for (File file : files) {
totalSize += file.length();
}
return totalSize;
}
private class TouchCallable implements Callable<Void> {
private final File file;
public TouchCallable(File file) {
this.file = file;
}
@Override
public Void call() throws Exception {
touchInBackground(file);
return null;
}
}
}

View File

@@ -1,10 +1,8 @@
package com.danikula.videocache;
package com.danikula.videocache.file;
import android.text.TextUtils;
import java.io.File;
import static com.danikula.videocache.Preconditions.checkNotNull;
import com.danikula.videocache.ProxyCacheUtils;
/**
* Implementation of {@link FileNameGenerator} that uses MD5 of url as file name
@@ -14,19 +12,12 @@ import static com.danikula.videocache.Preconditions.checkNotNull;
public class Md5FileNameGenerator implements FileNameGenerator {
private static final int MAX_EXTENSION_LENGTH = 4;
private final File cacheDirectory;
public Md5FileNameGenerator(File cacheDirectory) {
this.cacheDirectory = checkNotNull(cacheDirectory);
}
@Override
public File generate(String url) {
checkNotNull(url);
public String generate(String url) {
String extension = getExtension(url);
String name = ProxyCacheUtils.computeMD5(url);
name = TextUtils.isEmpty(extension) ? name : name + "." + extension;
return new File(cacheDirectory, name);
return TextUtils.isEmpty(extension) ? name : name + "." + extension;
}
private String getExtension(String url) {

View File

@@ -0,0 +1,25 @@
package com.danikula.videocache.file;
import java.io.File;
/**
* {@link DiskUsage} that uses LRU (Least Recently Used) strategy and trims cache size to max files count if needed.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
public class TotalCountLruDiskUsage extends LruDiskUsage {
private final int maxCount;
public TotalCountLruDiskUsage(int maxCount) {
if (maxCount <= 0) {
throw new IllegalArgumentException("Max count must be positive number!");
}
this.maxCount = maxCount;
}
@Override
protected boolean accept(File file, long totalSize, int totalCount) {
return totalCount <= maxCount;
}
}

View File

@@ -0,0 +1,25 @@
package com.danikula.videocache.file;
import java.io.File;
/**
* {@link DiskUsage} that uses LRU (Least Recently Used) strategy and trims cache size to max size if needed.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
public class TotalSizeLruDiskUsage extends LruDiskUsage {
private final long maxSize;
public TotalSizeLruDiskUsage(long maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("Max size must be positive number!");
}
this.maxSize = maxSize;
}
@Override
protected boolean accept(File file, long totalSize, int totalCount) {
return totalSize <= maxSize;
}
}

View File

@@ -0,0 +1,17 @@
package com.danikula.videocache.file;
import java.io.File;
import java.io.IOException;
/**
* Unlimited version of {@link DiskUsage}.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
public class UnlimitedDiskUsage implements DiskUsage {
@Override
public void touch(File file) throws IOException {
// do nothing
}
}

View File

@@ -35,10 +35,10 @@ apt {
}
dependencies {
// compile project(':library')
compile project(':library')
compile 'com.android.support:support-v4:23.1.0'
compile 'org.androidannotations:androidannotations-api:3.3.2'
compile 'com.danikula:videocache:2.2.0'
// compile 'com.danikula:videocache:2.2.0'
compile 'com.viewpagerindicator:library:2.4.2-SNAPSHOT@aar'
apt 'org.androidannotations:androidannotations:3.3.2'
}

View File

@@ -3,9 +3,7 @@ package com.danikula.videocache.sample;
import android.app.Application;
import android.content.Context;
import com.danikula.videocache.FileNameGenerator;
import com.danikula.videocache.HttpProxyCacheServer;
import com.danikula.videocache.Md5FileNameGenerator;
/**
* @author Alexey Danilov (danikula@gmail.com).
@@ -20,7 +18,6 @@ public class App extends Application {
}
private HttpProxyCacheServer newProxy() {
FileNameGenerator nameGenerator = new Md5FileNameGenerator(getExternalCacheDir());
return new HttpProxyCacheServer(nameGenerator);
return new HttpProxyCacheServer(this);
}
}

View File

@@ -1,5 +1,7 @@
package com.danikula.videocache;
import com.danikula.videocache.file.FileNameGenerator;
import com.danikula.videocache.file.Md5FileNameGenerator;
import com.danikula.videocache.test.BuildConfig;
import org.junit.Test;
@@ -62,21 +64,16 @@ public class FileNameGeneratorTest {
assertThat(path).isEqualTo(expected);
}
@Test(expected = NullPointerException.class)
public void testAssertNullRoot() throws Exception {
new Md5FileNameGenerator(null);
fail("Root folder should be not null");
}
@Test(expected = NullPointerException.class)
public void testAssertNullUrl() throws Exception {
FileNameGenerator nameGenerator = new Md5FileNameGenerator(new File("/"));
FileNameGenerator nameGenerator = new Md5FileNameGenerator();
nameGenerator.generate(null);
fail("Url should be not null");
}
private String generateMd5Name(String rootFolder, String url) {
FileNameGenerator nameGenerator = new Md5FileNameGenerator(new File(rootFolder));
return nameGenerator.generate(url).getAbsolutePath();
FileNameGenerator nameGenerator = new Md5FileNameGenerator();
String name = nameGenerator.generate(url);
return new File(rootFolder, name).getAbsolutePath();
}
}

View File

@@ -3,10 +3,13 @@ package com.danikula.videocache;
import android.util.Pair;
import com.danikula.android.garden.io.IoUtils;
import com.danikula.videocache.file.FileNameGenerator;
import com.danikula.videocache.file.Md5FileNameGenerator;
import com.danikula.videocache.support.ProxyCacheTestUtils;
import com.danikula.videocache.support.Response;
import com.danikula.videocache.test.BuildConfig;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricGradleTestRunner;
@@ -17,12 +20,18 @@ import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import static com.danikula.android.garden.io.Files.cleanDirectory;
import static com.danikula.android.garden.io.Files.createDirectory;
import static com.danikula.videocache.support.ProxyCacheTestUtils.ASSETS_DATA_BIG_NAME;
import static com.danikula.videocache.support.ProxyCacheTestUtils.ASSETS_DATA_NAME;
import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_BIG_SIZE;
import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_BIG_URL;
import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_BIG_URL_ONE_REDIRECT;
import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_SIZE;
import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_URL;
import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_URL_3_REDIRECTS;
import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_URL_6_REDIRECTS;
import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_URL_ONE_REDIRECT;
import static com.danikula.videocache.support.ProxyCacheTestUtils.getFileContent;
import static com.danikula.videocache.support.ProxyCacheTestUtils.loadAssetFile;
import static com.danikula.videocache.support.ProxyCacheTestUtils.readProxyResponse;
@@ -35,6 +44,15 @@ import static org.fest.assertions.api.Assertions.assertThat;
@Config(constants = BuildConfig.class, emulateSdk = BuildConfig.MIN_SDK_VERSION)
public class HttpProxyCacheServerTest {
private File cacheFolder;
@Before
public void setup() throws Exception {
cacheFolder = ProxyCacheTestUtils.newCacheFile();
createDirectory(cacheFolder);
cleanDirectory(cacheFolder);
}
@Test
public void testHttpProxyCache() throws Exception {
Pair<File, Response> response = readProxyData(HTTP_DATA_URL);
@@ -46,14 +64,14 @@ public class HttpProxyCacheServerTest {
@Test
public void testProxyContentWithPartialCache() throws Exception {
FileNameGenerator fileNameGenerator = new Md5FileNameGenerator(RuntimeEnvironment.application.getExternalCacheDir());
File file = fileNameGenerator.generate(HTTP_DATA_URL);
File cacheDir = RuntimeEnvironment.application.getExternalCacheDir();
File file = new File(cacheDir, new Md5FileNameGenerator().generate(HTTP_DATA_URL));
int partialCacheSize = 1000;
byte[] partialData = ProxyCacheTestUtils.generate(partialCacheSize);
File partialCacheFile = ProxyCacheTestUtils.getTempFile(file);
IoUtils.saveToFile(partialData, partialCacheFile);
HttpProxyCacheServer proxy = new HttpProxyCacheServer(fileNameGenerator);
HttpProxyCacheServer proxy = newProxy(cacheDir);
Response response = readProxyResponse(proxy, HTTP_DATA_URL);
proxy.shutdown();
@@ -132,11 +150,70 @@ public class HttpProxyCacheServerTest {
assertThat(getFileContent(response.first)).isEqualTo(loadAssetFile(ASSETS_DATA_BIG_NAME));
}
@Test
public void testMaxSizeCacheLimit() throws Exception {
HttpProxyCacheServer proxy = new HttpProxyCacheServer.Builder(RuntimeEnvironment.application)
.cacheDirectory(cacheFolder)
.maxCacheSize(HTTP_DATA_SIZE * 3 - 1) // for 2 files
.build();
// use different url (doesn't matter than same content)
readProxyResponse(proxy, HTTP_DATA_URL, 0);
Thread.sleep(1050); // wait for new last modified date (file rounds time to second)
readProxyResponse(proxy, HTTP_DATA_URL_ONE_REDIRECT, 0);
Thread.sleep(1050);
readProxyResponse(proxy, HTTP_DATA_URL_3_REDIRECTS, 0);
Thread.sleep(1050);
assertThat(file(cacheFolder, HTTP_DATA_URL)).doesNotExist();
assertThat(file(cacheFolder, HTTP_DATA_URL_ONE_REDIRECT)).exists();
assertThat(file(cacheFolder, HTTP_DATA_URL_3_REDIRECTS)).exists();
readProxyResponse(proxy, HTTP_DATA_URL_ONE_REDIRECT, 0); // touch file
readProxyResponse(proxy, HTTP_DATA_URL_6_REDIRECTS, 0);
proxy.shutdown();
assertThat(file(cacheFolder, HTTP_DATA_URL_3_REDIRECTS)).doesNotExist();
assertThat(file(cacheFolder, HTTP_DATA_URL_ONE_REDIRECT)).exists();
assertThat(file(cacheFolder, HTTP_DATA_URL_6_REDIRECTS)).exists();
}
@Test
public void testMaxFileCacheLimit() throws Exception {
HttpProxyCacheServer proxy = new HttpProxyCacheServer.Builder(RuntimeEnvironment.application)
.cacheDirectory(cacheFolder)
.maxCacheFilesCount(2)
.build();
// use different url (doesn't matter than same content)
readProxyResponse(proxy, HTTP_DATA_URL, 0);
Thread.sleep(1050); // wait for new last modified date (file rounds time to second)
readProxyResponse(proxy, HTTP_DATA_URL_ONE_REDIRECT, 0);
Thread.sleep(1050);
readProxyResponse(proxy, HTTP_DATA_URL_3_REDIRECTS, 0);
Thread.sleep(1050);
assertThat(file(cacheFolder, HTTP_DATA_URL)).doesNotExist();
assertThat(file(cacheFolder, HTTP_DATA_URL_ONE_REDIRECT)).exists();
assertThat(file(cacheFolder, HTTP_DATA_URL_3_REDIRECTS)).exists();
readProxyResponse(proxy, HTTP_DATA_URL_ONE_REDIRECT, 0); // touch file
readProxyResponse(proxy, HTTP_DATA_URL_6_REDIRECTS, 0);
proxy.shutdown();
assertThat(file(cacheFolder, HTTP_DATA_URL_3_REDIRECTS)).doesNotExist();
assertThat(file(cacheFolder, HTTP_DATA_URL_ONE_REDIRECT)).exists();
assertThat(file(cacheFolder, HTTP_DATA_URL_6_REDIRECTS)).exists();
}
private Pair<File, Response> readProxyData(String url, int offset) throws IOException {
File externalCacheDir = RuntimeEnvironment.application.getExternalCacheDir();
FileNameGenerator fileNameGenerator = new Md5FileNameGenerator(externalCacheDir);
File file = fileNameGenerator.generate(url);
HttpProxyCacheServer proxy = new HttpProxyCacheServer(fileNameGenerator);
File file = file(externalCacheDir, url);
HttpProxyCacheServer proxy = newProxy(externalCacheDir);
Response response = readProxyResponse(proxy, url, offset);
proxy.shutdown();
@@ -144,7 +221,19 @@ public class HttpProxyCacheServerTest {
return new Pair<>(file, response);
}
private File file(File parent, String url) {
FileNameGenerator fileNameGenerator = new Md5FileNameGenerator();
String name = fileNameGenerator.generate(url);
return new File(parent, name);
}
private Pair<File, Response> readProxyData(String url) throws IOException {
return readProxyData(url, -1);
}
private HttpProxyCacheServer newProxy(File cacheDir) {
return new HttpProxyCacheServer.Builder(RuntimeEnvironment.application)
.cacheDirectory(cacheDir)
.build();
}
}

View File

@@ -1,5 +1,6 @@
package com.danikula.videocache;
import com.danikula.videocache.file.FileCache;
import com.danikula.videocache.support.ProxyCacheTestUtils;
import com.danikula.videocache.support.Response;
import com.danikula.videocache.test.BuildConfig;

View File

@@ -1,6 +1,7 @@
package com.danikula.videocache;
import com.danikula.android.garden.io.IoUtils;
import com.danikula.videocache.file.FileCache;
import com.danikula.videocache.support.AngryHttpUrlSource;
import com.danikula.videocache.support.PhlegmaticByteArraySource;
import com.danikula.videocache.test.BuildConfig;

View File

@@ -0,0 +1,122 @@
package com.danikula.videocache.file;
import com.danikula.videocache.support.ProxyCacheTestUtils;
import com.danikula.videocache.test.BuildConfig;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.annotation.Config;
import java.io.File;
import java.io.IOException;
import static com.danikula.android.garden.io.Files.cleanDirectory;
import static com.danikula.android.garden.io.Files.createDirectory;
import static org.fest.assertions.api.Assertions.assertThat;
/**
* Tests for implementations of {@link DiskUsage}.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, emulateSdk = BuildConfig.MIN_SDK_VERSION)
public class DiskUsageTest {
private File cacheFolder;
@Before
public void setup() throws Exception {
cacheFolder = ProxyCacheTestUtils.newCacheFile();
createDirectory(cacheFolder);
cleanDirectory(cacheFolder);
}
@Test
public void testMaxSizeCacheLimit() throws Exception {
DiskUsage diskUsage = new TotalSizeLruDiskUsage(300);
long now = System.currentTimeMillis();
createFile(file("b"), 101, now - 10000);
createFile(file("c"), 102, now - 8000);
createFile(file("a"), 104, now - 4000); // exceeds
diskUsage.touch(file("c"));
waitForAsyncTrimming();
assertThat(file("b")).doesNotExist();
assertThat(file("c")).exists();
assertThat(file("a")).exists();
createFile(file("d"), 500, now); // exceeds all
diskUsage.touch(file("d"));
waitForAsyncTrimming();
assertThat(file("a")).doesNotExist();
assertThat(file("c")).doesNotExist();
assertThat(file("d")).doesNotExist();
}
@Test
public void testMaxFilesCount() throws Exception {
DiskUsage diskUsage = new TotalCountLruDiskUsage(2);
long now = System.currentTimeMillis();
createFile(file("b"), 101, now - 10000);
createFile(file("c"), 102, now - 8000);
createFile(file("a"), 104, now - 4000);
diskUsage.touch(file("c"));
waitForAsyncTrimming();
assertThat(file("b")).doesNotExist();
assertThat(file("a")).exists();
assertThat(file("c")).exists();
createFile(file("d"), 500, now);
diskUsage.touch(file("d"));
waitForAsyncTrimming();
assertThat(file("a")).doesNotExist();
assertThat(file("c")).exists();
assertThat(file("d")).exists();
}
@Test
public void testTouch() throws Exception {
DiskUsage diskUsage = new TotalCountLruDiskUsage(2);
long now = System.currentTimeMillis();
createFile(file("b"), 101, now - 10000);
createFile(file("c"), 102, now - 8000);
createFile(file("a"), 104, now - 4000);
diskUsage.touch(file("b"));
waitForAsyncTrimming();
assertThat(file("b")).exists();
assertThat(file("a")).exists();
assertThat(file("c")).doesNotExist();
Thread.sleep(1000); // last modified is rounded to seconds, so wait for sec
new TotalCountLruDiskUsage(1).touch(file("a"));
waitForAsyncTrimming();
assertThat(file("a")).exists();
assertThat(file("b")).doesNotExist();
}
private void waitForAsyncTrimming() throws InterruptedException {
Thread.sleep(200);
}
private File file(String name) {
return new File(cacheFolder, name);
}
private void createFile(File file, int capacity, long lastModified) throws IOException {
byte[] data = ProxyCacheTestUtils.generate(capacity);
com.google.common.io.Files.write(data, file);
boolean modified = file.setLastModified(lastModified);
assertThat(modified).isTrue();
}
}

View File

@@ -1,7 +1,9 @@
package com.danikula.videocache;
package com.danikula.videocache.file;
import com.danikula.android.garden.io.Files;
import com.danikula.android.garden.io.IoUtils;
import com.danikula.videocache.Cache;
import com.danikula.videocache.ProxyCacheException;
import com.danikula.videocache.test.BuildConfig;
import org.junit.Assert;

View File

@@ -0,0 +1,62 @@
package com.danikula.videocache.file;
import com.danikula.videocache.support.ProxyCacheTestUtils;
import com.danikula.videocache.test.BuildConfig;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.annotation.Config;
import java.io.File;
import static org.fest.assertions.api.Assertions.assertThat;
/**
* Tests for {@link Files}.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, emulateSdk = BuildConfig.MIN_SDK_VERSION)
public class FilesTest {
@Test
public void testModify() throws Exception {
byte[] data = ProxyCacheTestUtils.generate(999);
File file = ProxyCacheTestUtils.newCacheFile();
com.google.common.io.Files.write(data, file);
long lastModified = file.lastModified();
Thread.sleep(1100); // file can store modification date in seconds. so wait for ~ 1 sec
Files.modify(file);
assertThat(file).hasBinaryContent(data);
assertThat(file.lastModified()).isGreaterThan(lastModified);
}
@Test
public void testSetModifiedNow() throws Exception {
File file = ProxyCacheTestUtils.newCacheFile();
com.google.common.io.Files.write(ProxyCacheTestUtils.generate(22), file);
Thread.sleep(1100); // file can store modification date in seconds. so wait for ~ 1 sec
long nowRoundedToSecond = System.currentTimeMillis() / 1000 * 1000;
Files.setLastModifiedNow(file);
assertThat(file.lastModified()).isGreaterThanOrEqualTo(nowRoundedToSecond);
}
@Test
public void testModifyZeroSizeFile() throws Exception {
File file = ProxyCacheTestUtils.newCacheFile();
boolean created = file.createNewFile();
assertThat(created).isTrue();
Thread.sleep(1100); // file can store modification date in seconds. so wait for ~ 2 sec
long nowRoundedToSecond = System.currentTimeMillis() / 1000 * 1000;
Files.modify(file);
assertThat(file.lastModified()).isGreaterThanOrEqualTo(nowRoundedToSecond);
}
}

View File

@@ -8,6 +8,7 @@ import com.danikula.videocache.Source;
*
* @author Alexey Danilov (danikula@gmail.com).
*/
@Deprecated // use Mockito to throw error
public class AngryHttpUrlSource implements Source {
@Override

View File

@@ -8,6 +8,7 @@ import java.util.Random;
/**
* @author Alexey Danilov (danikula@gmail.com).
*/
@Deprecated // TODO: use Mockito to mock delay
public class PhlegmaticByteArraySource extends ByteArraySource {
private final Random delayGenerator;