40 Commits

Author SHA1 Message Date
Alexey Danilov
99b8330679 📦 release 2.6.0 2016-08-26 13:14:47 +03:00
Alexey Danilov
0f522f200d notify CacheListener after cache is completed with original file 2016-08-26 13:10:19 +03:00
Alexey Danilov
71c6301eb4 🤘 cache source's info in db to increase performance (#41, #45) 2016-08-05 14:36:25 +03:00
Alexey Danilov
372542c2dc update robolectric 3.0.rc-2 -> 3.1 2016-08-04 22:07:27 +03:00
Alexey Danilov
22cfddb2c9 add table of content, known problems to readme 2016-08-04 18:33:58 +03:00
Alexey Danilov
8fc4a9ac7d 📦 release 2.5.0 2016-07-29 17:30:16 +03:00
Alexey Danilov
6c996ea66c 🐛 don't close source after processing partial request without cache #43
prevent invalid calling source.close() in different threads to avoid crashes on Lollipop (#37, #29, #63, #66)
2016-07-29 14:09:11 +03:00
wlq
582832f8b5 fix default cache size 2016-07-28 23:01:03 +03:00
Alexey Danilov
cd0b4111a4 ability to check was content fully cached to file or not 2016-07-27 23:14:06 +03:00
Alexey Danilov
eb67640212 update readme 2016-07-27 21:07:35 +03:00
Alexey Danilov
1d50807fe0 fix HttpUrlSourceTest test 2016-07-27 20:56:21 +03:00
Alexey Danilov
6da9650030 update build tools 2016-07-27 20:36:41 +03:00
Alexey Danilov
a1d00fec7b use static files for tests and sample from github instead of dropbox 2016-03-03 14:13:46 +03:00
Alexey Danilov
f5a779266d add test files 2016-03-03 13:50:01 +03:00
Alexey Danilov
ca6f36ada3 update the latest version in README 2015-12-08 19:06:07 +03:00
Arthur Alves
33b7e5e9d3 Catch NullPointerException to avoid crash due to unsupported async disconnects on Android L (which uses an outdated okhttp version) 2015-12-08 18:59:37 +03:00
Alexey Danilov
0aad13f118 fix dividing by zero while loading empty file (#26) 2015-11-06 13:43:08 +03:00
Alexey Danilov
a270460c8d fix fetching content info (#27) 2015-10-29 16:14:20 +03:00
Alexey Danilov
0264944a20 fix self-pinging (#24) 2015-10-28 10:29:10 +03:00
Alexey Danilov
cf69365920 update last version in README 2015-10-23 18:55:19 +03:00
Alexey Danilov
14ac4a98b1 🐛 fix Content-Range header for partial response according to http spec (http://bit.ly/content-range) 2015-10-23 18:53:57 +03:00
Alexey Danilov
c5cf254b74 fix cleaning cache in 'sample' app 2015-10-23 18:07:01 +03:00
Alexey Danilov
e4ab124d57 fix readme 2015-10-23 16:32:48 +03:00
Alexey Danilov
e7f471983d explain how to use cache limits 2015-10-23 15:32:57 +03:00
Alexey Danilov
d32e88f641 add disk usage limits (total cache size, total files, unlimited) #5 2015-10-23 15:05:18 +03:00
Alexey Danilov
55988e278d use jcenter dependency for sample. fix readme 2015-10-20 15:42:18 +03:00
Alexey Danilov
e06e45c75c remove release notes from readme 2015-10-04 00:37:34 +06:00
Alexey Danilov
1d3230ba57 add seeking video support (#21) and fix streaming while caching (#17) 2015-10-04 00:21:16 +06:00
Alexey Danilov
fe9af27f96 add 'code contributions' section to readme 2015-09-29 11:24:47 +06:00
Alexey Danilov
9ffa983f58 hide 'publish.sh' 2015-09-28 11:39:06 +06:00
Alexey Danilov
6125478d27 fix 🐛 available cache percents callback 2015-09-28 11:31:02 +06:00
Alexey Danilov
e135bf0b42 ping proxy after starting to make sure it works fine 2015-09-25 20:46:02 +03:00
Alexey Danilov
edb12b574f fix readme 2015-09-25 17:08:50 +03:00
Alexey Danilov
63ee20f93c fix offline work 2015-09-25 17:07:46 +03:00
Alexey Danilov
88da0aa5c0 fix markdown 2015-09-21 23:50:06 +03:00
Alexey Danilov
3d50eb64c3 update 'whats new' section in readme 2015-09-21 23:46:17 +03:00
Alexey Danilov
f4b9e5c8f5 fix too long file name for cache. Md5FileNameGenerator tests 2015-09-21 23:38:57 +03:00
Alexey Danilov
15c5388f6c tests for url redirections 2015-09-21 21:24:37 +03:00
Relex
8263814aea unit test for url redirect 2015-09-14 11:29:16 +08:00
Relex
1c7cb32a97 Support redirection 2015-09-12 15:50:56 +08:00
76 changed files with 2242 additions and 320 deletions

1
.gitignore vendored
View File

@@ -4,6 +4,7 @@
/.idea
/build
/publish.sh
/local.properties
/gradle.properties
/library/build

100
README.md
View File

@@ -1,18 +1,42 @@
# Video cache support for Android
## Video cache support for Android
[![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-AndroidVideoCache-brightgreen.svg?style=flat)](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!
## How to use?
Just add link to repository and dependency:
## Features
- caching to disk during streaming;
- offline work with cached resources;
- partial loading;
- cache limits (max cache size, max files count);
- multiple clients for same url.
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.
## Get started
Just add dependency (`AndroidVideoCache` is available in jcenter):
```
repositories {
maven { url 'https://dl.bintray.com/alexeydanilov/maven' }
jcenter()
}
dependencies {
compile 'com.danikula:videocache:2.0.9'
compile 'com.danikula:videocache:2.6.0'
}
```
@@ -34,7 +58,7 @@ private HttpProxyCacheServer getProxy() {
```
To guarantee normal work you should use **single** instance of `HttpProxyCacheServer` for whole app.
For example you can store shared proxy on your `Application`:
For example you can store shared proxy in your `Application`:
```java
public class App extends Application {
@@ -47,24 +71,62 @@ public class App extends Application {
}
private HttpProxyCacheServer newProxy() {
FileNameGenerator nameGenerator = new Md5FileNameGenerator(getExternalCacheDir());
return new HttpProxyCacheServer(nameGenerator);
return new HttpProxyCacheServer(this);
}
}
```
or use [simple factory](http://pastebin.com/38uNkgBT).
or use [simple factory](http://pastebin.com/s2fafSYS).
More preferable way is use some dependency injector like [Dagger](http://square.github.io/dagger/).
See `sample` app for details.
## Recipes
### Disk cache limit
By default `HttpProxyCacheServer` uses 512Mb for caching files. You can change this value:
## Whats new in 2.0?
- simpler api
- single cache for multiple clients
- cache file name policy
- more powerful listener
- more samples
- less log flood
```java
private HttpProxyCacheServer newProxy() {
return new HttpProxyCacheServer.Builder(this)
.maxCacheSize(1024 * 1024 * 1024) // 1 Gb for cache
.build();
}
```
or can limit total count of files in cache:
```java
private HttpProxyCacheServer newProxy() {
return new HttpProxyCacheServer.Builder(this)
.maxCacheFilesCount(20)
.build();
}
```
### 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)
## Code contributions
If it's a feature that you think would need to be discussed please open an issue first, otherwise, you can follow this process:
1. [Fork the project](http://help.github.com/fork-a-repo/)
2. Create a feature branch (git checkout -b my_branch)
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)
@@ -74,7 +136,7 @@ See `sample` app for details.
## 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.
@@ -86,4 +148,4 @@ See `sample` app for details.
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
limitations under the License.

View File

@@ -3,12 +3,12 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.3.0'
classpath 'com.android.tools.build:gradle:2.1.2'
}
}
allprojects {
repositories {
mavenCentral()
jcenter()
}
}

BIN
files/android Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
files/android.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
files/devbytes.mp4 Normal file

Binary file not shown.

0
files/empty.txt Normal file
View File

BIN
files/orange1.mp4 Normal file

Binary file not shown.

BIN
files/orange2.mp4 Normal file

Binary file not shown.

BIN
files/orange3.mp4 Normal file

Binary file not shown.

BIN
files/orange4.mp4 Normal file

Binary file not shown.

BIN
files/orange5.mp4 Normal file

Binary file not shown.

BIN
files/phones.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

BIN
files/space.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

@@ -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.4-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-2.12-all.zip

View File

@@ -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.0.9'
publishVersion = '2.6.0'
description = 'Cache support for android VideoView'
website = 'https://github.com/danikula/AndroidVideoCache'
}

View File

@@ -22,7 +22,7 @@ public class ByteArraySource implements Source {
}
@Override
public int available() throws ProxyCacheException {
public int length() throws ProxyCacheException {
return data.length;
}

View File

@@ -0,0 +1,33 @@
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;
/**
* Configuration for proxy cache.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
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, SourceInfoStorage sourceInfoStorage) {
this.cacheRoot = cacheRoot;
this.fileNameGenerator = fileNameGenerator;
this.diskUsage = diskUsage;
this.sourceInfoStorage = sourceInfoStorage;
}
File generateCacheFile(String url) {
String name = fileNameGenerator.generate(url);
return new File(cacheRoot, name);
}
}

View File

@@ -63,9 +63,9 @@ class GetRequest {
@Override
public String toString() {
return "GetRequest{" +
"uri='" + uri + '\'' +
", rangeOffset=" + rangeOffset +
"rangeOffset=" + rangeOffset +
", partial=" + partial +
", uri='" + uri + '\'' +
'}';
}
}

View File

@@ -2,11 +2,15 @@ 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;
import java.net.Socket;
import static com.danikula.videocache.ProxyCacheUtils.DEFAULT_BUFFER_SIZE;
/**
* {@link ProxyCache} that read http url and writes data to {@link Socket}
*
@@ -14,6 +18,8 @@ import java.net.Socket;
*/
class HttpProxyCache extends ProxyCache {
private static final float NO_CACHE_BARRIER = .2f;
private final HttpUrlSource source;
private final FileCache cache;
private CacheListener listener;
@@ -30,30 +36,29 @@ class HttpProxyCache extends ProxyCache {
public void processRequest(GetRequest request, Socket socket) throws IOException, ProxyCacheException {
OutputStream out = new BufferedOutputStream(socket.getOutputStream());
byte[] buffer = new byte[ProxyCacheUtils.DEFAULT_BUFFER_SIZE];
int readBytes;
boolean headersWrote = false;
String responseHeaders = newResponseHeaders(request);
out.write(responseHeaders.getBytes("UTF-8"));
long offset = request.rangeOffset;
while ((readBytes = read(buffer, offset, buffer.length)) != -1) {
// tiny optimization: to prevent HEAD request in source for content-length. content-length 'll available after reading source
if (!headersWrote) {
String responseHeaders = newResponseHeaders(request);
out.write(responseHeaders.getBytes("UTF-8"));
headersWrote = true;
}
out.write(buffer, 0, readBytes);
offset += readBytes;
if (cache.isCompleted()) {
onCacheAvailable(100);
}
if (isUseCache(request)) {
responseWithCache(out, offset);
} else {
responseWithoutCache(out, offset);
}
out.flush();
}
private boolean isUseCache(GetRequest request) throws ProxyCacheException {
int sourceLength = source.length();
boolean sourceLengthKnown = sourceLength > 0;
int cacheAvailable = cache.available();
// do not use cache for partial requests which too far from available cache. It seems user seek video.
return !sourceLengthKnown || !request.partial || request.rangeOffset <= cacheAvailable + sourceLength * NO_CACHE_BARRIER;
}
private String newResponseHeaders(GetRequest request) throws IOException, ProxyCacheException {
String mime = source.getMime();
boolean mimeKnown = !TextUtils.isEmpty(mime);
int length = cache.isCompleted() ? cache.available() : source.available();
int length = cache.isCompleted() ? cache.available() : source.length();
boolean lengthKnown = length >= 0;
long contentLength = request.partial ? length - request.rangeOffset : length;
boolean addRange = lengthKnown && request.partial;
@@ -61,16 +66,42 @@ class HttpProxyCache extends ProxyCache {
.append(request.partial ? "HTTP/1.1 206 PARTIAL CONTENT\n" : "HTTP/1.1 200 OK\n")
.append("Accept-Ranges: bytes\n")
.append(lengthKnown ? String.format("Content-Length: %d\n", contentLength) : "")
.append(addRange ? String.format("Content-Range: bytes %d-%d/%d\n", request.rangeOffset, length, length) : "")
.append(addRange ? String.format("Content-Range: bytes %d-%d/%d\n", request.rangeOffset, length - 1, length) : "")
.append(mimeKnown ? String.format("Content-Type: %s\n", mime) : "")
.append("\n") // headers end
.toString();
}
private void responseWithCache(OutputStream out, long offset) throws ProxyCacheException, IOException {
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
int readBytes;
while ((readBytes = read(buffer, offset, buffer.length)) != -1) {
out.write(buffer, 0, readBytes);
offset += readBytes;
}
out.flush();
}
private void responseWithoutCache(OutputStream out, long offset) throws ProxyCacheException, IOException {
HttpUrlSource newSourceNoCache = new HttpUrlSource(this.source);
try {
newSourceNoCache.open((int) offset);
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
int readBytes;
while ((readBytes = newSourceNoCache.read(buffer)) != -1) {
out.write(buffer, 0, readBytes);
offset += readBytes;
}
out.flush();
} finally {
newSourceNoCache.close();
}
}
@Override
protected void onCacheAvailable(int percents) {
protected void onCachePercentsAvailableChanged(int percents) {
if (listener != null) {
listener.onCacheAvailable(cache.file, source.url, percents);
listener.onCacheAvailable(cache.file, source.getUrl(), percents);
}
}
}

View File

@@ -1,21 +1,39 @@
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 com.danikula.videocache.sourcestorage.SourceInfoStorage;
import com.danikula.videocache.sourcestorage.SourceInfoStorageFactory;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeoutException;
import static com.danikula.videocache.Preconditions.checkAllNotNull;
import static com.danikula.videocache.Preconditions.checkNotNull;
import static com.danikula.videocache.ProxyCacheUtils.LOG_TAG;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
/**
* Simple lightweight proxy server with file caching support that handles HTTP requests.
@@ -39,6 +57,8 @@ import static com.danikula.videocache.ProxyCacheUtils.LOG_TAG;
public class HttpProxyCacheServer {
private static final String PROXY_HOST = "127.0.0.1";
private static final String PING_REQUEST = "ping";
private static final String PING_RESPONSE = "ping ok";
private final Object clientsLock = new Object();
private final ExecutorService socketProcessor = Executors.newFixedThreadPool(8);
@@ -46,10 +66,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);
@@ -58,13 +83,64 @@ public class HttpProxyCacheServer {
this.waitConnectionThread = new Thread(new WaitRequestsRunnable(startSignal));
this.waitConnectionThread.start();
startSignal.await(); // freeze thread, wait for server starts
Log.i(LOG_TAG, "Proxy cache server started. Ping it...");
makeSureServerWorks();
} catch (IOException | InterruptedException e) {
socketProcessor.shutdown();
throw new IllegalStateException("Error starting local proxy server", e);
}
}
private void makeSureServerWorks() {
int maxPingAttempts = 3;
int delay = 300;
int pingAttempts = 0;
while (pingAttempts < maxPingAttempts) {
try {
Future<Boolean> pingFuture = socketProcessor.submit(new PingCallable());
this.pinged = pingFuture.get(delay, MILLISECONDS);
if (this.pinged) {
return;
}
SystemClock.sleep(delay);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
Log.e(LOG_TAG, "Error pinging server [attempt: " + pingAttempts + ", timeout: " + delay + "]. ", e);
}
pingAttempts++;
delay *= 2;
}
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();
}
private boolean pingServer() throws ProxyCacheException {
String pingUrl = appendToProxyUrl(PING_REQUEST);
HttpUrlSource source = new HttpUrlSource(pingUrl);
try {
byte[] expectedResponse = PING_RESPONSE.getBytes();
source.open(0);
byte[] response = new byte[expectedResponse.length];
source.read(response);
boolean pingOk = Arrays.equals(expectedResponse, response);
Log.d(LOG_TAG, "Ping response: `" + new String(response) + "`, pinged? " + pingOk);
return pingOk;
} catch (ProxyCacheException e) {
Log.e(LOG_TAG, "Error reading ping response", e);
return false;
} finally {
source.close();
}
}
public String getProxyUrl(String url) {
if (!pinged) {
Log.e(LOG_TAG, "Proxy server isn't pinged. Caching doesn't work. If you see this message, please, email me danikula@gmail.com");
}
return pinged ? appendToProxyUrl(url) : url;
}
private String appendToProxyUrl(String url) {
return String.format("http://%s:%d/%s", PROXY_HOST, port, ProxyCacheUtils.encode(url));
}
@@ -99,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()) {
@@ -140,12 +232,16 @@ public class HttpProxyCacheServer {
GetRequest request = GetRequest.read(socket.getInputStream());
Log.i(LOG_TAG, "Request to cache proxy:" + request);
String url = ProxyCacheUtils.decode(request.uri);
HttpProxyCacheServerClients clients = getClients(url);
clients.processRequest(request, socket);
if (PING_REQUEST.equals(url)) {
responseToPing(socket);
} else {
HttpProxyCacheServerClients clients = getClients(url);
clients.processRequest(request, socket);
}
} catch (SocketException e) {
// There is no way to determine that client closed connection http://stackoverflow.com/a/10241044/999458
// So just to prevent log flooding don't log stacktrace
Log.d(LOG_TAG, "Client communication problem. It seems client closed connection");
Log.d(LOG_TAG, "Closing socket… Socket is closed by client.");
} catch (ProxyCacheException | IOException e) {
onError(new ProxyCacheException("Error processing request", e));
} finally {
@@ -154,11 +250,17 @@ public class HttpProxyCacheServer {
}
}
private void responseToPing(Socket socket) throws IOException {
OutputStream out = socket.getOutputStream();
out.write("HTTP/1.1 200 OK\n\n".getBytes());
out.write(PING_RESPONSE.getBytes());
}
private HttpProxyCacheServerClients getClients(String url) throws ProxyCacheException {
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;
@@ -189,7 +291,7 @@ public class HttpProxyCacheServer {
} catch (SocketException e) {
// There is no way to determine that client closed connection http://stackoverflow.com/a/10241044/999458
// So just to prevent log flooding don't log stacktrace
Log.d(LOG_TAG, "Error closing client's input stream: it seems client closed connection");
Log.d(LOG_TAG, "Releasing input stream… Socket is closed by client.");
} catch (IOException e) {
onError(new ProxyCacheException("Error closing socket input stream", e));
}
@@ -248,4 +350,103 @@ public class HttpProxyCacheServer {
}
}
private class PingCallable implements Callable<Boolean> {
@Override
public Boolean call() throws Exception {
return pingServer();
}
}
/**
* Builder for {@link HttpProxyCacheServer}.
*/
public static final class Builder {
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();
}
/**
* 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, sourceInfoStorage);
}
}
}

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);
}
@@ -77,8 +79,8 @@ final class HttpProxyCacheServerClients {
}
private HttpProxyCache newHttpProxyCache() throws ProxyCacheException {
HttpUrlSource source = new HttpUrlSource(url);
FileCache cache = new FileCache(fileNameGenerator.generate(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);
return httpProxyCache;

View File

@@ -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,10 +13,14 @@ 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;
import static java.net.HttpURLConnection.HTTP_MOVED_TEMP;
import static java.net.HttpURLConnection.HTTP_OK;
import static java.net.HttpURLConnection.HTTP_PARTIAL;
import static java.net.HttpURLConnection.HTTP_SEE_OTHER;
/**
* {@link Source} that uses http resource as source for {@link ProxyCache}.
@@ -22,89 +29,98 @@ import static java.net.HttpURLConnection.HTTP_PARTIAL;
*/
public class HttpUrlSource implements Source {
public final String url;
private static final int MAX_REDIRECTS = 5;
private final SourceInfoStorage sourceInfoStorage;
private SourceInfo sourceInfo;
private HttpURLConnection connection;
private InputStream inputStream;
private volatile int available = 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.sourceInfo = source.sourceInfo;
this.sourceInfoStorage = source.sourceInfoStorage;
}
@Override
public synchronized int available() throws ProxyCacheException {
if (available == Integer.MIN_VALUE) {
public synchronized int length() throws ProxyCacheException {
if (sourceInfo.length == Integer.MIN_VALUE) {
fetchContentInfo();
}
return available;
return sourceInfo.length;
}
@Override
public void open(int offset) throws ProxyCacheException {
try {
Log.d(LOG_TAG, "Open connection " + (offset > 0 ? " with offset " + offset : "") + " to " + url);
connection = (HttpURLConnection) new URL(url).openConnection();
if (offset > 0) {
connection.setRequestProperty("Range", "bytes=" + offset + "-");
}
mime = connection.getContentType();
connection = openConnection(offset, -1);
String mime = connection.getContentType();
inputStream = new BufferedInputStream(connection.getInputStream(), DEFAULT_BUFFER_SIZE);
available = readSourceAvailableBytes(connection, offset);
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) throws IOException {
private int readSourceAvailableBytes(HttpURLConnection connection, int offset, int responseCode) throws IOException {
int contentLength = connection.getContentLength();
int responseCode = connection.getResponseCode();
return responseCode == HTTP_OK ? contentLength :
responseCode == HTTP_PARTIAL ? contentLength + offset :
available;
return responseCode == HTTP_OK ? contentLength
: 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 = (HttpURLConnection) new URL(url).openConnection();
urlConnection.setConnectTimeout(10000);
urlConnection.setReadTimeout(10000);
urlConnection.setRequestMethod("HEAD");
available = 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: " + available);
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) {
throw new ProxyCacheException("Error fetching Content-Length from " + url);
Log.e(LOG_TAG, "Error fetching info from " + sourceInfo.url, e);
} finally {
ProxyCacheUtils.close(inputStream);
if (urlConnection != null) {
@@ -113,15 +129,48 @@ public class HttpUrlSource implements Source {
}
}
private HttpURLConnection openConnection(int offset, int timeout) throws IOException, ProxyCacheException {
HttpURLConnection connection;
boolean redirected;
int redirectCount = 0;
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();
if (offset > 0) {
connection.setRequestProperty("Range", "bytes=" + offset + "-");
}
if (timeout > 0) {
connection.setConnectTimeout(timeout);
connection.setReadTimeout(timeout);
}
int code = connection.getResponseCode();
redirected = code == HTTP_MOVED_PERM || code == HTTP_MOVED_TEMP || code == HTTP_SEE_OTHER;
if (redirected) {
url = connection.getHeaderField("Location");
redirectCount++;
connection.disconnect();
}
if (redirectCount > MAX_REDIRECTS) {
throw new ProxyCacheException("Too many redirects: " + redirectCount);
}
} while (redirected);
return connection;
}
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 sourceInfo.url;
}
@Override
public String toString() {
return "HttpUrlSource{url='" + url + "}";
return "HttpUrlSource{sourceInfo='" + sourceInfo + "}";
}
}

View File

@@ -1,56 +0,0 @@
package com.danikula.videocache;
import android.text.TextUtils;
import java.io.File;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import static com.danikula.videocache.Preconditions.checkNotNull;
/**
* Implementation of {@link FileNameGenerator} that uses MD5 of url as file name
*
* @author Alexey Danilov (danikula@gmail.com).
*/
public class Md5FileNameGenerator implements FileNameGenerator {
private final File cacheDirectory;
public Md5FileNameGenerator(File cacheDirectory) {
this.cacheDirectory = checkNotNull(cacheDirectory);
}
@Override
public File generate(String url) {
checkNotNull(url);
String extension = getExtension(url);
String name = computeMD5(url);
name = TextUtils.isEmpty(extension) ? name : name + "." + extension;
return new File(cacheDirectory, name);
}
private String getExtension(String url) {
int dotIndex = url.lastIndexOf('.');
int slashIndex = url.lastIndexOf(File.separator);
return dotIndex != -1 && dotIndex > slashIndex ? url.substring(dotIndex + 1, url.length()) : "";
}
private String computeMD5(String string) {
try {
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
byte[] digestBytes = messageDigest.digest(string.getBytes());
return bytesToHexString(digestBytes);
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
}
}
private String bytesToHexString(byte[] bytes) {
StringBuffer sb = new StringBuffer();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
}

View File

@@ -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);
}

View File

@@ -24,9 +24,10 @@ class ProxyCache {
private final Cache cache;
private final Object wc = new Object();
private final Object stopLock = new Object();
private final AtomicInteger readSourceErrorsCount;
private volatile Thread sourceReaderThread;
private volatile boolean stopped;
private final AtomicInteger readSourceErrorsCount;
private volatile int percentsAvailable = -1;
public ProxyCache(Source source, Cache cache) {
this.source = checkNotNull(source);
@@ -42,7 +43,12 @@ class ProxyCache {
waitForSourceData();
checkReadSourceErrorsCount();
}
return cache.read(buffer, offset, length);
int read = cache.read(buffer, offset, length);
if (cache.isCompleted() && percentsAvailable != 100) {
percentsAvailable = 100;
onCachePercentsAvailableChanged(100);
}
return read;
}
private void checkReadSourceErrorsCount() throws ProxyCacheException {
@@ -86,22 +92,35 @@ class ProxyCache {
}
}
private void notifyNewCacheDataAvailable(int cachePercentage) {
onCacheAvailable(cachePercentage);
private void notifyNewCacheDataAvailable(long cacheAvailable, long sourceAvailable) {
onCacheAvailable(cacheAvailable, sourceAvailable);
synchronized (wc) {
wc.notifyAll();
}
}
protected void onCacheAvailable(int percents) {
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 = sourceLength >= 0;
if (sourceLengthKnown && percentsChanged) {
onCachePercentsAvailableChanged(percents);
}
percentsAvailable = percents;
}
protected void onCachePercentsAvailableChanged(int percentsAvailable) {
}
private void readSource() {
int cachePercentage = 0;
int sourceAvailable = -1;
int offset = 0;
try {
int offset = cache.available();
offset = cache.available();
source.open(offset);
sourceAvailable = source.length();
byte[] buffer = new byte[ProxyCacheUtils.DEFAULT_BUFFER_SIZE];
int readBytes;
while ((readBytes = source.read(buffer)) != -1) {
@@ -112,23 +131,28 @@ class ProxyCache {
cache.append(buffer, readBytes);
}
offset += readBytes;
cachePercentage = offset * 100 / source.available();
notifyNewCacheDataAvailable(cachePercentage);
notifyNewCacheDataAvailable(offset, sourceAvailable);
}
tryComplete();
onSourceRead();
} catch (Throwable e) {
readSourceErrorsCount.incrementAndGet();
onError(e);
} finally {
closeSource();
notifyNewCacheDataAvailable(cachePercentage);
notifyNewCacheDataAvailable(offset, sourceAvailable);
}
}
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.available()) {
if (!isStopped() && cache.available() == source.length()) {
cache.complete();
}
}

View File

@@ -5,11 +5,12 @@ 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;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import static com.danikula.videocache.Preconditions.checkArgument;
@@ -20,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;
@@ -48,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");
@@ -86,4 +74,22 @@ class ProxyCacheUtils {
}
}
}
public static String computeMD5(String string) {
try {
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
byte[] digestBytes = messageDigest.digest(string.getBytes());
return bytesToHexString(digestBytes);
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
}
}
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

@@ -7,11 +7,34 @@ package com.danikula.videocache;
*/
public interface Source {
int available() throws ProxyCacheException;
/**
* Opens source. Source should be open before using {@link #read(byte[])}
*
* @param offset offset in bytes for source.
* @throws ProxyCacheException if error occur while opening source.
*/
void open(int offset) throws ProxyCacheException;
void close() throws ProxyCacheException;
/**
* Returns length bytes or <b>negative value</b> if length is unknown.
*
* @return bytes length
* @throws ProxyCacheException if error occur while fetching source data.
*/
int length() throws ProxyCacheException;
/**
* Read data to byte buffer from source with current offset.
*
* @param buffer a buffer to be used for reading data.
* @throws ProxyCacheException if error occur while reading source.
*/
int read(byte[] buffer) throws ProxyCacheException;
/**
* Closes source and release resources. Every opened source should be closed.
*
* @throws ProxyCacheException if error occur while closing source.
*/
void close() throws ProxyCacheException;
}

View File

@@ -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 + '\'' +
'}';
}
}

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

@@ -0,0 +1,29 @@
package com.danikula.videocache.file;
import android.text.TextUtils;
import com.danikula.videocache.ProxyCacheUtils;
/**
* Implementation of {@link FileNameGenerator} that uses MD5 of url as file name
*
* @author Alexey Danilov (danikula@gmail.com).
*/
public class Md5FileNameGenerator implements FileNameGenerator {
private static final int MAX_EXTENSION_LENGTH = 4;
@Override
public String generate(String url) {
String extension = getExtension(url);
String name = ProxyCacheUtils.computeMD5(url);
return TextUtils.isEmpty(extension) ? name : name + "." + extension;
}
private String getExtension(String url) {
int dotIndex = url.lastIndexOf('.');
int slashIndex = url.lastIndexOf('/');
return dotIndex != -1 && dotIndex > slashIndex && dotIndex + 2 + MAX_EXTENSION_LENGTH > url.length() ?
url.substring(dotIndex + 1, url.length()) : "";
}
}

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

@@ -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;
}
}

View File

@@ -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() {
}
}

View File

@@ -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();
}

View File

@@ -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();
}
}

View File

@@ -8,7 +8,6 @@ buildscript {
}
repositories {
maven { url 'https://dl.bintray.com/alexeydanilov/maven' }
maven { url 'https://github.com/dahlgren/vpi-aar/raw/master' }
}
@@ -17,10 +16,10 @@ apply plugin: 'com.neenbedankt.android-apt'
android {
compileSdkVersion 23
buildToolsVersion '23.0.0'
buildToolsVersion '24'
defaultConfig {
applicationId "com.danikula.videocache.sample"
applicationId 'com.danikula.videocache.sample'
minSdkVersion 15
targetSdkVersion 23
versionCode 1
@@ -37,9 +36,9 @@ apt {
dependencies {
// compile project(':library')
compile 'com.android.support:support-v4:23.0.0'
compile 'com.android.support:support-v4:23.1.0'
compile 'org.androidannotations:androidannotations-api:3.3.2'
compile 'com.danikula:videocache:2.0.9'
compile 'com.danikula:videocache:2.6.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

@@ -3,9 +3,11 @@ package com.danikula.videocache.sample;
import android.content.Intent;
import android.support.annotation.NonNull;
import android.support.v4.app.FragmentActivity;
import android.util.Log;
import android.widget.ArrayAdapter;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.Toast;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
@@ -13,7 +15,7 @@ import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.ItemClick;
import org.androidannotations.annotations.ViewById;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
@@ -46,11 +48,11 @@ public class MenuActivity extends FragmentActivity {
@Click(R.id.cleanCacheButton)
void onClearCacheButtonClick() {
File externalCacheDir = getExternalCacheDir();
if (externalCacheDir != null) {
for (File cacheEntry : externalCacheDir.listFiles()) {
cacheEntry.delete();
}
try {
Utils.cleanDirectory(getExternalCacheDir());
} catch (IOException e) {
Log.e(null, "Error cleaning cache", e);
Toast.makeText(this, "Error cleaning cache", Toast.LENGTH_LONG).show();
}
}

View File

@@ -0,0 +1,42 @@
package com.danikula.videocache.sample;
import java.io.File;
import java.io.IOException;
/**
* Some utils methods.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
public class Utils {
public static void cleanDirectory(File file) throws IOException {
if (!file.exists()) {
return;
}
File[] contentFiles = file.listFiles();
if (contentFiles != null) {
for (File contentFile : contentFiles) {
delete(contentFile);
}
}
}
private static void delete(File file) throws IOException {
if (file.isFile() && file.exists()) {
deleteOrThrow(file);
} else {
cleanDirectory(file);
deleteOrThrow(file);
}
}
private static void deleteOrThrow(File file) throws IOException {
if (file.exists()) {
boolean isDeleted = file.delete();
if (!isDeleted) {
throw new IOException(String.format("File %s can't be deleted", file.getAbsolutePath()));
}
}
}
}

View File

@@ -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/";
}
}

View File

@@ -4,6 +4,8 @@ import android.content.Context;
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;
@@ -26,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;
@@ -44,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);
@@ -77,6 +87,8 @@ 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));
}
private void updateVideoProgress() {
@@ -90,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() {

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 417 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 672 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 610 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 855 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 789 B

View File

@@ -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"

View File

@@ -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') {

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

@@ -0,0 +1,79 @@
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;
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;
import static org.fest.assertions.api.Assertions.fail;
/**
* Tests for {@link FileNameGenerator} and implementations.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class)
public class FileNameGeneratorTest {
@Test
public void testMd5SimpleName() throws Exception {
String url = "http://host.com/videos/video.mpeg";
String path = generateMd5Name("/home", url);
String expected = "/home/" + ProxyCacheUtils.computeMD5(url) + ".mpeg";
assertThat(path).isEqualTo(expected);
}
@Test
public void testMd5NoExtension() throws Exception {
String url = "http://host.com/video";
String path = generateMd5Name("/home", url);
String expected = "/home/" + ProxyCacheUtils.computeMD5(url);
assertThat(path).isEqualTo(expected);
}
@Test
public void testMd5TooLongExtension() throws Exception {
String url = "http://host.com/videos/video-with-dot-.12345";
String path = generateMd5Name("/home", url);
String expected = "/home/" + ProxyCacheUtils.computeMD5(url);
assertThat(path).isEqualTo(expected);
}
@Test
public void testMd5InvalidExtension() throws Exception {
String url = "http://host.com/videos/video.mp4?token=-648729473536183645";
String path = generateMd5Name("/home", url);
String expected = "/home/" + ProxyCacheUtils.computeMD5(url);
assertThat(path).isEqualTo(expected);
}
@Test
public void testMd5ExtraLongExtension() throws Exception {
// https://github.com/danikula/AndroidVideoCache/issues/14
String url = "https://d1wst0behutosd.cloudfront.net/videos/4367900/10807247.480p.mp4?Expires=1442849176&Signature=JXV~3AoI0rWcGuZBywg3-ukf6Ycw2X8v7Htog3lyvuFwp8o6VUEDFUsTC9-XtIGu-ULxCd7dP3fvB306lRyGFxdvf-sXLX~ar~HCQ7lullNyeLtp8BJOT5Y~W5rJE7X-AZaueNcycGtLFRhRtr5ySTguwtmJNaO3T1apX~-oVrFh1dWStEKbuPoXY04RgkmhMHoFgtwgXMC1ctIDeQHxZeXLi6LLyZnQsgzlUDffCx4P16iiW0uh2-Z~HUOi9BLBwHMQ5k5lYwZqdQ6DhhYoWlniRfQz6mp1IEiMgr4L3Z1ijgGITV4cYeF31CmFzCxaJTE7IIAC5tMDQSTt7M9Q4A__&Key-Pair-Id=APKAJJ6WELAPEP47UKWQ";
String path = generateMd5Name("/home", url);
String expected = "/home/" + ProxyCacheUtils.computeMD5(url);
assertThat(path).isEqualTo(expected);
}
@Test(expected = NullPointerException.class)
public void testAssertNullUrl() throws Exception {
FileNameGenerator nameGenerator = new Md5FileNameGenerator();
nameGenerator.generate(null);
fail("Url should be not null");
}
private String generateMd5Name(String rootFolder, String url) {
FileNameGenerator nameGenerator = new Md5FileNameGenerator();
String name = nameGenerator.generate(url);
return new File(rootFolder, name).getAbsolutePath();
}
}

View File

@@ -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

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,11 +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;
@@ -31,9 +41,18 @@ 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;
@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);
@@ -45,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();
@@ -63,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");
}
@@ -81,9 +100,23 @@ public class HttpProxyCacheServerTest {
assertThat(response.second.data).isEqualTo(loadAssetFile(ASSETS_DATA_BIG_NAME));
}
@Test
public void testProxyFullResponseWithRedirect() throws Exception {
Pair<File, Response> response = readProxyData(HTTP_DATA_BIG_URL_ONE_REDIRECT);
assertThat(response.second.code).isEqualTo(200);
assertThat(response.second.contentLength).isEqualTo(HTTP_DATA_BIG_SIZE);
assertThat(response.second.contentType).isEqualTo("image/jpeg");
assertThat(response.second.headers.containsKey("Accept-Ranges")).isTrue();
assertThat(response.second.headers.get("Accept-Ranges").get(0)).isEqualTo("bytes");
assertThat(response.second.headers.containsKey("Content-Range")).isFalse();
assertThat(response.second.data).isEqualTo(getFileContent(response.first));
assertThat(response.second.data).isEqualTo(loadAssetFile(ASSETS_DATA_BIG_NAME));
}
@Test
public void testProxyPartialResponse() throws Exception {
int offset = 42000;
int offset = 18000;
Pair<File, Response> response = readProxyData(HTTP_DATA_BIG_URL, offset);
assertThat(response.second.code).isEqualTo(206);
@@ -92,18 +125,140 @@ public class HttpProxyCacheServerTest {
assertThat(response.second.headers.containsKey("Accept-Ranges")).isTrue();
assertThat(response.second.headers.get("Accept-Ranges").get(0)).isEqualTo("bytes");
assertThat(response.second.headers.containsKey("Content-Range")).isTrue();
String rangeHeader = String.format("bytes %d-%d/%d", offset, HTTP_DATA_BIG_SIZE, HTTP_DATA_BIG_SIZE);
String rangeHeader = String.format("bytes %d-%d/%d", offset, HTTP_DATA_BIG_SIZE - 1, HTTP_DATA_BIG_SIZE);
assertThat(response.second.headers.get("Content-Range").get(0)).isEqualTo(rangeHeader);
byte[] expectedData = Arrays.copyOfRange(loadAssetFile(ASSETS_DATA_BIG_NAME), offset, HTTP_DATA_BIG_SIZE);
assertThat(response.second.data).isEqualTo(expectedData);
assertThat(getFileContent(response.first)).isEqualTo(loadAssetFile(ASSETS_DATA_BIG_NAME));
}
@Test
public void testProxyPartialResponseWithRedirect() throws Exception {
int offset = 18000;
Pair<File, Response> response = readProxyData(HTTP_DATA_BIG_URL_ONE_REDIRECT, offset);
assertThat(response.second.code).isEqualTo(206);
assertThat(response.second.contentLength).isEqualTo(HTTP_DATA_BIG_SIZE - offset);
assertThat(response.second.contentType).isEqualTo("image/jpeg");
assertThat(response.second.headers.containsKey("Accept-Ranges")).isTrue();
assertThat(response.second.headers.get("Accept-Ranges").get(0)).isEqualTo("bytes");
assertThat(response.second.headers.containsKey("Content-Range")).isTrue();
String rangeHeader = String.format("bytes %d-%d/%d", offset, HTTP_DATA_BIG_SIZE - 1, HTTP_DATA_BIG_SIZE);
assertThat(response.second.headers.get("Content-Range").get(0)).isEqualTo(rangeHeader);
byte[] expectedData = Arrays.copyOfRange(loadAssetFile(ASSETS_DATA_BIG_NAME), offset, HTTP_DATA_BIG_SIZE);
assertThat(response.second.data).isEqualTo(expectedData);
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();
}
@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();
FileNameGenerator fileNameGenerator = new Md5FileNameGenerator(externalCacheDir);
File file = fileNameGenerator.generate(url);
HttpProxyCacheServer proxy = new HttpProxyCacheServer(fileNameGenerator);
File file = file(cacheFolder, url);
HttpProxyCacheServer proxy = newProxy(cacheFolder);
Response response = readProxyResponse(proxy, url, offset);
proxy.shutdown();
@@ -111,7 +266,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

@@ -0,0 +1,265 @@
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;
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;
/**
* Test {@link HttpProxyCache}.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class)
public class HttpProxyCacheTest {
@Test
public void testProcessRequestNoCache() throws Exception {
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(HTTP_DATA_SIZE);
assertThat(response.contentType).isEqualTo("image/jpeg");
}
@Test
public void testProcessPartialRequestWithoutCache() throws Exception {
FileCache fileCache = new FileCache(ProxyCacheTestUtils.newCacheFile());
FileCache spyFileCache = Mockito.spy(fileCache);
doThrow(new RuntimeException()).when(spyFileCache).read(any(byte[].class), anyLong(), anyInt());
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];
System.arraycopy(fullData, 2000, partialData, 0, partialData.length);
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);
}
});
}
}

View File

@@ -1,29 +1,40 @@
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;
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.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.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
@@ -58,7 +69,65 @@ public class HttpUrlSourceTest {
@Test
public void testFetchContentLength() throws Exception {
Source source = new HttpUrlSource(HTTP_DATA_URL);
assertThat(source.available()).isEqualTo(loadAssetFile(ASSETS_DATA_NAME).length);
assertThat(source.length()).isEqualTo(loadAssetFile(ASSETS_DATA_NAME).length);
}
@Test
public void testFetchInfoWithRedirect() throws Exception {
HttpUrlSource source = new HttpUrlSource(HTTP_DATA_URL_ONE_REDIRECT);
source.open(0);
int available = source.length();
String mime = source.getMime();
source.close();
assertThat(available).isEqualTo(HTTP_DATA_SIZE);
assertThat(mime).isEqualTo("image/jpeg");
}
@Test
public void testFetchDataWithRedirect() throws Exception {
HttpUrlSource source = new HttpUrlSource(HTTP_DATA_URL_ONE_REDIRECT);
source.open(0);
byte[] readData = new byte[HTTP_DATA_SIZE];
readSource(source, readData);
source.close();
byte[] expectedData = Arrays.copyOfRange(loadAssetFile(ASSETS_DATA_NAME), 0, HTTP_DATA_SIZE);
assertThat(readData).isEqualTo(expectedData);
}
@Test
public void testFetchPartialDataWithRedirect() throws Exception {
int offset = 42;
HttpUrlSource source = new HttpUrlSource(HTTP_DATA_URL_ONE_REDIRECT);
source.open(offset);
byte[] readData = new byte[HTTP_DATA_SIZE - offset];
readSource(source, readData);
source.close();
byte[] expectedData = Arrays.copyOfRange(loadAssetFile(ASSETS_DATA_NAME), offset, HTTP_DATA_SIZE);
assertThat(readData).isEqualTo(expectedData);
}
@Test
public void testFetchPartialDataWithMultiRedirects() throws Exception {
int offset = 42;
HttpUrlSource source = new HttpUrlSource(HTTP_DATA_URL_3_REDIRECTS);
source.open(offset);
byte[] readData = new byte[HTTP_DATA_SIZE - offset];
readSource(source, readData);
source.close();
byte[] expectedData = Arrays.copyOfRange(loadAssetFile(ASSETS_DATA_NAME), offset, HTTP_DATA_SIZE);
assertThat(readData).isEqualTo(expectedData);
}
@Ignore("To test it fairly we should disable caching connection.setUseCaches(false), but it will decrease performance")
@Test(expected = ProxyCacheException.class)
public void testExceedingRedirects() throws Exception {
HttpUrlSource source = new HttpUrlSource(HTTP_DATA_URL_6_REDIRECTS);
source.open(0);
fail("Too many redirects");
}
@Ignore("Seems Robolectric bug: MimeTypeMap.getFileExtensionFromUrl always returns null")
@@ -67,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;
}
}
}

View File

@@ -1,8 +1,8 @@
package com.danikula.videocache;
import com.danikula.android.garden.io.IoUtils;
import com.danikula.videocache.support.AngryHttpUrlSource;
import com.danikula.videocache.support.PhlegmaticByteArraySource;
import com.danikula.videocache.file.FileCache;
import com.danikula.videocache.support.ProxyCacheTestUtils;
import com.danikula.videocache.test.BuildConfig;
import org.junit.Test;
@@ -22,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
@@ -94,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);
@@ -105,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);
@@ -117,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);
@@ -175,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);

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)
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;
@@ -26,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

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)
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

@@ -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");
}
}

View File

@@ -1,32 +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).
*/
public class AngryHttpUrlSource implements Source {
@Override
public int available() 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();
}
}

View File

@@ -1,31 +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).
*/
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);
}
}

View File

@@ -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,13 +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_BIG_URL = "https://dl.dropboxusercontent.com/u/15506779/persistent/proxycache/phones.jpg";
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;
@@ -36,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) {
@@ -48,6 +70,10 @@ public class ProxyCacheTestUtils {
}
}
public static byte[] loadTestData() throws IOException {
return loadAssetFile(ASSETS_DATA_NAME);
}
public static byte[] loadAssetFile(String name) throws IOException {
InputStream in = RuntimeEnvironment.application.getResources().getAssets().open(name);
ByteArrayOutputStream out = new ByteArrayOutputStream();
@@ -71,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;
}
}

View File

@@ -1,14 +1,27 @@
package com.danikula.videocache.support;
import android.text.TextUtils;
import com.google.common.base.Preconditions;
import com.google.common.io.ByteStreams;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.net.HttpURLConnection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class Response {
private static final String CONTENT_TYPE_HEADER = "Content-Type";
private static final String CONTENT_LENGTH_HEADER = "Content-Length";
private static final Pattern STATUS_CODE_PATTERN = Pattern.compile("HTTP/1.1 (\\d{3}) ");
public final int code;
public final byte[] data;
public final int contentLength;
@@ -22,4 +35,33 @@ public class Response {
this.headers = connection.getHeaderFields();
this.data = ByteStreams.toByteArray(connection.getInputStream());
}
public Response(byte[] responseData) throws IOException {
int read = 0;
BufferedReader reader = new BufferedReader(new StringReader(new String(responseData, "ascii")));
String statusLine = reader.readLine();
read += statusLine.length() + 1;
Matcher matcher = STATUS_CODE_PATTERN.matcher(statusLine);
boolean hasCode = matcher.find();
Preconditions.checkArgument(hasCode, "Status code not found in `" + statusLine + "`");
this.code = Integer.parseInt(matcher.group(1));
String header;
this.headers = new HashMap<>();
while (!TextUtils.isEmpty(header = reader.readLine())) {
read += header.length() + 1;
String[] keyValue = header.split(":");
String headerName = keyValue[0].trim();
String headerValue = keyValue[1].trim();
headers.put(headerName, Collections.singletonList(headerValue));
}
read++;
this.contentType = headers.containsKey(CONTENT_TYPE_HEADER) ? headers.get(CONTENT_TYPE_HEADER).get(0) : null;
this.contentLength = headers.containsKey(CONTENT_LENGTH_HEADER) ? Integer.parseInt(headers.get(CONTENT_LENGTH_HEADER).get(0)) : -1;
int bodySize = responseData.length - read;
this.data = new byte[bodySize];
System.arraycopy(responseData, read, data, 0, bodySize);
}
}