Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99b8330679 | ||
|
|
0f522f200d | ||
|
|
71c6301eb4 | ||
|
|
372542c2dc | ||
|
|
22cfddb2c9 | ||
|
|
8fc4a9ac7d | ||
|
|
6c996ea66c | ||
|
|
582832f8b5 | ||
|
|
cd0b4111a4 | ||
|
|
eb67640212 | ||
|
|
1d50807fe0 | ||
|
|
6da9650030 | ||
|
|
a1d00fec7b | ||
|
|
f5a779266d | ||
|
|
ca6f36ada3 | ||
|
|
33b7e5e9d3 | ||
|
|
0aad13f118 | ||
|
|
a270460c8d |
48
README.md
@@ -1,6 +1,21 @@
|
||||
# Video cache support for Android
|
||||
## Video cache support for Android
|
||||
[](http://android-arsenal.com/details/1/1751)
|
||||
|
||||
## Table of Content
|
||||
- [Why AndroidVideoCache?](#why-androidvideocache)
|
||||
- [Features](#features)
|
||||
- [Get started](#get-started)
|
||||
- [Recipes](#recipes)
|
||||
- [Disk cache limit](#disk-cache-limit)
|
||||
- [Listen caching progress](#listen-caching-progress)
|
||||
- [Sample](#sample)
|
||||
- [Known problems](#known-problems)
|
||||
- [Whats new](#whats-new)
|
||||
- [Code contributions](#code-contributions)
|
||||
- [Where published?](#where-published)
|
||||
- [Questions?](#questions)
|
||||
- [License](#license)
|
||||
|
||||
## Why AndroidVideoCache?
|
||||
Because there is no sense to download video a lot of times while streaming!
|
||||
`AndroidVideoCache` allows to add caching support to your `VideoView/MediaPlayer`, [ExoPlayer](https://github.com/danikula/ExoPlayer/commit/6110be8559f003f98020ada8c5e09691b67aaff4) or any another player with help of single line!
|
||||
@@ -14,14 +29,14 @@ Because there is no sense to download video a lot of times while streaming!
|
||||
|
||||
Note `AndroidVideoCache` works only with **direct urls** to media file, it [**doesn't support**](https://github.com/danikula/AndroidVideoCache/issues/19) any streaming technology like DASH, SmoothStreaming, HLS.
|
||||
|
||||
## How to use?
|
||||
## Get started
|
||||
Just add dependency (`AndroidVideoCache` is available in jcenter):
|
||||
```
|
||||
repositories {
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
compile 'com.danikula:videocache:2.3.2'
|
||||
compile 'com.danikula:videocache:2.6.0'
|
||||
}
|
||||
```
|
||||
|
||||
@@ -64,6 +79,8 @@ public class App extends Application {
|
||||
or use [simple factory](http://pastebin.com/s2fafSYS).
|
||||
More preferable way is use some dependency injector like [Dagger](http://square.github.io/dagger/).
|
||||
|
||||
## Recipes
|
||||
### Disk cache limit
|
||||
By default `HttpProxyCacheServer` uses 512Mb for caching files. You can change this value:
|
||||
|
||||
```java
|
||||
@@ -82,9 +99,20 @@ private HttpProxyCacheServer newProxy() {
|
||||
.maxCacheFilesCount(20)
|
||||
.build();
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
See `sample` app for details.
|
||||
### Listen caching progress
|
||||
Use `HttpProxyCacheServer.registerCacheListener(CacheListener listener)` method to set listener with callback `onCacheAvailable(File cacheFile, String url, int percentsAvailable)` to be aware of caching progress. Do not forget to to unsubscribe listener with help of `HttpProxyCacheServer.unregisterCacheListener(CacheListener listener)` method to avoid memory leaks.
|
||||
|
||||
Use `HttpProxyCacheServer.isCached(String url)` method to check was url's content fully cached to file or not.
|
||||
|
||||
See `sample` app for more details.
|
||||
|
||||
### Sample
|
||||
See `sample` app.
|
||||
|
||||
## Known problems
|
||||
`AndroidVideoCache` [doesn't work](https://github.com/danikula/AndroidVideoCache/issues/28) if wifi or mobile internet connection uses proxy.
|
||||
|
||||
## Whats new
|
||||
See Release Notes [here](https://github.com/danikula/AndroidVideoCache/releases)
|
||||
@@ -94,9 +122,11 @@ If it's a feature that you think would need to be discussed please open an issue
|
||||
|
||||
1. [Fork the project](http://help.github.com/fork-a-repo/)
|
||||
2. Create a feature branch (git checkout -b my_branch)
|
||||
3. Push your changes to your new branch (git push origin my_branch)
|
||||
4. Initiate a [pull request](http://help.github.com/send-pull-requests/) on github
|
||||
5. Your pull request will be reviewed and hopefully merged :)
|
||||
3. Fix a problem. Your code **must** contain test for reproducing problem. Your tests **must be passed** with help of your fix
|
||||
4. Push your changes to your new branch (git push origin my_branch)
|
||||
5. Initiate a [pull request](http://help.github.com/send-pull-requests/) on github
|
||||
6. Rebase [master branch](https://github.com/danikula/AndroidVideoCache) if your local branch is not actual. Merging is not acceptable, only rebase
|
||||
6. Your pull request will be reviewed and hopefully merged :)
|
||||
|
||||
## Where published?
|
||||
[Here](https://bintray.com/alexeydanilov/maven/videocache/view)
|
||||
@@ -106,7 +136,7 @@ If it's a feature that you think would need to be discussed please open an issue
|
||||
|
||||
## License
|
||||
|
||||
Copyright 2014-2015 Alexey Danilov
|
||||
Copyright 2014-2016 Alexey Danilov
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -3,7 +3,7 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:1.3.0'
|
||||
classpath 'com.android.tools.build:gradle:2.1.2'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
BIN
files/android
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
files/android.jpg
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
files/devbytes.mp4
Normal file
0
files/empty.txt
Normal file
BIN
files/orange1.mp4
Normal file
BIN
files/orange2.mp4
Normal file
BIN
files/orange3.mp4
Normal file
BIN
files/orange4.mp4
Normal file
BIN
files/orange5.mp4
Normal file
BIN
files/phones.jpg
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
files/space.jpg
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-2.12-all.zip
|
||||
|
||||
@@ -3,7 +3,7 @@ buildscript {
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.novoda:bintray-release:0.2.10'
|
||||
classpath 'com.novoda:bintray-release:0.3.4'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,9 @@ idea {
|
||||
}
|
||||
}
|
||||
|
||||
targetCompatibility = '1.7'
|
||||
sourceCompatibility = '1.7'
|
||||
|
||||
dependencies {
|
||||
compile 'com.google.android:android:1.6_r2'
|
||||
}
|
||||
@@ -26,7 +29,7 @@ publish {
|
||||
userOrg = 'alexeydanilov'
|
||||
groupId = 'com.danikula'
|
||||
artifactId = 'videocache'
|
||||
publishVersion = '2.3.2'
|
||||
publishVersion = '2.6.0'
|
||||
description = 'Cache support for android VideoView'
|
||||
website = 'https://github.com/danikula/AndroidVideoCache'
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.danikula.videocache;
|
||||
|
||||
import com.danikula.videocache.file.DiskUsage;
|
||||
import com.danikula.videocache.file.FileNameGenerator;
|
||||
import com.danikula.videocache.sourcestorage.SourceInfoStorage;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
@@ -15,11 +16,13 @@ class Config {
|
||||
public final File cacheRoot;
|
||||
public final FileNameGenerator fileNameGenerator;
|
||||
public final DiskUsage diskUsage;
|
||||
public final SourceInfoStorage sourceInfoStorage;
|
||||
|
||||
Config(File cacheRoot, FileNameGenerator fileNameGenerator, DiskUsage diskUsage) {
|
||||
Config(File cacheRoot, FileNameGenerator fileNameGenerator, DiskUsage diskUsage, SourceInfoStorage sourceInfoStorage) {
|
||||
this.cacheRoot = cacheRoot;
|
||||
this.fileNameGenerator = fileNameGenerator;
|
||||
this.diskUsage = diskUsage;
|
||||
this.sourceInfoStorage = sourceInfoStorage;
|
||||
}
|
||||
|
||||
File generateCacheFile(String url) {
|
||||
|
||||
@@ -83,25 +83,25 @@ class HttpProxyCache extends ProxyCache {
|
||||
}
|
||||
|
||||
private void responseWithoutCache(OutputStream out, long offset) throws ProxyCacheException, IOException {
|
||||
HttpUrlSource newSourceNoCache = new HttpUrlSource(this.source);
|
||||
try {
|
||||
HttpUrlSource source = new HttpUrlSource(this.source);
|
||||
source.open((int) offset);
|
||||
newSourceNoCache.open((int) offset);
|
||||
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
|
||||
int readBytes;
|
||||
while ((readBytes = source.read(buffer)) != -1) {
|
||||
while ((readBytes = newSourceNoCache.read(buffer)) != -1) {
|
||||
out.write(buffer, 0, readBytes);
|
||||
offset += readBytes;
|
||||
}
|
||||
out.flush();
|
||||
} finally {
|
||||
source.close();
|
||||
newSourceNoCache.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCachePercentsAvailableChanged(int percents) {
|
||||
if (listener != null) {
|
||||
listener.onCacheAvailable(cache.file, source.url, percents);
|
||||
listener.onCacheAvailable(cache.file, source.getUrl(), percents);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ 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 com.danikula.videocache.sourcestorage.SourceInfoStorage;
|
||||
import com.danikula.videocache.sourcestorage.SourceInfoStorageFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -91,7 +93,7 @@ public class HttpProxyCacheServer {
|
||||
|
||||
private void makeSureServerWorks() {
|
||||
int maxPingAttempts = 3;
|
||||
int delay = 200;
|
||||
int delay = 300;
|
||||
int pingAttempts = 0;
|
||||
while (pingAttempts < maxPingAttempts) {
|
||||
try {
|
||||
@@ -107,7 +109,7 @@ public class HttpProxyCacheServer {
|
||||
pingAttempts++;
|
||||
delay *= 2;
|
||||
}
|
||||
Log.e(LOG_TAG, "Shutdown server… Error pinging server [attempt: " + pingAttempts + ", timeout: " + delay + "]. " +
|
||||
Log.e(LOG_TAG, "Shutdown server… Error pinging server [attempts: " + pingAttempts + ", max timeout: " + delay / 2 + "]. " +
|
||||
"If you see this message, please, email me danikula@gmail.com");
|
||||
shutdown();
|
||||
}
|
||||
@@ -173,11 +175,27 @@ public class HttpProxyCacheServer {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks is cache contains fully cached file for particular url.
|
||||
*
|
||||
* @param url an url cache file will be checked for.
|
||||
* @return {@code true} if cache contains fully cached file for passed in parameters url.
|
||||
*/
|
||||
public boolean isCached(String url) {
|
||||
checkNotNull(url, "Url can't be null!");
|
||||
File cacheDir = config.cacheRoot;
|
||||
String fileName = config.fileNameGenerator.generate(url);
|
||||
File cacheFile = new File(cacheDir, fileName);
|
||||
return cacheFile.exists();
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
Log.i(LOG_TAG, "Shutdown proxy server");
|
||||
|
||||
shutdownClients();
|
||||
|
||||
config.sourceInfoStorage.release();
|
||||
|
||||
waitConnectionThread.interrupt();
|
||||
try {
|
||||
if (!serverSocket.isClosed()) {
|
||||
@@ -345,13 +363,15 @@ public class HttpProxyCacheServer {
|
||||
*/
|
||||
public static final class Builder {
|
||||
|
||||
private static final long DEFAULT_MAX_SIZE = 512 * 104 * 1024;
|
||||
private static final long DEFAULT_MAX_SIZE = 512 * 1024 * 1024;
|
||||
|
||||
private File cacheRoot;
|
||||
private FileNameGenerator fileNameGenerator;
|
||||
private DiskUsage diskUsage;
|
||||
private SourceInfoStorage sourceInfoStorage;
|
||||
|
||||
public Builder(Context context) {
|
||||
this.sourceInfoStorage = SourceInfoStorageFactory.newSourceInfoStorage(context);
|
||||
this.cacheRoot = StorageUtils.getIndividualCacheDirectory(context);
|
||||
this.diskUsage = new TotalSizeLruDiskUsage(DEFAULT_MAX_SIZE);
|
||||
this.fileNameGenerator = new Md5FileNameGenerator();
|
||||
@@ -425,7 +445,7 @@ public class HttpProxyCacheServer {
|
||||
}
|
||||
|
||||
private Config buildConfig() {
|
||||
return new Config(cacheRoot, fileNameGenerator, diskUsage);
|
||||
return new Config(cacheRoot, fileNameGenerator, diskUsage, sourceInfoStorage);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ final class HttpProxyCacheServerClients {
|
||||
}
|
||||
|
||||
private HttpProxyCache newHttpProxyCache() throws ProxyCacheException {
|
||||
HttpUrlSource source = new HttpUrlSource(url);
|
||||
HttpUrlSource source = new HttpUrlSource(url, config.sourceInfoStorage);
|
||||
FileCache cache = new FileCache(config.generateCacheFile(url), config.diskUsage);
|
||||
HttpProxyCache httpProxyCache = new HttpProxyCache(source, cache);
|
||||
httpProxyCache.registerCacheListener(uiCacheListener);
|
||||
|
||||
@@ -3,6 +3,9 @@ package com.danikula.videocache;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import com.danikula.videocache.sourcestorage.SourceInfoStorage;
|
||||
import com.danikula.videocache.sourcestorage.SourceInfoStorageFactory;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
@@ -10,6 +13,7 @@ import java.io.InterruptedIOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
|
||||
import static com.danikula.videocache.Preconditions.checkNotNull;
|
||||
import static com.danikula.videocache.ProxyCacheUtils.DEFAULT_BUFFER_SIZE;
|
||||
import static com.danikula.videocache.ProxyCacheUtils.LOG_TAG;
|
||||
import static java.net.HttpURLConnection.HTTP_MOVED_PERM;
|
||||
@@ -26,86 +30,97 @@ import static java.net.HttpURLConnection.HTTP_SEE_OTHER;
|
||||
public class HttpUrlSource implements Source {
|
||||
|
||||
private static final int MAX_REDIRECTS = 5;
|
||||
public final String url;
|
||||
private final SourceInfoStorage sourceInfoStorage;
|
||||
private SourceInfo sourceInfo;
|
||||
private HttpURLConnection connection;
|
||||
private InputStream inputStream;
|
||||
private volatile int length = Integer.MIN_VALUE;
|
||||
private volatile String mime;
|
||||
|
||||
public HttpUrlSource(String url) {
|
||||
this(url, ProxyCacheUtils.getSupposablyMime(url));
|
||||
this(url, SourceInfoStorageFactory.newEmptySourceInfoStorage());
|
||||
}
|
||||
|
||||
public HttpUrlSource(String url, String mime) {
|
||||
this.url = Preconditions.checkNotNull(url);
|
||||
this.mime = mime;
|
||||
public HttpUrlSource(String url, SourceInfoStorage sourceInfoStorage) {
|
||||
this.sourceInfoStorage = checkNotNull(sourceInfoStorage);
|
||||
SourceInfo sourceInfo = sourceInfoStorage.get(url);
|
||||
this.sourceInfo = sourceInfo != null ? sourceInfo :
|
||||
new SourceInfo(url, Integer.MIN_VALUE, ProxyCacheUtils.getSupposablyMime(url));
|
||||
}
|
||||
|
||||
public HttpUrlSource(HttpUrlSource source) {
|
||||
this.url = source.url;
|
||||
this.mime = source.mime;
|
||||
this.length = source.length;
|
||||
this.sourceInfo = source.sourceInfo;
|
||||
this.sourceInfoStorage = source.sourceInfoStorage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized int length() throws ProxyCacheException {
|
||||
if (length == Integer.MIN_VALUE) {
|
||||
if (sourceInfo.length == Integer.MIN_VALUE) {
|
||||
fetchContentInfo();
|
||||
}
|
||||
return length;
|
||||
return sourceInfo.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void open(int offset) throws ProxyCacheException {
|
||||
try {
|
||||
connection = openConnection(offset, "GET", -1);
|
||||
mime = connection.getContentType();
|
||||
connection = openConnection(offset, -1);
|
||||
String mime = connection.getContentType();
|
||||
inputStream = new BufferedInputStream(connection.getInputStream(), DEFAULT_BUFFER_SIZE);
|
||||
length = readSourceAvailableBytes(connection, offset, connection.getResponseCode());
|
||||
int length = readSourceAvailableBytes(connection, offset, connection.getResponseCode());
|
||||
this.sourceInfo = new SourceInfo(sourceInfo.url, length, mime);
|
||||
this.sourceInfoStorage.put(sourceInfo.url, sourceInfo);
|
||||
} catch (IOException e) {
|
||||
throw new ProxyCacheException("Error opening connection for " + url + " with offset " + offset, e);
|
||||
throw new ProxyCacheException("Error opening connection for " + sourceInfo.url + " with offset " + offset, e);
|
||||
}
|
||||
}
|
||||
|
||||
private int readSourceAvailableBytes(HttpURLConnection connection, int offset, int responseCode) throws IOException {
|
||||
int contentLength = connection.getContentLength();
|
||||
return responseCode == HTTP_OK ? contentLength
|
||||
: responseCode == HTTP_PARTIAL ? contentLength + offset : length;
|
||||
: responseCode == HTTP_PARTIAL ? contentLength + offset : sourceInfo.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws ProxyCacheException {
|
||||
if (connection != null) {
|
||||
connection.disconnect();
|
||||
try {
|
||||
connection.disconnect();
|
||||
} catch (NullPointerException | IllegalArgumentException e) {
|
||||
String message = "Wait... but why? WTF!? " +
|
||||
"Really shouldn't happen any more after fixing https://github.com/danikula/AndroidVideoCache/issues/43. " +
|
||||
"If you read it on your device log, please, notify me danikula@gmail.com or create issue here https://github.com/danikula/AndroidVideoCache/issues.";
|
||||
throw new RuntimeException(message, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] buffer) throws ProxyCacheException {
|
||||
if (inputStream == null) {
|
||||
throw new ProxyCacheException("Error reading data from " + url + ": connection is absent!");
|
||||
throw new ProxyCacheException("Error reading data from " + sourceInfo.url + ": connection is absent!");
|
||||
}
|
||||
try {
|
||||
return inputStream.read(buffer, 0, buffer.length);
|
||||
} catch (InterruptedIOException e) {
|
||||
throw new InterruptedProxyCacheException("Reading source " + url + " is interrupted", e);
|
||||
throw new InterruptedProxyCacheException("Reading source " + sourceInfo.url + " is interrupted", e);
|
||||
} catch (IOException e) {
|
||||
throw new ProxyCacheException("Error reading data from " + url, e);
|
||||
throw new ProxyCacheException("Error reading data from " + sourceInfo.url, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void fetchContentInfo() throws ProxyCacheException {
|
||||
Log.d(LOG_TAG, "Read content info from " + url);
|
||||
Log.d(LOG_TAG, "Read content info from " + sourceInfo.url);
|
||||
HttpURLConnection urlConnection = null;
|
||||
InputStream inputStream = null;
|
||||
try {
|
||||
urlConnection = openConnection(0, "HEAD", 10000);
|
||||
length = urlConnection.getContentLength();
|
||||
mime = urlConnection.getContentType();
|
||||
urlConnection = openConnection(0, 10000);
|
||||
int length = urlConnection.getContentLength();
|
||||
String mime = urlConnection.getContentType();
|
||||
inputStream = urlConnection.getInputStream();
|
||||
Log.i(LOG_TAG, "Content info for `" + url + "`: mime: " + mime + ", content-length: " + length);
|
||||
this.sourceInfo = new SourceInfo(sourceInfo.url, length, mime);
|
||||
this.sourceInfoStorage.put(sourceInfo.url, sourceInfo);
|
||||
Log.i(LOG_TAG, "Source info fetched: " + sourceInfo);
|
||||
} catch (IOException e) {
|
||||
Log.e(LOG_TAG, "Error fetching info from " + url, e);
|
||||
Log.e(LOG_TAG, "Error fetching info from " + sourceInfo.url, e);
|
||||
} finally {
|
||||
ProxyCacheUtils.close(inputStream);
|
||||
if (urlConnection != null) {
|
||||
@@ -114,15 +129,14 @@ public class HttpUrlSource implements Source {
|
||||
}
|
||||
}
|
||||
|
||||
private HttpURLConnection openConnection(int offset, String method, int timeout) throws IOException, ProxyCacheException {
|
||||
private HttpURLConnection openConnection(int offset, int timeout) throws IOException, ProxyCacheException {
|
||||
HttpURLConnection connection;
|
||||
boolean redirected;
|
||||
int redirectCount = 0;
|
||||
String url = this.url;
|
||||
String url = this.sourceInfo.url;
|
||||
do {
|
||||
Log.d(LOG_TAG, "Open connection " + (offset > 0 ? " with offset " + offset : "") + " to " + url);
|
||||
connection = (HttpURLConnection) new URL(url).openConnection();
|
||||
connection.setRequestMethod(method);
|
||||
if (offset > 0) {
|
||||
connection.setRequestProperty("Range", "bytes=" + offset + "-");
|
||||
}
|
||||
@@ -145,18 +159,18 @@ public class HttpUrlSource implements Source {
|
||||
}
|
||||
|
||||
public synchronized String getMime() throws ProxyCacheException {
|
||||
if (TextUtils.isEmpty(mime)) {
|
||||
if (TextUtils.isEmpty(sourceInfo.mime)) {
|
||||
fetchContentInfo();
|
||||
}
|
||||
return mime;
|
||||
return sourceInfo.mime;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
return sourceInfo.url;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "HttpUrlSource{url='" + url + "}";
|
||||
return "HttpUrlSource{sourceInfo='" + sourceInfo + "}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
package com.danikula.videocache;
|
||||
|
||||
final class Preconditions {
|
||||
public final class Preconditions {
|
||||
|
||||
static <T> T checkNotNull(T reference) {
|
||||
public static <T> T checkNotNull(T reference) {
|
||||
if (reference == null) {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
return reference;
|
||||
}
|
||||
|
||||
static void checkAllNotNull(Object... references) {
|
||||
public static void checkAllNotNull(Object... references) {
|
||||
for (Object reference : references) {
|
||||
if (reference == null) {
|
||||
throw new NullPointerException();
|
||||
@@ -17,7 +17,7 @@ final class Preconditions {
|
||||
}
|
||||
}
|
||||
|
||||
static <T> T checkNotNull(T reference, String errorMessage) {
|
||||
public static <T> T checkNotNull(T reference, String errorMessage) {
|
||||
if (reference == null) {
|
||||
throw new NullPointerException(errorMessage);
|
||||
}
|
||||
|
||||
@@ -100,10 +100,11 @@ class ProxyCache {
|
||||
}
|
||||
}
|
||||
|
||||
protected void onCacheAvailable(long cacheAvailable, long sourceAvailable) {
|
||||
int percents = (int) (cacheAvailable * 100 / sourceAvailable);
|
||||
protected void onCacheAvailable(long cacheAvailable, long sourceLength) {
|
||||
boolean zeroLengthSource = sourceLength == 0;
|
||||
int percents = zeroLengthSource ? 100 : (int) (cacheAvailable * 100 / sourceLength);
|
||||
boolean percentsChanged = percents != percentsAvailable;
|
||||
boolean sourceLengthKnown = sourceAvailable >= 0;
|
||||
boolean sourceLengthKnown = sourceLength >= 0;
|
||||
if (sourceLengthKnown && percentsChanged) {
|
||||
onCachePercentsAvailableChanged(percents);
|
||||
}
|
||||
@@ -133,6 +134,7 @@ class ProxyCache {
|
||||
notifyNewCacheDataAvailable(offset, sourceAvailable);
|
||||
}
|
||||
tryComplete();
|
||||
onSourceRead();
|
||||
} catch (Throwable e) {
|
||||
readSourceErrorsCount.incrementAndGet();
|
||||
onError(e);
|
||||
@@ -142,6 +144,12 @@ class ProxyCache {
|
||||
}
|
||||
}
|
||||
|
||||
private void onSourceRead() {
|
||||
// guaranteed notify listeners after source read and cache completed
|
||||
percentsAvailable = 100;
|
||||
onCachePercentsAvailableChanged(percentsAvailable);
|
||||
}
|
||||
|
||||
private void tryComplete() throws ProxyCacheException {
|
||||
synchronized (stopLock) {
|
||||
if (!isStopped() && cache.available() == source.length()) {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.danikula.videocache;
|
||||
|
||||
/**
|
||||
* Stores source's info.
|
||||
*
|
||||
* @author Alexey Danilov (danikula@gmail.com).
|
||||
*/
|
||||
public class SourceInfo {
|
||||
|
||||
public final String url;
|
||||
public final int length;
|
||||
public final String mime;
|
||||
|
||||
public SourceInfo(String url, int length, String mime) {
|
||||
this.url = url;
|
||||
this.length = length;
|
||||
this.mime = mime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SourceInfo{" +
|
||||
"url='" + url + '\'' +
|
||||
", length=" + length +
|
||||
", mime='" + mime + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.danikula.videocache.sourcestorage;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
|
||||
import com.danikula.videocache.SourceInfo;
|
||||
|
||||
import static com.danikula.videocache.Preconditions.checkAllNotNull;
|
||||
import static com.danikula.videocache.Preconditions.checkNotNull;
|
||||
|
||||
/**
|
||||
* Database based {@link SourceInfoStorage}.
|
||||
*
|
||||
* @author Alexey Danilov (danikula@gmail.com).
|
||||
*/
|
||||
class DatabaseSourceInfoStorage extends SQLiteOpenHelper implements SourceInfoStorage {
|
||||
|
||||
private static final String TABLE = "SourceInfo";
|
||||
private static final String COLUMN_ID = "_id";
|
||||
private static final String COLUMN_URL = "url";
|
||||
private static final String COLUMN_LENGTH = "length";
|
||||
private static final String COLUMN_MIME = "mime";
|
||||
private static final String[] ALL_COLUMNS = new String[]{COLUMN_ID, COLUMN_URL, COLUMN_LENGTH, COLUMN_MIME};
|
||||
private static final String CREATE_SQL =
|
||||
"CREATE TABLE " + TABLE + " (" +
|
||||
COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," +
|
||||
COLUMN_URL + " TEXT NOT NULL," +
|
||||
COLUMN_MIME + " TEXT," +
|
||||
COLUMN_LENGTH + " INTEGER" +
|
||||
");";
|
||||
|
||||
DatabaseSourceInfoStorage(Context context) {
|
||||
super(context, "AndroidVideoCache.db", null, 1);
|
||||
checkNotNull(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
checkNotNull(db);
|
||||
db.execSQL(CREATE_SQL);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
throw new IllegalStateException("Should not be called. There is no any migration");
|
||||
}
|
||||
|
||||
@Override
|
||||
public SourceInfo get(String url) {
|
||||
checkNotNull(url);
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
cursor = getReadableDatabase().query(TABLE, ALL_COLUMNS, COLUMN_URL + "=?", new String[]{url}, null, null, null);
|
||||
return cursor == null || !cursor.moveToFirst() ? null : convert(cursor);
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void put(String url, SourceInfo sourceInfo) {
|
||||
checkAllNotNull(url, sourceInfo);
|
||||
SourceInfo sourceInfoFromDb = get(url);
|
||||
boolean exist = sourceInfoFromDb != null;
|
||||
ContentValues contentValues = convert(sourceInfo);
|
||||
if (exist) {
|
||||
getWritableDatabase().update(TABLE, contentValues, COLUMN_URL + "=?", new String[]{url});
|
||||
} else {
|
||||
getWritableDatabase().insert(TABLE, null, contentValues);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
close();
|
||||
}
|
||||
|
||||
private SourceInfo convert(Cursor cursor) {
|
||||
return new SourceInfo(
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_URL)),
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(COLUMN_LENGTH)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_MIME))
|
||||
);
|
||||
}
|
||||
|
||||
private ContentValues convert(SourceInfo sourceInfo) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(COLUMN_URL, sourceInfo.url);
|
||||
values.put(COLUMN_LENGTH, sourceInfo.length);
|
||||
values.put(COLUMN_MIME, sourceInfo.mime);
|
||||
return values;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.danikula.videocache.sourcestorage;
|
||||
|
||||
import com.danikula.videocache.SourceInfo;
|
||||
|
||||
/**
|
||||
* {@link SourceInfoStorage} that does nothing.
|
||||
*
|
||||
* @author Alexey Danilov (danikula@gmail.com).
|
||||
*/
|
||||
public class NoSourceInfoStorage implements SourceInfoStorage {
|
||||
|
||||
@Override
|
||||
public SourceInfo get(String url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void put(String url, SourceInfo sourceInfo) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.danikula.videocache.sourcestorage;
|
||||
|
||||
import com.danikula.videocache.SourceInfo;
|
||||
|
||||
/**
|
||||
* Storage for {@link SourceInfo}.
|
||||
*
|
||||
* @author Alexey Danilov (danikula@gmail.com).
|
||||
*/
|
||||
public interface SourceInfoStorage {
|
||||
|
||||
SourceInfo get(String url);
|
||||
|
||||
void put(String url, SourceInfo sourceInfo);
|
||||
|
||||
void release();
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.danikula.videocache.sourcestorage;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
/**
|
||||
* Simple factory for {@link SourceInfoStorage}.
|
||||
*
|
||||
* @author Alexey Danilov (danikula@gmail.com).
|
||||
*/
|
||||
public class SourceInfoStorageFactory {
|
||||
|
||||
public static SourceInfoStorage newSourceInfoStorage(Context context) {
|
||||
return new DatabaseSourceInfoStorage(context);
|
||||
}
|
||||
|
||||
public static SourceInfoStorage newEmptySourceInfoStorage() {
|
||||
return new NoSourceInfoStorage();
|
||||
}
|
||||
}
|
||||
@@ -16,10 +16,10 @@ apply plugin: 'com.neenbedankt.android-apt'
|
||||
|
||||
android {
|
||||
compileSdkVersion 23
|
||||
buildToolsVersion '23.0.1'
|
||||
buildToolsVersion '24'
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.danikula.videocache.sample"
|
||||
applicationId 'com.danikula.videocache.sample'
|
||||
minSdkVersion 15
|
||||
targetSdkVersion 23
|
||||
versionCode 1
|
||||
@@ -38,7 +38,7 @@ dependencies {
|
||||
// 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.3.2'
|
||||
compile 'com.danikula:videocache:2.6.0'
|
||||
compile 'com.viewpagerindicator:library:2.4.2-SNAPSHOT@aar'
|
||||
apt 'org.androidannotations:androidannotations:3.3.2'
|
||||
}
|
||||
|
||||
@@ -6,11 +6,11 @@ import java.io.File;
|
||||
|
||||
public enum Video {
|
||||
|
||||
ORANGE_1("https://dl.dropboxusercontent.com/u/15506779/persistent/proxycache/orange1.mp4"),
|
||||
ORANGE_2("https://dl.dropboxusercontent.com/u/15506779/persistent/proxycache/orange2.mp4"),
|
||||
ORANGE_3("https://dl.dropboxusercontent.com/u/15506779/persistent/proxycache/orange3.mp4"),
|
||||
ORANGE_4("https://dl.dropboxusercontent.com/u/15506779/persistent/proxycache/orange4.mp4"),
|
||||
ORANGE_5("https://dl.dropboxusercontent.com/u/15506779/persistent/proxycache/orange5.mp4");
|
||||
ORANGE_1(Config.ROOT + "orange1.mp4"),
|
||||
ORANGE_2(Config.ROOT + "orange2.mp4"),
|
||||
ORANGE_3(Config.ROOT + "orange3.mp4"),
|
||||
ORANGE_4(Config.ROOT + "orange4.mp4"),
|
||||
ORANGE_5(Config.ROOT + "orange5.mp4");
|
||||
|
||||
public final String url;
|
||||
|
||||
@@ -21,4 +21,8 @@ public enum Video {
|
||||
public File getCacheFile(Context context) {
|
||||
return new File(context.getExternalCacheDir(), name());
|
||||
}
|
||||
|
||||
private class Config {
|
||||
private static final String ROOT = "https://raw.githubusercontent.com/danikula/AndroidVideoCache/master/files/";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.util.Log;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.VideoView;
|
||||
|
||||
@@ -27,6 +28,7 @@ public class VideoFragment extends Fragment implements CacheListener {
|
||||
@FragmentArg String url;
|
||||
@FragmentArg String cachePath;
|
||||
|
||||
@ViewById ImageView cacheStatusImageView;
|
||||
@ViewById VideoView videoView;
|
||||
@ViewById ProgressBar progressBar;
|
||||
|
||||
@@ -45,9 +47,16 @@ public class VideoFragment extends Fragment implements CacheListener {
|
||||
|
||||
@AfterViews
|
||||
void afterViewInjected() {
|
||||
checkCachedState();
|
||||
startVideo();
|
||||
}
|
||||
|
||||
private void checkCachedState() {
|
||||
HttpProxyCacheServer proxy = App.getProxy(getActivity());
|
||||
boolean fullyCached = proxy.isCached(url);
|
||||
setCachedState(fullyCached);
|
||||
}
|
||||
|
||||
private void startVideo() {
|
||||
HttpProxyCacheServer proxy = App.getProxy(getActivity());
|
||||
proxy.registerCacheListener(this, url);
|
||||
@@ -78,6 +87,7 @@ public class VideoFragment extends Fragment implements CacheListener {
|
||||
@Override
|
||||
public void onCacheAvailable(File file, String url, int percentsAvailable) {
|
||||
progressBar.setSecondaryProgress(percentsAvailable);
|
||||
setCachedState(percentsAvailable == 100);
|
||||
Log.d(LOG_TAG, String.format("onCacheAvailable. percents: %d, file: %s, url: %s", percentsAvailable, file, url));
|
||||
}
|
||||
|
||||
@@ -92,6 +102,11 @@ public class VideoFragment extends Fragment implements CacheListener {
|
||||
videoView.seekTo(videoPosition);
|
||||
}
|
||||
|
||||
private void setCachedState(boolean cached) {
|
||||
int statusIconId = cached ? R.drawable.ic_cloud_done : R.drawable.ic_cloud_download;
|
||||
cacheStatusImageView.setImageResource(statusIconId);
|
||||
}
|
||||
|
||||
private final class VideoProgressUpdater extends Handler {
|
||||
|
||||
public void start() {
|
||||
|
||||
BIN
sample/src/main/res/drawable-hdpi/ic_cloud_done.png
Normal file
|
After Width: | Height: | Size: 379 B |
BIN
sample/src/main/res/drawable-hdpi/ic_cloud_download.png
Normal file
|
After Width: | Height: | Size: 353 B |
BIN
sample/src/main/res/drawable-mdpi/ic_cloud_done.png
Normal file
|
After Width: | Height: | Size: 265 B |
BIN
sample/src/main/res/drawable-mdpi/ic_cloud_download.png
Normal file
|
After Width: | Height: | Size: 242 B |
BIN
sample/src/main/res/drawable-xhdpi/ic_cloud_done.png
Normal file
|
After Width: | Height: | Size: 449 B |
BIN
sample/src/main/res/drawable-xhdpi/ic_cloud_download.png
Normal file
|
After Width: | Height: | Size: 417 B |
BIN
sample/src/main/res/drawable-xxhdpi/ic_cloud_done.png
Normal file
|
After Width: | Height: | Size: 672 B |
BIN
sample/src/main/res/drawable-xxhdpi/ic_cloud_download.png
Normal file
|
After Width: | Height: | Size: 610 B |
BIN
sample/src/main/res/drawable-xxxhdpi/ic_cloud_done.png
Normal file
|
After Width: | Height: | Size: 855 B |
BIN
sample/src/main/res/drawable-xxxhdpi/ic_cloud_download.png
Normal file
|
After Width: | Height: | Size: 789 B |
@@ -10,6 +10,13 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/cacheStatusImageView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:src="@drawable/ic_cloud_download" />
|
||||
|
||||
<SeekBar
|
||||
android:id="@+id/progressBar"
|
||||
style="@android:style/Widget.Holo.ProgressBar.Horizontal"
|
||||
|
||||
@@ -15,14 +15,6 @@ android {
|
||||
versionCode 1
|
||||
versionName '0.1'
|
||||
}
|
||||
buildTypes {
|
||||
debug {
|
||||
buildConfigField "int", "MIN_SDK_VERSION", Integer.toString(android.defaultConfig.minSdkVersion.apiLevel)
|
||||
}
|
||||
release {
|
||||
buildConfigField "int", "MIN_SDK_VERSION", Integer.toString(android.defaultConfig.minSdkVersion.apiLevel)
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_7
|
||||
targetCompatibility JavaVersion.VERSION_1_7
|
||||
@@ -30,13 +22,10 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile project (':library')
|
||||
compile project(':library')
|
||||
|
||||
testCompile 'junit:junit:4.12'
|
||||
testCompile('org.robolectric:robolectric:3.0-rc2') {
|
||||
exclude group: 'commons-logging', module: 'commons-logging'
|
||||
exclude group: 'org.apache.httpcomponents', module: 'httpclient'
|
||||
}
|
||||
testCompile 'org.robolectric:robolectric:3.1'
|
||||
testCompile 'com.squareup:fest-android:1.0.0'
|
||||
testCompile 'com.google.guava:guava-jdk5:17.0'
|
||||
testCompile('com.danikula:android-garden:2.1.4') {
|
||||
|
||||
BIN
test/src/main/assets/space.jpg
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
@@ -20,7 +20,7 @@ import static org.fest.assertions.api.Assertions.fail;
|
||||
* @author Alexey Danilov (danikula@gmail.com).
|
||||
*/
|
||||
@RunWith(RobolectricGradleTestRunner.class)
|
||||
@Config(constants = BuildConfig.class, emulateSdk = BuildConfig.MIN_SDK_VERSION)
|
||||
@Config(constants = BuildConfig.class)
|
||||
public class FileNameGeneratorTest {
|
||||
|
||||
@Test
|
||||
|
||||
@@ -17,7 +17,7 @@ import static org.fest.assertions.api.Assertions.fail;
|
||||
* @author Alexey Danilov (danikula@gmail.com).
|
||||
*/
|
||||
@RunWith(RobolectricGradleTestRunner.class)
|
||||
@Config(constants = BuildConfig.class, emulateSdk = BuildConfig.MIN_SDK_VERSION)
|
||||
@Config(constants = BuildConfig.class)
|
||||
public class GetRequestTest {
|
||||
|
||||
@Test
|
||||
|
||||
@@ -41,7 +41,7 @@ import static org.fest.assertions.api.Assertions.assertThat;
|
||||
* @author Alexey Danilov (danikula@gmail.com).
|
||||
*/
|
||||
@RunWith(RobolectricGradleTestRunner.class)
|
||||
@Config(constants = BuildConfig.class, emulateSdk = BuildConfig.MIN_SDK_VERSION)
|
||||
@Config(constants = BuildConfig.class)
|
||||
public class HttpProxyCacheServerTest {
|
||||
|
||||
private File cacheFolder;
|
||||
@@ -82,7 +82,7 @@ public class HttpProxyCacheServerTest {
|
||||
|
||||
@Test
|
||||
public void testMimeFromResponse() throws Exception {
|
||||
Pair<File, Response> response = readProxyData("https://dl.dropboxusercontent.com/u/15506779/persistent/proxycache/android");
|
||||
Pair<File, Response> response = readProxyData("https://raw.githubusercontent.com/danikula/AndroidVideoCache/master/files/android");
|
||||
assertThat(response.second.contentType).isEqualTo("application/octet-stream");
|
||||
}
|
||||
|
||||
@@ -210,10 +210,55 @@ public class HttpProxyCacheServerTest {
|
||||
assertThat(file(cacheFolder, HTTP_DATA_URL_6_REDIRECTS)).exists();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCheckFileExistForNotCachedUrl() throws Exception {
|
||||
HttpProxyCacheServer proxy = newProxy(cacheFolder);
|
||||
proxy.shutdown();
|
||||
assertThat(proxy.isCached(HTTP_DATA_URL)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCheckFileExistForFullyCachedUrl() throws Exception {
|
||||
HttpProxyCacheServer proxy = newProxy(cacheFolder);
|
||||
readProxyResponse(proxy, HTTP_DATA_URL, 0);
|
||||
proxy.shutdown();
|
||||
|
||||
assertThat(proxy.isCached(HTTP_DATA_URL)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCheckFileExistForPartiallyCachedUrl() throws Exception {
|
||||
File cacheDir = RuntimeEnvironment.application.getExternalCacheDir();
|
||||
File file = file(cacheDir, HTTP_DATA_URL);
|
||||
int partialCacheSize = 1000;
|
||||
byte[] partialData = ProxyCacheTestUtils.generate(partialCacheSize);
|
||||
File partialCacheFile = ProxyCacheTestUtils.getTempFile(file);
|
||||
IoUtils.saveToFile(partialData, partialCacheFile);
|
||||
|
||||
HttpProxyCacheServer proxy = newProxy(cacheDir);
|
||||
assertThat(proxy.isCached(HTTP_DATA_URL)).isFalse();
|
||||
|
||||
readProxyResponse(proxy, HTTP_DATA_URL);
|
||||
proxy.shutdown();
|
||||
|
||||
assertThat(proxy.isCached(HTTP_DATA_URL)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCheckFileExistForDeletedCacheFile() throws Exception {
|
||||
HttpProxyCacheServer proxy = newProxy(cacheFolder);
|
||||
readProxyResponse(proxy, HTTP_DATA_URL, 0);
|
||||
proxy.shutdown();
|
||||
File cacheFile = file(cacheFolder, HTTP_DATA_URL);
|
||||
boolean deleted = cacheFile.delete();
|
||||
|
||||
assertThat(deleted).isTrue();
|
||||
assertThat(proxy.isCached(HTTP_DATA_URL)).isFalse();
|
||||
}
|
||||
|
||||
private Pair<File, Response> readProxyData(String url, int offset) throws IOException {
|
||||
File externalCacheDir = RuntimeEnvironment.application.getExternalCacheDir();
|
||||
File file = file(externalCacheDir, url);
|
||||
HttpProxyCacheServer proxy = newProxy(externalCacheDir);
|
||||
File file = file(cacheFolder, url);
|
||||
HttpProxyCacheServer proxy = newProxy(cacheFolder);
|
||||
|
||||
Response response = readProxyResponse(proxy, url, offset);
|
||||
proxy.shutdown();
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package com.danikula.videocache;
|
||||
|
||||
import com.danikula.android.garden.io.IoUtils;
|
||||
import com.danikula.videocache.file.FileCache;
|
||||
import com.danikula.videocache.sourcestorage.SourceInfoStorage;
|
||||
import com.danikula.videocache.sourcestorage.SourceInfoStorageFactory;
|
||||
import com.danikula.videocache.support.ProxyCacheTestUtils;
|
||||
import com.danikula.videocache.support.Response;
|
||||
import com.danikula.videocache.test.BuildConfig;
|
||||
@@ -9,17 +12,34 @@ import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mockito;
|
||||
import org.robolectric.RobolectricGradleTestRunner;
|
||||
import org.robolectric.RuntimeEnvironment;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.Socket;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
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_URL;
|
||||
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.loadAssetFile;
|
||||
import static com.danikula.videocache.support.ProxyCacheTestUtils.loadTestData;
|
||||
import static com.danikula.videocache.support.ProxyCacheTestUtils.newCacheFile;
|
||||
import static org.fest.assertions.api.Assertions.assertThat;
|
||||
import static org.fest.assertions.api.Assertions.fail;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.anyInt;
|
||||
import static org.mockito.Matchers.anyLong;
|
||||
import static org.mockito.Matchers.eq;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -30,42 +50,27 @@ import static org.mockito.Mockito.when;
|
||||
* @author Alexey Danilov (danikula@gmail.com).
|
||||
*/
|
||||
@RunWith(RobolectricGradleTestRunner.class)
|
||||
@Config(constants = BuildConfig.class, emulateSdk = BuildConfig.MIN_SDK_VERSION)
|
||||
@Config(constants = BuildConfig.class)
|
||||
public class HttpProxyCacheTest {
|
||||
|
||||
@Test
|
||||
public void testProcessRequestNoCache() throws Exception {
|
||||
HttpUrlSource source = new HttpUrlSource(HTTP_DATA_URL);
|
||||
FileCache cache = new FileCache(ProxyCacheTestUtils.newCacheFile());
|
||||
HttpProxyCache proxyCache = new HttpProxyCache(source, cache);
|
||||
GetRequest request = new GetRequest("GET /" + HTTP_DATA_URL + " HTTP/1.1");
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
Socket socket = mock(Socket.class);
|
||||
when(socket.getOutputStream()).thenReturn(out);
|
||||
|
||||
proxyCache.processRequest(request, socket);
|
||||
Response response = new Response(out.toByteArray());
|
||||
Response response = processRequest(HTTP_DATA_URL, "GET /" + HTTP_DATA_URL + " HTTP/1.1");
|
||||
|
||||
assertThat(response.data).isEqualTo(loadTestData());
|
||||
assertThat(response.code).isEqualTo(200);
|
||||
assertThat(response.contentLength).isEqualTo(ProxyCacheTestUtils.HTTP_DATA_SIZE);
|
||||
assertThat(response.contentLength).isEqualTo(HTTP_DATA_SIZE);
|
||||
assertThat(response.contentType).isEqualTo("image/jpeg");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProcessPartialRequestWithoutCache() throws Exception {
|
||||
HttpUrlSource source = new HttpUrlSource(HTTP_DATA_URL);
|
||||
FileCache fileCache = new FileCache(ProxyCacheTestUtils.newCacheFile());
|
||||
FileCache spyFileCache = Mockito.spy(fileCache);
|
||||
doThrow(new RuntimeException()).when(spyFileCache).read(any(byte[].class), anyLong(), anyInt());
|
||||
HttpProxyCache proxyCache = new HttpProxyCache(source, spyFileCache);
|
||||
GetRequest request = new GetRequest("GET /" + HTTP_DATA_URL + " HTTP/1.1\nRange: bytes=2000-");
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
Socket socket = mock(Socket.class);
|
||||
when(socket.getOutputStream()).thenReturn(out);
|
||||
|
||||
proxyCache.processRequest(request, socket);
|
||||
Response response = new Response(out.toByteArray());
|
||||
String httpRequest = "GET /" + HTTP_DATA_URL + " HTTP/1.1\nRange: bytes=2000-";
|
||||
Response response = processRequest(HTTP_DATA_URL, httpRequest, spyFileCache);
|
||||
|
||||
byte[] fullData = loadTestData();
|
||||
byte[] partialData = new byte[fullData.length - 2000];
|
||||
@@ -73,4 +78,188 @@ public class HttpProxyCacheTest {
|
||||
assertThat(response.data).isEqualTo(partialData);
|
||||
assertThat(response.code).isEqualTo(206);
|
||||
}
|
||||
|
||||
@Test // https://github.com/danikula/AndroidVideoCache/issues/43
|
||||
public void testPreventClosingOriginalSourceForNewPartialRequestWithoutCache() throws Exception {
|
||||
HttpUrlSource source = new HttpUrlSource(HTTP_DATA_BIG_URL);
|
||||
FileCache fileCache = new FileCache(ProxyCacheTestUtils.newCacheFile());
|
||||
HttpProxyCache proxyCache = new HttpProxyCache(source, fileCache);
|
||||
ExecutorService executor = Executors.newFixedThreadPool(5);
|
||||
Future<Response> firstRequestFeature = processAsync(executor, proxyCache, "GET /" + HTTP_DATA_URL + " HTTP/1.1");
|
||||
Thread.sleep(100); // wait for first request started to process
|
||||
|
||||
int offset = 30000;
|
||||
String partialRequest = "GET /" + HTTP_DATA_URL + " HTTP/1.1\nRange: bytes=" + offset + "-";
|
||||
Future<Response> secondRequestFeature = processAsync(executor, proxyCache, partialRequest);
|
||||
|
||||
Response secondResponse = secondRequestFeature.get();
|
||||
Response firstResponse = firstRequestFeature.get();
|
||||
|
||||
byte[] responseData = loadAssetFile(ASSETS_DATA_BIG_NAME);
|
||||
assertThat(firstResponse.data).isEqualTo(responseData);
|
||||
|
||||
byte[] partialData = new byte[responseData.length - offset];
|
||||
System.arraycopy(responseData, offset, partialData, 0, partialData.length);
|
||||
assertThat(secondResponse.data).isEqualTo(partialData);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProcessManyThreads() throws Exception {
|
||||
final String url = "https://raw.githubusercontent.com/danikula/AndroidVideoCache/master/files/space.jpg";
|
||||
HttpUrlSource source = new HttpUrlSource(url);
|
||||
FileCache fileCache = new FileCache(ProxyCacheTestUtils.newCacheFile());
|
||||
final HttpProxyCache proxyCache = new HttpProxyCache(source, fileCache);
|
||||
final byte[] loadedData = loadAssetFile("space.jpg");
|
||||
final Random random = new Random(System.currentTimeMillis());
|
||||
int concurrentRequests = 10;
|
||||
ExecutorService executor = Executors.newFixedThreadPool(concurrentRequests);
|
||||
Future[] results = new Future[concurrentRequests];
|
||||
int[] offsets = new int[concurrentRequests];
|
||||
final CountDownLatch finishLatch = new CountDownLatch(concurrentRequests);
|
||||
final CountDownLatch startLatch = new CountDownLatch(1);
|
||||
for (int i = 0; i < concurrentRequests; i++) {
|
||||
final int offset = random.nextInt(loadedData.length);
|
||||
offsets[i] = offset;
|
||||
results[i] = executor.submit(new Callable<Response>() {
|
||||
|
||||
@Override
|
||||
public Response call() throws Exception {
|
||||
try {
|
||||
startLatch.await();
|
||||
String partialRequest = "GET /" + url + " HTTP/1.1\nRange: bytes=" + offset + "-";
|
||||
return processRequest(proxyCache, partialRequest);
|
||||
} finally {
|
||||
finishLatch.countDown();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
startLatch.countDown();
|
||||
finishLatch.await();
|
||||
|
||||
for (int i = 0; i < results.length; i++) {
|
||||
Response response = (Response) results[i].get();
|
||||
int offset = offsets[i];
|
||||
byte[] partialData = new byte[loadedData.length - offset];
|
||||
System.arraycopy(loadedData, offset, partialData, 0, partialData.length);
|
||||
assertThat(response.data).isEqualTo(partialData);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadEmptyFile() throws Exception {
|
||||
String zeroSizeUrl = "https://raw.githubusercontent.com/danikula/AndroidVideoCache/master/files/empty.txt";
|
||||
HttpUrlSource source = new HttpUrlSource(zeroSizeUrl);
|
||||
HttpProxyCache proxyCache = new HttpProxyCache(source, new FileCache(ProxyCacheTestUtils.newCacheFile()));
|
||||
GetRequest request = new GetRequest("GET /" + HTTP_DATA_URL + " HTTP/1.1");
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
Socket socket = mock(Socket.class);
|
||||
when(socket.getOutputStream()).thenReturn(out);
|
||||
|
||||
CacheListener listener = Mockito.mock(CacheListener.class);
|
||||
proxyCache.registerCacheListener(listener);
|
||||
proxyCache.processRequest(request, socket);
|
||||
proxyCache.registerCacheListener(null);
|
||||
Response response = new Response(out.toByteArray());
|
||||
|
||||
Mockito.verify(listener).onCacheAvailable(Mockito.<File>any(), eq(zeroSizeUrl), eq(100));
|
||||
assertThat(response.data).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCacheListenerCalledAtTheEnd() throws Exception {
|
||||
File file = ProxyCacheTestUtils.newCacheFile();
|
||||
File tempFile = ProxyCacheTestUtils.getTempFile(file);
|
||||
HttpProxyCache proxyCache = new HttpProxyCache(new HttpUrlSource(HTTP_DATA_URL), new FileCache(file));
|
||||
CacheListener listener = Mockito.mock(CacheListener.class);
|
||||
proxyCache.registerCacheListener(listener);
|
||||
processRequest(proxyCache, "GET /" + HTTP_DATA_URL + " HTTP/1.1");
|
||||
|
||||
Mockito.verify(listener).onCacheAvailable(tempFile, HTTP_DATA_URL, 100); // must be called for temp file ...
|
||||
Mockito.verify(listener).onCacheAvailable(file, HTTP_DATA_URL, 100); // .. and for original file too
|
||||
}
|
||||
|
||||
@Test(expected = ProxyCacheException.class)
|
||||
public void testTouchSourceForAbsentSourceInfoAndCache() throws Exception {
|
||||
SourceInfoStorage sourceInfoStorage = SourceInfoStorageFactory.newEmptySourceInfoStorage();
|
||||
HttpUrlSource source = ProxyCacheTestUtils.newNotOpenableHttpUrlSource(HTTP_DATA_URL, sourceInfoStorage);
|
||||
HttpProxyCache proxyCache = new HttpProxyCache(source, new FileCache(newCacheFile()));
|
||||
processRequest(proxyCache, "GET /" + HTTP_DATA_URL + " HTTP/1.1");
|
||||
proxyCache.shutdown();
|
||||
fail("Angry source should throw error! There is no file and caches source info");
|
||||
}
|
||||
|
||||
@Test(expected = ProxyCacheException.class)
|
||||
public void testTouchSourceForExistedSourceInfoAndAbsentCache() throws Exception {
|
||||
SourceInfoStorage sourceInfoStorage = SourceInfoStorageFactory.newSourceInfoStorage(RuntimeEnvironment.application);
|
||||
sourceInfoStorage.put(HTTP_DATA_URL, new SourceInfo(HTTP_DATA_URL, HTTP_DATA_SIZE, "image/jpg"));
|
||||
HttpUrlSource source = ProxyCacheTestUtils.newNotOpenableHttpUrlSource(HTTP_DATA_URL, sourceInfoStorage);
|
||||
HttpProxyCache proxyCache = new HttpProxyCache(source, new FileCache(newCacheFile()));
|
||||
processRequest(proxyCache, "GET /" + HTTP_DATA_URL + " HTTP/1.1");
|
||||
proxyCache.shutdown();
|
||||
fail("Angry source should throw error! There is no cache file");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTouchSourceForExistedSourceInfoAndCache() throws Exception {
|
||||
SourceInfoStorage sourceInfoStorage = SourceInfoStorageFactory.newSourceInfoStorage(RuntimeEnvironment.application);
|
||||
sourceInfoStorage.put(HTTP_DATA_URL, new SourceInfo(HTTP_DATA_URL, HTTP_DATA_SIZE, "cached/mime"));
|
||||
HttpUrlSource source = ProxyCacheTestUtils.newNotOpenableHttpUrlSource(HTTP_DATA_URL, sourceInfoStorage);
|
||||
File file = newCacheFile();
|
||||
IoUtils.saveToFile(loadAssetFile(ASSETS_DATA_NAME), file);
|
||||
HttpProxyCache proxyCache = new HttpProxyCache(source, new FileCache(file));
|
||||
Response response = processRequest(proxyCache, "GET /" + HTTP_DATA_URL + " HTTP/1.1");
|
||||
proxyCache.shutdown();
|
||||
assertThat(response.data).isEqualTo(loadAssetFile(ASSETS_DATA_NAME));
|
||||
assertThat(response.contentLength).isEqualTo(HTTP_DATA_SIZE);
|
||||
assertThat(response.contentType).isEqualTo("cached/mime");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReuseSourceInfo() throws Exception {
|
||||
SourceInfoStorage sourceInfoStorage = SourceInfoStorageFactory.newSourceInfoStorage(RuntimeEnvironment.application);
|
||||
HttpUrlSource source = new HttpUrlSource(HTTP_DATA_URL, sourceInfoStorage);
|
||||
File cacheFile = newCacheFile();
|
||||
HttpProxyCache proxyCache = new HttpProxyCache(source, new FileCache(cacheFile));
|
||||
processRequest(proxyCache, "GET /" + HTTP_DATA_URL + " HTTP/1.1");
|
||||
|
||||
HttpUrlSource notOpenableSource = ProxyCacheTestUtils.newNotOpenableHttpUrlSource(HTTP_DATA_URL, sourceInfoStorage);
|
||||
HttpProxyCache proxyCache2 = new HttpProxyCache(notOpenableSource, new FileCache(cacheFile));
|
||||
Response response = processRequest(proxyCache2, "GET /" + HTTP_DATA_URL + " HTTP/1.1");
|
||||
proxyCache.shutdown();
|
||||
|
||||
assertThat(response.data).isEqualTo(loadAssetFile(ASSETS_DATA_NAME));
|
||||
assertThat(response.contentLength).isEqualTo(HTTP_DATA_SIZE);
|
||||
assertThat(response.contentType).isEqualTo("image/jpeg");
|
||||
}
|
||||
|
||||
private Response processRequest(String sourceUrl, String httpRequest) throws ProxyCacheException, IOException {
|
||||
FileCache fileCache = new FileCache(ProxyCacheTestUtils.newCacheFile());
|
||||
return processRequest(sourceUrl, httpRequest, fileCache);
|
||||
}
|
||||
|
||||
private Response processRequest(String sourceUrl, String httpRequest, FileCache fileCache) throws ProxyCacheException, IOException {
|
||||
HttpUrlSource source = new HttpUrlSource(sourceUrl);
|
||||
HttpProxyCache proxyCache = new HttpProxyCache(source, fileCache);
|
||||
return processRequest(proxyCache, httpRequest);
|
||||
}
|
||||
|
||||
private Response processRequest(HttpProxyCache proxyCache, String httpRequest) throws ProxyCacheException, IOException {
|
||||
GetRequest request = new GetRequest(httpRequest);
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
Socket socket = mock(Socket.class);
|
||||
when(socket.getOutputStream()).thenReturn(out);
|
||||
proxyCache.processRequest(request, socket);
|
||||
return new Response(out.toByteArray());
|
||||
}
|
||||
|
||||
private Future<Response> processAsync(ExecutorService executor, final HttpProxyCache proxyCache, final String httpRequest) {
|
||||
return executor.submit(new Callable<Response>() {
|
||||
|
||||
@Override
|
||||
public Response call() throws Exception {
|
||||
return processRequest(proxyCache, httpRequest);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
package com.danikula.videocache;
|
||||
|
||||
import com.danikula.videocache.sourcestorage.SourceInfoStorage;
|
||||
import com.danikula.videocache.sourcestorage.SourceInfoStorageFactory;
|
||||
import com.danikula.videocache.support.ProxyCacheTestUtils;
|
||||
import com.danikula.videocache.test.BuildConfig;
|
||||
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mockito;
|
||||
import org.robolectric.RobolectricGradleTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.Arrays;
|
||||
|
||||
import static com.danikula.videocache.ProxyCacheUtils.DEFAULT_BUFFER_SIZE;
|
||||
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;
|
||||
@@ -23,12 +28,13 @@ import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_URL_
|
||||
import static com.danikula.videocache.support.ProxyCacheTestUtils.loadAssetFile;
|
||||
import static org.fest.assertions.api.Assertions.assertThat;
|
||||
import static org.fest.assertions.api.Assertions.fail;
|
||||
import static org.mockito.Matchers.any;
|
||||
|
||||
/**
|
||||
* @author Alexey Danilov (danikula@gmail.com).
|
||||
*/
|
||||
@RunWith(RobolectricGradleTestRunner.class)
|
||||
@Config(constants = BuildConfig.class, emulateSdk = BuildConfig.MIN_SDK_VERSION)
|
||||
@Config(constants = BuildConfig.class)
|
||||
public class HttpUrlSourceTest {
|
||||
|
||||
@Test
|
||||
@@ -83,7 +89,7 @@ public class HttpUrlSourceTest {
|
||||
HttpUrlSource source = new HttpUrlSource(HTTP_DATA_URL_ONE_REDIRECT);
|
||||
source.open(0);
|
||||
byte[] readData = new byte[HTTP_DATA_SIZE];
|
||||
source.read(readData);
|
||||
readSource(source, readData);
|
||||
source.close();
|
||||
|
||||
byte[] expectedData = Arrays.copyOfRange(loadAssetFile(ASSETS_DATA_NAME), 0, HTTP_DATA_SIZE);
|
||||
@@ -96,7 +102,7 @@ public class HttpUrlSourceTest {
|
||||
HttpUrlSource source = new HttpUrlSource(HTTP_DATA_URL_ONE_REDIRECT);
|
||||
source.open(offset);
|
||||
byte[] readData = new byte[HTTP_DATA_SIZE - offset];
|
||||
source.read(readData);
|
||||
readSource(source, readData);
|
||||
source.close();
|
||||
|
||||
byte[] expectedData = Arrays.copyOfRange(loadAssetFile(ASSETS_DATA_NAME), offset, HTTP_DATA_SIZE);
|
||||
@@ -109,7 +115,7 @@ public class HttpUrlSourceTest {
|
||||
HttpUrlSource source = new HttpUrlSource(HTTP_DATA_URL_3_REDIRECTS);
|
||||
source.open(offset);
|
||||
byte[] readData = new byte[HTTP_DATA_SIZE - offset];
|
||||
source.read(readData);
|
||||
readSource(source, readData);
|
||||
source.close();
|
||||
|
||||
byte[] expectedData = Arrays.copyOfRange(loadAssetFile(ASSETS_DATA_NAME), offset, HTTP_DATA_SIZE);
|
||||
@@ -130,4 +136,39 @@ public class HttpUrlSourceTest {
|
||||
assertThat(new HttpUrlSource("http://mysite.by/video.mp4").getMime()).isEqualTo("video/mp4");
|
||||
assertThat(new HttpUrlSource(HTTP_DATA_URL).getMime()).isEqualTo("image/jpeg");
|
||||
}
|
||||
|
||||
@Test(expected = RuntimeException.class)
|
||||
public void testAngryHttpUrlSourceLength() throws Exception {
|
||||
ProxyCacheTestUtils.newAngryHttpUrlSource().length();
|
||||
fail("source.length() should throw exception");
|
||||
}
|
||||
|
||||
@Test(expected = RuntimeException.class)
|
||||
public void testAngryHttpUrlSourceOpen() throws Exception {
|
||||
ProxyCacheTestUtils.newAngryHttpUrlSource().open(Mockito.anyInt());
|
||||
fail("source.open() should throw exception");
|
||||
}
|
||||
|
||||
@Test(expected = RuntimeException.class)
|
||||
public void testAngryHttpUrlSourceRead() throws Exception {
|
||||
ProxyCacheTestUtils.newAngryHttpUrlSource().read(any(byte[].class));
|
||||
fail("source.read() should throw exception");
|
||||
}
|
||||
|
||||
@Test(expected = RuntimeException.class)
|
||||
public void testNotOpenableHttpUrlSourceOpen() throws Exception {
|
||||
SourceInfoStorage sourceInfoStorage = SourceInfoStorageFactory.newEmptySourceInfoStorage();
|
||||
ProxyCacheTestUtils.newNotOpenableHttpUrlSource("", sourceInfoStorage).open(Mockito.anyInt());
|
||||
fail("source.open() should throw exception");
|
||||
}
|
||||
|
||||
private void readSource(Source source, byte[] target) throws ProxyCacheException {
|
||||
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
|
||||
int totalRead = 0;
|
||||
int readBytes;
|
||||
while ((readBytes = source.read(buffer)) != -1) {
|
||||
System.arraycopy(buffer, 0, target, totalRead, readBytes);
|
||||
totalRead += readBytes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,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.support.ProxyCacheTestUtils;
|
||||
import com.danikula.videocache.test.BuildConfig;
|
||||
|
||||
import org.junit.Test;
|
||||
@@ -23,13 +22,14 @@ import static com.danikula.videocache.support.ProxyCacheTestUtils.generate;
|
||||
import static com.danikula.videocache.support.ProxyCacheTestUtils.getFileContent;
|
||||
import static com.danikula.videocache.support.ProxyCacheTestUtils.loadAssetFile;
|
||||
import static com.danikula.videocache.support.ProxyCacheTestUtils.newCacheFile;
|
||||
import static com.danikula.videocache.support.ProxyCacheTestUtils.newPhlegmaticSource;
|
||||
import static org.fest.assertions.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Alexey Danilov (danikula@gmail.com).
|
||||
*/
|
||||
@RunWith(RobolectricGradleTestRunner.class)
|
||||
@Config(constants = BuildConfig.class, emulateSdk = BuildConfig.MIN_SDK_VERSION)
|
||||
@Config(constants = BuildConfig.class)
|
||||
public class ProxyCacheTest {
|
||||
|
||||
@Test
|
||||
@@ -95,7 +95,7 @@ public class ProxyCacheTest {
|
||||
public void testProxyWithPhlegmaticSource() throws Exception {
|
||||
int dataSize = 100000;
|
||||
byte[] sourceData = generate(dataSize);
|
||||
Source source = new PhlegmaticByteArraySource(sourceData, 200);
|
||||
Source source = newPhlegmaticSource(sourceData, 200);
|
||||
ProxyCache proxyCache = new ProxyCache(source, new FileCache(newCacheFile()));
|
||||
byte[] readData = new byte[dataSize];
|
||||
proxyCache.read(readData, 0, dataSize);
|
||||
@@ -106,7 +106,7 @@ public class ProxyCacheTest {
|
||||
@Test
|
||||
public void testReadEnd() throws Exception {
|
||||
int capacity = 5323;
|
||||
Source source = new PhlegmaticByteArraySource(generate(capacity), 200);
|
||||
Source source = newPhlegmaticSource(generate(capacity), 200);
|
||||
Cache cache = new FileCache(newCacheFile());
|
||||
ProxyCache proxyCache = new ProxyCache(source, cache);
|
||||
proxyCache.read(new byte[1], capacity - 1, 1);
|
||||
@@ -118,7 +118,7 @@ public class ProxyCacheTest {
|
||||
public void testReadRandomParts() throws Exception {
|
||||
int dataSize = 123456;
|
||||
byte[] sourceData = generate(dataSize);
|
||||
Source source = new PhlegmaticByteArraySource(sourceData, 300);
|
||||
Source source = newPhlegmaticSource(sourceData, 300);
|
||||
File file = newCacheFile();
|
||||
Cache cache = new FileCache(file);
|
||||
ProxyCache proxyCache = new ProxyCache(source, cache);
|
||||
@@ -176,8 +176,9 @@ public class ProxyCacheTest {
|
||||
byte[] data = generate(dataSize);
|
||||
File file = newCacheFile();
|
||||
IoUtils.saveToFile(data, file);
|
||||
ProxyCache proxyCache = new ProxyCache(new AngryHttpUrlSource(), new FileCache(file));
|
||||
|
||||
Source source = ProxyCacheTestUtils.newAngryHttpUrlSource();
|
||||
ProxyCache proxyCache = new ProxyCache(source, new FileCache(file));
|
||||
byte[] readData = new byte[dataSize];
|
||||
proxyCache.read(readData, 0, dataSize);
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import static org.fest.assertions.api.Assertions.assertThat;
|
||||
* @author Alexey Danilov (danikula@gmail.com).
|
||||
*/
|
||||
@RunWith(RobolectricGradleTestRunner.class)
|
||||
@Config(constants = BuildConfig.class, emulateSdk = BuildConfig.MIN_SDK_VERSION)
|
||||
@Config(constants = BuildConfig.class)
|
||||
public class DiskUsageTest {
|
||||
|
||||
private File cacheFolder;
|
||||
|
||||
@@ -28,7 +28,7 @@ import static org.fest.assertions.api.Assertions.assertThat;
|
||||
* @author Alexey Danilov (danikula@gmail.com).
|
||||
*/
|
||||
@RunWith(RobolectricGradleTestRunner.class)
|
||||
@Config(constants = BuildConfig.class, emulateSdk = BuildConfig.MIN_SDK_VERSION)
|
||||
@Config(constants = BuildConfig.class)
|
||||
public class FileCacheTest {
|
||||
|
||||
@Test
|
||||
|
||||
@@ -18,7 +18,7 @@ import static org.fest.assertions.api.Assertions.assertThat;
|
||||
* @author Alexey Danilov (danikula@gmail.com).
|
||||
*/
|
||||
@RunWith(RobolectricGradleTestRunner.class)
|
||||
@Config(constants = BuildConfig.class, emulateSdk = BuildConfig.MIN_SDK_VERSION)
|
||||
@Config(constants = BuildConfig.class)
|
||||
public class FilesTest {
|
||||
|
||||
@Test
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.danikula.videocache.sourcestorage;
|
||||
|
||||
import com.danikula.videocache.SourceInfo;
|
||||
import com.danikula.videocache.test.BuildConfig;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RobolectricGradleTestRunner;
|
||||
import org.robolectric.RuntimeEnvironment;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
||||
import static org.fest.assertions.api.Assertions.assertThat;
|
||||
import static org.fest.assertions.api.Assertions.fail;
|
||||
|
||||
/**
|
||||
* Tests for {@link SourceInfoStorage}.
|
||||
*
|
||||
* @author Alexey Danilov (danikula@gmail.com).
|
||||
*/
|
||||
@RunWith(RobolectricGradleTestRunner.class)
|
||||
@Config(constants = BuildConfig.class)
|
||||
public class SourceInfoStorageTest {
|
||||
|
||||
private SourceInfoStorage storage;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
storage = SourceInfoStorageFactory.newSourceInfoStorage(RuntimeEnvironment.application);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() throws Exception {
|
||||
storage.release();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetAbsent() throws Exception {
|
||||
SourceInfo sourceInfo = storage.get(":-)");
|
||||
assertThat(sourceInfo).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSaving() throws Exception {
|
||||
storage.put(":-)", new SourceInfo(":-)", 42, "text/plain"));
|
||||
storage.put(":-(", new SourceInfo(":-(", 43, "video/mp4"));
|
||||
|
||||
SourceInfo sourceInfo = storage.get(":-)");
|
||||
assertThat(sourceInfo.url).isEqualTo(":-)");
|
||||
assertThat(sourceInfo.length).isEqualTo(42);
|
||||
assertThat(sourceInfo.mime).isEqualTo("text/plain");
|
||||
|
||||
SourceInfo sourceInfo2 = storage.get(":-(");
|
||||
assertThat(sourceInfo2.url).isEqualTo(":-(");
|
||||
assertThat(sourceInfo2.length).isEqualTo(43);
|
||||
assertThat(sourceInfo2.mime).isEqualTo("video/mp4");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdating() throws Exception {
|
||||
String url = ":-)";
|
||||
storage.put(url, new SourceInfo(url, 42, "text/plain"));
|
||||
storage.put(url, new SourceInfo(url, 43, "video/mp4"));
|
||||
|
||||
SourceInfo sourceInfo = storage.get(url);
|
||||
assertThat(sourceInfo.url).isEqualTo(url);
|
||||
assertThat(sourceInfo.length).isEqualTo(43);
|
||||
assertThat(sourceInfo.mime).isEqualTo("video/mp4");
|
||||
}
|
||||
|
||||
@Test(expected = NullPointerException.class)
|
||||
public void testNpeForGetting() throws Exception {
|
||||
storage.get(null);
|
||||
fail("null is not acceptable");
|
||||
}
|
||||
|
||||
@Test(expected = NullPointerException.class)
|
||||
public void testNpeForPuttingUrl() throws Exception {
|
||||
storage.put(null, new SourceInfo("", 0, ""));
|
||||
fail("null is not acceptable");
|
||||
}
|
||||
|
||||
@Test(expected = NullPointerException.class)
|
||||
public void testNpeForPuttingSource() throws Exception {
|
||||
storage.put("url", null);
|
||||
fail("null is not acceptable");
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package com.danikula.videocache.support;
|
||||
|
||||
import com.danikula.videocache.ProxyCacheException;
|
||||
import com.danikula.videocache.Source;
|
||||
|
||||
/**
|
||||
* {@link Source} that throws exception in all methods.
|
||||
*
|
||||
* @author Alexey Danilov (danikula@gmail.com).
|
||||
*/
|
||||
@Deprecated // use Mockito to throw error
|
||||
public class AngryHttpUrlSource implements Source {
|
||||
|
||||
@Override
|
||||
public int length() throws ProxyCacheException {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void open(int offset) throws ProxyCacheException {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws ProxyCacheException {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] buffer) throws ProxyCacheException {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package com.danikula.videocache.support;
|
||||
|
||||
import com.danikula.videocache.ByteArraySource;
|
||||
import com.danikula.videocache.ProxyCacheException;
|
||||
|
||||
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;
|
||||
private final int maxDelayMs;
|
||||
|
||||
public PhlegmaticByteArraySource(byte[] data, int maxDelayMs) {
|
||||
super(data);
|
||||
this.maxDelayMs = maxDelayMs;
|
||||
this.delayGenerator = new Random(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] buffer) throws ProxyCacheException {
|
||||
try {
|
||||
Thread.sleep(delayGenerator.nextInt(maxDelayMs));
|
||||
} catch (InterruptedException e) {
|
||||
throw new ProxyCacheException("Error sleeping", e);
|
||||
}
|
||||
return super.read(buffer);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,16 @@
|
||||
package com.danikula.videocache.support;
|
||||
|
||||
import com.danikula.android.garden.io.IoUtils;
|
||||
import com.danikula.videocache.ByteArraySource;
|
||||
import com.danikula.videocache.HttpProxyCacheServer;
|
||||
import com.danikula.videocache.HttpUrlSource;
|
||||
import com.danikula.videocache.ProxyCacheException;
|
||||
import com.danikula.videocache.Source;
|
||||
import com.danikula.videocache.sourcestorage.SourceInfoStorage;
|
||||
import com.google.common.io.Files;
|
||||
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.mockito.stubbing.Answer;
|
||||
import org.robolectric.RuntimeEnvironment;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
@@ -15,17 +22,24 @@ import java.net.URL;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.anyInt;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
|
||||
/**
|
||||
* @author Alexey Danilov (danikula@gmail.com).
|
||||
*/
|
||||
public class ProxyCacheTestUtils {
|
||||
|
||||
public static final String HTTP_DATA_URL = "https://dl.dropboxusercontent.com/u/15506779/persistent/proxycache/android.jpg";
|
||||
public static final String HTTP_DATA_URL_ONE_REDIRECT = "http://bit.ly/1V5PeY5";
|
||||
public static final String HTTP_DATA_URL_3_REDIRECTS = "http://bit.ly/1KvVmgZ";
|
||||
public static final String HTTP_DATA_URL_6_REDIRECTS = "http://ow.ly/SugRH";
|
||||
public static final String HTTP_DATA_BIG_URL = "https://dl.dropboxusercontent.com/u/15506779/persistent/proxycache/phones.jpg";
|
||||
public static final String HTTP_DATA_BIG_URL_ONE_REDIRECT = "http://bit.ly/1iJ69yA";
|
||||
public static final String HTTP_DATA_URL = "https://raw.githubusercontent.com/danikula/AndroidVideoCache/master/files/android.jpg";
|
||||
public static final String HTTP_DATA_URL_ONE_REDIRECT = "http://bit.ly/1LAJKAy";
|
||||
public static final String HTTP_DATA_URL_3_REDIRECTS = "http://bit.ly/1QtKJiB";
|
||||
public static final String HTTP_DATA_URL_6_REDIRECTS = "http://ow.ly/Z17wz";
|
||||
public static final String HTTP_DATA_BIG_URL = "https://raw.githubusercontent.com/danikula/AndroidVideoCache/master/files/phones.jpg";
|
||||
public static final String HTTP_DATA_BIG_URL_ONE_REDIRECT = "http://bit.ly/24DdZ06";
|
||||
public static final String ASSETS_DATA_NAME = "android.jpg";
|
||||
public static final String ASSETS_DATA_BIG_NAME = "phones.jpg";
|
||||
public static final int HTTP_DATA_SIZE = 4768;
|
||||
@@ -40,7 +54,11 @@ public class ProxyCacheTestUtils {
|
||||
}
|
||||
|
||||
public static Response readProxyResponse(HttpProxyCacheServer proxy, String url, int offset) throws IOException {
|
||||
URL proxiedUrl = new URL(proxy.getProxyUrl(url));
|
||||
String proxyUrl = proxy.getProxyUrl(url);
|
||||
if (!proxyUrl.startsWith("http://127.0.0.1")) {
|
||||
throw new IllegalStateException("Url " + url + " is not proxied!");
|
||||
}
|
||||
URL proxiedUrl = new URL(proxyUrl);
|
||||
HttpURLConnection connection = (HttpURLConnection) proxiedUrl.openConnection();
|
||||
try {
|
||||
if (offset >= 0) {
|
||||
@@ -79,4 +97,42 @@ public class ProxyCacheTestUtils {
|
||||
random.nextBytes(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static HttpUrlSource newAngryHttpUrlSource() throws ProxyCacheException {
|
||||
HttpUrlSource source = mock(HttpUrlSource.class);
|
||||
doThrow(new RuntimeException()).when(source).getMime();
|
||||
doThrow(new RuntimeException()).when(source).read(any(byte[].class));
|
||||
doThrow(new RuntimeException()).when(source).open(anyInt());
|
||||
doThrow(new RuntimeException()).when(source).length();
|
||||
doThrow(new RuntimeException()).when(source).getUrl();
|
||||
doThrow(new RuntimeException()).when(source).close();
|
||||
return source;
|
||||
}
|
||||
|
||||
public static HttpUrlSource newNotOpenableHttpUrlSource(String url, SourceInfoStorage sourceInfoStorage) throws ProxyCacheException {
|
||||
HttpUrlSource httpUrlSource = new HttpUrlSource(url, sourceInfoStorage);
|
||||
HttpUrlSource source = spy(httpUrlSource);
|
||||
doAnswer(new Answer() {
|
||||
@Override
|
||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||
System.out.print("Can't open!!!");
|
||||
throw new RuntimeException();
|
||||
}
|
||||
}).when(source).open(anyInt());
|
||||
return source;
|
||||
}
|
||||
|
||||
public static Source newPhlegmaticSource(byte[] data, final int maxDelayMs) throws ProxyCacheException {
|
||||
Source spySource = spy(new ByteArraySource(data));
|
||||
final Random delayGenerator = new Random(System.currentTimeMillis());
|
||||
doAnswer(new Answer() {
|
||||
|
||||
@Override
|
||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||
Thread.sleep(delayGenerator.nextInt(maxDelayMs));
|
||||
return null;
|
||||
}
|
||||
}).doCallRealMethod().when(spySource).read(any(byte[].class));
|
||||
return spySource;
|
||||
}
|
||||
}
|
||||
|
||||