12 Commits

Author SHA1 Message Date
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
Alexey Danilov
f8f19c5a5c optimize featching source data 2015-09-11 15:37:21 +03:00
Alexey Danilov
c0ef7dd1b1 update README for 2.0 2015-09-09 17:49:57 +03:00
Alexey Danilov
5115b96a9e allow to use shared proxy with shared cache for multiple clients 2015-09-09 17:49:49 +03:00
Alexey Danilov
7f22a66941 simplify percentage and errors delivering 2015-09-09 17:31:51 +03:00
Alexey Danilov
983ae8d7b7 add more samples: multiple video, gallery with preloading, shared cache 2015-09-03 19:26:09 +03:00
Alexey Danilov
492706ce69 update build tools 2015-09-02 16:12:35 +03:00
Alexey Danilov
a57acc1ad5 update dependency 2015-06-12 13:29:11 +03:00
43 changed files with 1837 additions and 604 deletions

View File

@@ -2,7 +2,8 @@
[![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-AndroidVideoCache-brightgreen.svg?style=flat)](http://android-arsenal.com/details/1/1751)
## Why AndroidVideoCache?
Because android MediaPlayer doesn't cache video while streaming.
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:
@@ -11,43 +12,70 @@ repositories {
maven { url 'https://dl.bintray.com/alexeydanilov/maven' }
}
dependencies {
compile 'com.danikula:videocache:1.0.1'
compile 'com.danikula:videocache:2.1.1'
}
```
and use proxy for caching video:
and use url from proxy instead of original url for adding caching:
```java
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
try {
Cache cache = new FileCache(new File(getExternalCacheDir(), VIDEO_CACHE_NAME));
HttpUrlSource source = new HttpUrlSource(VIDEO_URL);
proxyCache = new HttpProxyCache(source, cache);
videoView.setVideoPath(proxyCache.getUrl());
videoView.start();
} catch (ProxyCacheException e) {
Log.e(LOG_TAG, "Error playing video", e);
}
super.onCreate(savedInstanceState);
HttpProxyCacheServer proxy = getProxy();
String proxyUrl = proxy.getProxyUrl(VIDEO_URL);
videoView.setVideoPath(proxyUrl);
}
@Override
public void onDestroy() {
super.onDestroy();
private HttpProxyCacheServer getProxy() {
// should return single instance of HttpProxyCacheServer shared for whole app.
}
```
if (proxyCache != null) {
proxyCache.shutdown();
To guarantee normal work you should use **single** instance of `HttpProxyCacheServer` for whole app.
For example you can store shared proxy on your `Application`:
```java
public class App extends Application {
private HttpProxyCacheServer proxy;
public static HttpProxyCacheServer getProxy(Context context) {
App app = (App) context.getApplicationContext();
return app.proxy == null ? (app.proxy = app.newProxy()) : app.proxy;
}
private HttpProxyCacheServer newProxy() {
FileNameGenerator nameGenerator = new Md5FileNameGenerator(getExternalCacheDir());
return new HttpProxyCacheServer(nameGenerator);
}
}
```
or use [simple factory](http://pastebin.com/38uNkgBT).
More preferable way is use some dependency injector like [Dagger](http://square.github.io/dagger/).
See `sample` app for details.
## Whats new
### 2.1
- fix for too long cache file name
- url redirects support (thanks [ongakuer](https://github.com/ongakuer) for [PR](https://github.com/danikula/AndroidVideoCache/pull/12))
### 2.0
- simpler api
- single cache for multiple clients
- cache file name policy
- more powerful listener
- more samples
- less log flood
## Where published?
[Here](https://bintray.com/alexeydanilov/maven/videocache/view)
## Questions?
[danikula@gmail.com](mailto:danikula@gmail.com)
## License
Copyright 2014-2015 Alexey Danilov

View File

@@ -3,7 +3,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.2.3'
classpath 'com.android.tools.build:gradle:1.3.0'
}
}

View File

@@ -7,9 +7,17 @@ buildscript {
}
}
apply plugin: 'idea'
apply plugin: 'java'
apply plugin: 'bintray-release'
idea {
module {
downloadJavadoc = true
downloadSources = true
}
}
dependencies {
compile 'com.google.android:android:1.6_r2'
}
@@ -18,7 +26,7 @@ publish {
userOrg = 'alexeydanilov'
groupId = 'com.danikula'
artifactId = 'videocache'
publishVersion = '1.0.1'
publishVersion = '2.1.1'
description = 'Cache support for android VideoView'
website = 'https://github.com/danikula/AndroidVideoCache'
}

View File

@@ -3,9 +3,6 @@ package com.danikula.videocache;
import java.io.ByteArrayInputStream;
import java.util.Arrays;
import static com.danikula.videocache.Preconditions.checkArgument;
import static com.danikula.videocache.Preconditions.checkNotNull;
/**
* Simple memory based {@link Cache} implementation.
*
@@ -24,7 +21,6 @@ public class ByteArrayCache implements Cache {
this.data = Preconditions.checkNotNull(data);
}
@Override
public int read(byte[] buffer, long offset, int length) throws ProxyCacheException {
if (offset >= data.length) {

View File

@@ -1,10 +1,14 @@
package com.danikula.videocache;
import java.io.File;
/**
* @author Egor Makovsky (yahor.makouski@gmail.com).
* Listener for cache availability.
*
* @author Egor Makovsky (yahor.makouski@gmail.com)
* @author Alexey Danilov (danikula@gmail.com).
*/
public interface CacheListener {
void onError(ProxyCacheException e);
void onCacheDataAvailable(int cachePercentage);
void onCacheAvailable(File cacheFile, String url, int percentsAvailable);
}

View File

@@ -15,22 +15,16 @@ public class FileCache implements Cache {
private static final String TEMP_POSTFIX = ".download";
public File file;
private RandomAccessFile dataFile;
private File file;
public FileCache(File file) throws ProxyCacheException {
try {
checkNotNull(file);
boolean partialFile = isTempFile(file);
boolean completed = file.exists() && !partialFile;
if (completed) {
this.dataFile = new RandomAccessFile(file, "r");
this.file = file;
} else {
ProxyCacheUtils.createDirectory(file.getParentFile());
this.file = partialFile ? file : new File(file.getParentFile(), file.getName() + TEMP_POSTFIX);
this.dataFile = new RandomAccessFile(this.file, "rw");
}
ProxyCacheUtils.createDirectory(file.getParentFile());
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");
} catch (IOException e) {
throw new ProxyCacheException("Error using file " + file + " as disc cache", e);
}
@@ -41,7 +35,7 @@ public class FileCache implements Cache {
try {
return (int) dataFile.length();
} catch (IOException e) {
throw new ProxyCacheException("Error reading length of file " + dataFile, e);
throw new ProxyCacheException("Error reading length of file " + file, e);
}
}
@@ -117,4 +111,5 @@ public class FileCache implements Cache {
private boolean isTempFile(File file) {
return file.getName().endsWith(TEMP_POSTFIX);
}
}

View File

@@ -0,0 +1,14 @@
package com.danikula.videocache;
import java.io.File;
/**
* Generator for files to be used for caching.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
public interface FileNameGenerator {
File generate(String url);
}

View File

@@ -0,0 +1,71 @@
package com.danikula.videocache;
import android.text.TextUtils;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.danikula.videocache.Preconditions.checkNotNull;
/**
* Model for Http GET request.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
class GetRequest {
private static final Pattern RANGE_HEADER_PATTERN = Pattern.compile("[R,r]ange:[ ]?bytes=(\\d*)-");
private static final Pattern URL_PATTERN = Pattern.compile("GET /(.*) HTTP");
public final String uri;
public final long rangeOffset;
public final boolean partial;
public GetRequest(String request) {
checkNotNull(request);
long offset = findRangeOffset(request);
this.rangeOffset = Math.max(0, offset);
this.partial = offset >= 0;
this.uri = findUri(request);
}
public static GetRequest read(InputStream inputStream) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
StringBuilder stringRequest = new StringBuilder();
String line;
while (!TextUtils.isEmpty(line = reader.readLine())) { // until new line (headers ending)
stringRequest.append(line).append('\n');
}
return new GetRequest(stringRequest.toString());
}
private long findRangeOffset(String request) {
Matcher matcher = RANGE_HEADER_PATTERN.matcher(request);
if (matcher.find()) {
String rangeValue = matcher.group(1);
return Long.parseLong(rangeValue);
}
return -1;
}
private String findUri(String request) {
Matcher matcher = URL_PATTERN.matcher(request);
if (matcher.find()) {
return matcher.group(1);
}
throw new IllegalArgumentException("Invalid request `" + request + "`: url not found!");
}
@Override
public String toString() {
return "GetRequest{" +
"uri='" + uri + '\'' +
", rangeOffset=" + rangeOffset +
", partial=" + partial +
'}';
}
}

View File

@@ -1,240 +1,76 @@
package com.danikula.videocache;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* {@link ProxyCache} that uses local server to handle requests and cache data.
* Typical usage:
* <pre><code>
* HttpProxyCache proxy;
* public onCreate(Bundle state) {
* super.onCreate(state);
* ...
* try{
* HttpUrlSource source = new HttpUrlSource(YOUR_VIDEO_URI);
* Cache cache = new FileCache(new File(context.getCacheDir(), "video.mp4"));
* proxy = new HttpProxyCache(source, cache);
* videoView.setVideoPath(proxy.getUrl());
* } catch(ProxyCacheException e) {
* Log.e(LOG_TAG, "Error playing video", e);
* }
* }
* public onDestroy(){
* super.onDestroy();
*
* if (proxy != null) {
* proxy.shutdown();
* }
* }
* <code/></pre>
* {@link ProxyCache} that read http url and writes data to {@link Socket}
*
* @author Alexey Danilov (danikula@gmail.com).
*/
public class HttpProxyCache extends ProxyCache {
class HttpProxyCache extends ProxyCache {
private static final int CLIENT_COUNT = 3;
private static final Pattern RANGE_HEADER_PATTERN = Pattern.compile("[R,r]ange:[ ]?bytes=(\\d*)-");
private static final String PROXY_HOST = "127.0.0.1";
private final HttpUrlSource source;
private final FileCache cache;
private CacheListener listener;
private final HttpUrlSource httpUrlSource;
private final Cache cache;
private final ServerSocket serverSocket;
private final int port;
private final Thread waitConnectionThread;
private final ExecutorService executorService;
public HttpProxyCache(HttpUrlSource source, Cache cache, boolean logEnabled) throws ProxyCacheException {
super(source, cache, logEnabled);
this.httpUrlSource = source;
public HttpProxyCache(HttpUrlSource source, FileCache cache) {
super(source, cache);
this.cache = cache;
this.executorService = Executors.newFixedThreadPool(CLIENT_COUNT);
try {
InetAddress inetAddress = InetAddress.getByName(PROXY_HOST);
this.serverSocket = new ServerSocket(0, CLIENT_COUNT, inetAddress);
this.port = serverSocket.getLocalPort();
CountDownLatch startSignal = new CountDownLatch(1);
this.waitConnectionThread = new Thread(new WaitRequestsRunnable(startSignal));
this.waitConnectionThread.start();
startSignal.await(); // freeze thread, wait for server starts
} catch (IOException | InterruptedException e) {
executorService.shutdown();
throw new ProxyCacheException("Error starting local server", e);
}
this.source = source;
}
public HttpProxyCache(HttpUrlSource source, Cache cache) throws ProxyCacheException {
this(source, cache, false);
public void registerCacheListener(CacheListener cacheListener) {
this.listener = cacheListener;
}
public String getUrl() {
return "http://" + PROXY_HOST + ":" + port + Uri.parse(httpUrlSource.url).getPath();
}
@Override
public void shutdown() {
super.shutdown();
Log.i(ProxyCacheUtils.LOG_TAG, "Shutdown proxy");
waitConnectionThread.interrupt();
try {
if (!serverSocket.isClosed()) {
serverSocket.close();
}
} catch (IOException e) {
onError(new ProxyCacheException("Error shutting down local server", e));
}
}
private void waitForRequest() {
try {
while (!Thread.currentThread().isInterrupted()) {
Socket socket = serverSocket.accept();
Log.d(ProxyCacheUtils.LOG_TAG, "Accept new socket " + socket);
processSocketInBackground(socket);
}
} catch (IOException e) {
onError(new ProxyCacheException("Error during waiting connection", e));
}
}
private void processSocketInBackground(final Socket socket) throws IOException {
executorService.submit(new Runnable() {
@Override
public void run() {
try {
processSocket(socket);
} catch (Throwable e) {
onError(e);
}
}
});
}
private void processSocket(Socket socket) {
try {
InputStream inputStream = socket.getInputStream();
String request = readRequest(inputStream);
Log.i(ProxyCacheUtils.LOG_TAG, "Request to cache proxy:\n" + request);
long rangeOffset = getRangeOffset(request);
writeResponse(socket, rangeOffset);
} catch (ProxyCacheException | IOException e) {
onError(new ProxyCacheException("Error processing request", e));
} finally {
releaseSocket(socket);
}
}
private void writeResponse(Socket socket, long rangeOffset) throws ProxyCacheException, IOException {
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;
long offset = Math.max(rangeOffset, 0);
boolean headersWrote = false;
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) {
writeResponseHeaders(out, rangeOffset);
String responseHeaders = newResponseHeaders(request);
out.write(responseHeaders.getBytes("UTF-8"));
headersWrote = true;
}
out.write(buffer, 0, readBytes);
if (isLogEnabled()) {
Log.d(ProxyCacheUtils.LOG_TAG, "Write data[" + readBytes + " bytes] to socket " + socket + " with offset " + offset + ": " + ProxyCacheUtils.preview(buffer, readBytes));
}
offset += readBytes;
if (cache.isCompleted()) {
onCacheAvailable(100);
}
}
out.flush();
}
private void writeResponseHeaders(OutputStream out, long rangeOffset) throws IOException, ProxyCacheException {
String responseHeaders = newResponseHeaders(rangeOffset);
out.write(responseHeaders.getBytes("UTF-8"));
Log.i(ProxyCacheUtils.LOG_TAG, "Response headers:\n" + responseHeaders);
}
private String newResponseHeaders(long offset) throws IOException, ProxyCacheException {
boolean partial = offset >= 0;
String mime = httpUrlSource.getMime();
private String newResponseHeaders(GetRequest request) throws IOException, ProxyCacheException {
String mime = source.getMime();
boolean mimeKnown = !TextUtils.isEmpty(mime);
int length = cache.isCompleted() ? cache.available() : httpUrlSource.available();
int length = cache.isCompleted() ? cache.available() : source.available();
boolean lengthKnown = length >= 0;
long contentLength = partial ? length - offset : length;
long contentLength = request.partial ? length - request.rangeOffset : length;
boolean addRange = lengthKnown && request.partial;
return new StringBuilder()
.append(partial ? "HTTP/1.1 206 PARTIAL CONTENT\n" : "HTTP/1.1 200 OK\n")
.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(lengthKnown && partial ? String.format("Content-Range: bytes %d-%d/%d\n", offset, length, length) : "")
.append(addRange ? String.format("Content-Range: bytes %d-%d/%d\n", request.rangeOffset, length, length) : "")
.append(mimeKnown ? String.format("Content-Type: %s\n", mime) : "")
.append("\n") // headers end
.toString();
}
private String readRequest(InputStream inputStream) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
StringBuilder str = new StringBuilder();
String line;
while (!TextUtils.isEmpty(line = reader.readLine())) { // until new line (headers ending)
str.append(line).append('\n');
}
return str.toString();
}
private long getRangeOffset(String request) {
Matcher matcher = RANGE_HEADER_PATTERN.matcher(request);
if (matcher.find()) {
String rangeValue = matcher.group(1);
return Long.parseLong(rangeValue);
}
return -1;
}
private void releaseSocket(Socket socket) {
try {
socket.shutdownInput();
} catch (IOException e) {
onError(new ProxyCacheException("Error closing socket input stream", e));
}
try {
socket.shutdownOutput();
} catch (IOException e) {
onError(new ProxyCacheException("Error closing socket output stream", e));
}
try {
socket.close();
} catch (IOException e) {
onError(new ProxyCacheException("Error closing socket", e));
}
}
private final class WaitRequestsRunnable implements Runnable {
private final CountDownLatch startSignal;
public WaitRequestsRunnable(CountDownLatch startSignal) {
this.startSignal = startSignal;
}
@Override
public void run() {
startSignal.countDown();
waitForRequest();
@Override
protected void onCacheAvailable(int percents) {
if (listener != null) {
listener.onCacheAvailable(cache.file, source.url, percents);
}
}
}

View File

@@ -0,0 +1,251 @@
package com.danikula.videocache;
import android.util.Log;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import static com.danikula.videocache.Preconditions.checkAllNotNull;
import static com.danikula.videocache.Preconditions.checkNotNull;
import static com.danikula.videocache.ProxyCacheUtils.LOG_TAG;
/**
* Simple lightweight proxy server with file caching support that handles HTTP requests.
* Typical usage:
* <pre><code>
* public onCreate(Bundle state) {
* super.onCreate(state);
* <p/>
* HttpProxyCacheServer proxy = getProxy();
* String proxyUrl = proxy.getProxyUrl(VIDEO_URL);
* videoView.setVideoPath(proxyUrl);
* }
* <p/>
* private HttpProxyCacheServer getProxy() {
* // should return single instance of HttpProxyCacheServer shared for whole app.
* }
* <code/></pre>
*
* @author Alexey Danilov (danikula@gmail.com).
*/
public class HttpProxyCacheServer {
private static final String PROXY_HOST = "127.0.0.1";
private final Object clientsLock = new Object();
private final ExecutorService socketProcessor = Executors.newFixedThreadPool(8);
private final Map<String, HttpProxyCacheServerClients> clientsMap = new ConcurrentHashMap<>();
private final ServerSocket serverSocket;
private final int port;
private final Thread waitConnectionThread;
private final FileNameGenerator fileNameGenerator;
public HttpProxyCacheServer(FileNameGenerator fileNameGenerator) {
this.fileNameGenerator = checkNotNull(fileNameGenerator);
try {
InetAddress inetAddress = InetAddress.getByName(PROXY_HOST);
this.serverSocket = new ServerSocket(0, 8, inetAddress);
this.port = serverSocket.getLocalPort();
CountDownLatch startSignal = new CountDownLatch(1);
this.waitConnectionThread = new Thread(new WaitRequestsRunnable(startSignal));
this.waitConnectionThread.start();
startSignal.await(); // freeze thread, wait for server starts
} catch (IOException | InterruptedException e) {
socketProcessor.shutdown();
throw new IllegalStateException("Error starting local proxy server", e);
}
}
public String getProxyUrl(String url) {
return String.format("http://%s:%d/%s", PROXY_HOST, port, ProxyCacheUtils.encode(url));
}
public void registerCacheListener(CacheListener cacheListener, String url) {
checkAllNotNull(cacheListener, url);
synchronized (clientsLock) {
try {
getClients(url).registerCacheListener(cacheListener);
} catch (ProxyCacheException e) {
Log.d(LOG_TAG, "Error registering cache listener", e);
}
}
}
public void unregisterCacheListener(CacheListener cacheListener, String url) {
checkAllNotNull(cacheListener, url);
synchronized (clientsLock) {
try {
getClients(url).unregisterCacheListener(cacheListener);
} catch (ProxyCacheException e) {
Log.d(LOG_TAG, "Error registering cache listener", e);
}
}
}
public void unregisterCacheListener(CacheListener cacheListener) {
checkNotNull(cacheListener);
synchronized (clientsLock) {
for (HttpProxyCacheServerClients clients : clientsMap.values()) {
clients.unregisterCacheListener(cacheListener);
}
}
}
public void shutdown() {
Log.i(LOG_TAG, "Shutdown proxy server");
shutdownClients();
waitConnectionThread.interrupt();
try {
if (!serverSocket.isClosed()) {
serverSocket.close();
}
} catch (IOException e) {
onError(new ProxyCacheException("Error shutting down proxy server", e));
}
}
private void shutdownClients() {
synchronized (clientsLock) {
for (HttpProxyCacheServerClients clients : clientsMap.values()) {
clients.shutdown();
}
clientsMap.clear();
}
}
private void waitForRequest() {
try {
while (!Thread.currentThread().isInterrupted()) {
Socket socket = serverSocket.accept();
Log.d(LOG_TAG, "Accept new socket " + socket);
socketProcessor.submit(new SocketProcessorRunnable(socket));
}
} catch (IOException e) {
onError(new ProxyCacheException("Error during waiting connection", e));
}
}
private void processSocket(Socket socket) {
try {
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);
} 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");
} catch (ProxyCacheException | IOException e) {
onError(new ProxyCacheException("Error processing request", e));
} finally {
releaseSocket(socket);
Log.d(LOG_TAG, "Opened connections: " + getClientsCount());
}
}
private HttpProxyCacheServerClients getClients(String url) throws ProxyCacheException {
synchronized (clientsLock) {
HttpProxyCacheServerClients clients = clientsMap.get(url);
if (clients == null) {
clients = new HttpProxyCacheServerClients(url, fileNameGenerator);
clientsMap.put(url, clients);
}
return clients;
}
}
private int getClientsCount() {
synchronized (clientsLock) {
int count = 0;
for (HttpProxyCacheServerClients clients : clientsMap.values()) {
count += clients.getClientsCount();
}
return count;
}
}
private void releaseSocket(Socket socket) {
closeSocketInput(socket);
closeSocketOutput(socket);
closeSocket(socket);
}
private void closeSocketInput(Socket socket) {
try {
if (!socket.isInputShutdown()) {
socket.shutdownInput();
}
} 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");
} catch (IOException e) {
onError(new ProxyCacheException("Error closing socket input stream", e));
}
}
private void closeSocketOutput(Socket socket) {
try {
if (socket.isOutputShutdown()) {
socket.shutdownOutput();
}
} catch (IOException e) {
onError(new ProxyCacheException("Error closing socket output stream", e));
}
}
private void closeSocket(Socket socket) {
try {
if (!socket.isClosed()) {
socket.close();
}
} catch (IOException e) {
onError(new ProxyCacheException("Error closing socket", e));
}
}
private void onError(Throwable e) {
Log.e(LOG_TAG, "HttpProxyCacheServer error", e);
}
private final class WaitRequestsRunnable implements Runnable {
private final CountDownLatch startSignal;
public WaitRequestsRunnable(CountDownLatch startSignal) {
this.startSignal = startSignal;
}
@Override
public void run() {
startSignal.countDown();
waitForRequest();
}
}
private final class SocketProcessorRunnable implements Runnable {
private final Socket socket;
public SocketProcessorRunnable(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
processSocket(socket);
}
}
}

View File

@@ -0,0 +1,113 @@
package com.danikula.videocache;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import java.io.File;
import java.io.IOException;
import java.net.Socket;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicInteger;
import static com.danikula.videocache.Preconditions.checkNotNull;
/**
* Client for {@link HttpProxyCacheServer}
*
* @author Alexey Danilov (danikula@gmail.com).
*/
final class HttpProxyCacheServerClients {
private final AtomicInteger clientsCount = new AtomicInteger(0);
private final String url;
private volatile HttpProxyCache proxyCache;
private final List<CacheListener> listeners = new CopyOnWriteArrayList<>();
private final FileNameGenerator fileNameGenerator;
private final CacheListener uiCacheListener;
public HttpProxyCacheServerClients(String url, FileNameGenerator fileNameGenerator) {
this.url = checkNotNull(url);
this.fileNameGenerator = checkNotNull(fileNameGenerator);
this.uiCacheListener = new UiListenerHandler(url, listeners);
}
public void processRequest(GetRequest request, Socket socket) throws ProxyCacheException, IOException {
startProcessRequest();
try {
clientsCount.incrementAndGet();
proxyCache.processRequest(request, socket);
} finally {
finishProcessRequest();
}
}
private synchronized void startProcessRequest() throws ProxyCacheException {
proxyCache = proxyCache == null ? newHttpProxyCache() : proxyCache;
}
private synchronized void finishProcessRequest() {
if (clientsCount.decrementAndGet() <= 0) {
proxyCache.shutdown();
proxyCache = null;
}
}
public void registerCacheListener(CacheListener cacheListener) {
listeners.add(cacheListener);
}
public void unregisterCacheListener(CacheListener cacheListener) {
listeners.remove(cacheListener);
}
public void shutdown() {
listeners.clear();
if (proxyCache != null) {
proxyCache.registerCacheListener(null);
proxyCache.shutdown();
proxyCache = null;
}
clientsCount.set(0);
}
public int getClientsCount() {
return clientsCount.get();
}
private HttpProxyCache newHttpProxyCache() throws ProxyCacheException {
HttpUrlSource source = new HttpUrlSource(url);
FileCache cache = new FileCache(fileNameGenerator.generate(url));
HttpProxyCache httpProxyCache = new HttpProxyCache(source, cache);
httpProxyCache.registerCacheListener(uiCacheListener);
return httpProxyCache;
}
private static final class UiListenerHandler extends Handler implements CacheListener {
private final String url;
private final List<CacheListener> listeners;
public UiListenerHandler(String url, List<CacheListener> listeners) {
super(Looper.getMainLooper());
this.url = url;
this.listeners = listeners;
}
@Override
public void onCacheAvailable(File file, String url, int percentsAvailable) {
Message message = obtainMessage();
message.arg1 = percentsAvailable;
message.obj = file;
sendMessage(message);
}
@Override
public void handleMessage(Message msg) {
for (CacheListener cacheListener : listeners) {
cacheListener.onCacheAvailable((File) msg.obj, url, msg.arg1);
}
}
}
}

View File

@@ -3,14 +3,20 @@ package com.danikula.videocache;
import android.text.TextUtils;
import android.util.Log;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
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}.
@@ -19,7 +25,8 @@ import static java.net.HttpURLConnection.HTTP_PARTIAL;
*/
public class HttpUrlSource implements Source {
final String url;
private static final int MAX_REDIRECTS = 5;
public final String url;
private HttpURLConnection connection;
private InputStream inputStream;
private volatile int available = Integer.MIN_VALUE;
@@ -35,7 +42,7 @@ public class HttpUrlSource implements Source {
}
@Override
public int available() throws ProxyCacheException {
public synchronized int available() throws ProxyCacheException {
if (available == Integer.MIN_VALUE) {
fetchContentInfo();
}
@@ -45,25 +52,19 @@ public class HttpUrlSource implements Source {
@Override
public void open(int offset) throws ProxyCacheException {
try {
Log.d(ProxyCacheUtils.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 + "-");
}
connection = openConnection(offset, "GET", -1);
mime = connection.getContentType();
inputStream = connection.getInputStream();
readSourceAvailableBytes(connection, offset);
inputStream = new BufferedInputStream(connection.getInputStream(), DEFAULT_BUFFER_SIZE);
available = readSourceAvailableBytes(connection, offset, connection.getResponseCode());
} catch (IOException e) {
throw new ProxyCacheException("Error opening connection for " + url + " with offset " + offset, e);
}
}
private void 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();
available = responseCode == HTTP_OK ? contentLength :
responseCode == HTTP_PARTIAL ? contentLength + offset :
available;
return responseCode == HTTP_OK ? contentLength
: responseCode == HTTP_PARTIAL ? contentLength + offset : available;
}
@Override
@@ -80,33 +81,76 @@ public class HttpUrlSource implements Source {
}
try {
return inputStream.read(buffer, 0, buffer.length);
} catch (InterruptedIOException e) {
throw new InterruptedProxyCacheException("Reading source " + url + " is interrupted", e);
} catch (IOException e) {
throw new ProxyCacheException("Error reading data from " + url, e);
}
}
private void fetchContentInfo() throws ProxyCacheException {
Log.d(ProxyCacheUtils.LOG_TAG, "Read content info from " + url);
Log.d(LOG_TAG, "Read content info from " + url);
HttpURLConnection urlConnection = null;
InputStream inputStream = null;
try {
urlConnection = (HttpURLConnection) new URL(url).openConnection();
urlConnection.setRequestMethod("HEAD");
urlConnection = openConnection(0, "HEAD", 10000);
available = urlConnection.getContentLength();
mime = urlConnection.getContentType();
Log.d(ProxyCacheUtils.LOG_TAG, "Content-Length of " + url + " is " + available + " bytes, mime is " + mime);
inputStream = urlConnection.getInputStream();
Log.i(LOG_TAG, "Content info for `" + url + "`: mime: " + mime + ", content-length: " + available);
} catch (IOException e) {
throw new ProxyCacheException("Error fetching Content-Length from " + url);
} finally {
ProxyCacheUtils.close(inputStream);
if (urlConnection != null) {
urlConnection.disconnect();
}
}
}
public String getMime() throws ProxyCacheException {
private HttpURLConnection openConnection(int offset, String method, int timeout) throws IOException, ProxyCacheException {
HttpURLConnection connection;
boolean redirected;
int redirectCount = 0;
String url = this.url;
do {
Log.d(LOG_TAG, "Open connection " + (offset > 0 ? " with offset " + offset : "") + " to " + url);
connection = (HttpURLConnection) new URL(url).openConnection();
connection.setRequestMethod(method);
if (offset > 0) {
connection.setRequestProperty("Range", "bytes=" + offset + "-");
}
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)) {
fetchContentInfo();
}
return mime;
}
public String getUrl() {
return url;
}
@Override
public String toString() {
return "HttpUrlSource{url='" + url + "}";
}
}

View File

@@ -0,0 +1,21 @@
package com.danikula.videocache;
/**
* Indicates interruption error in work of {@link ProxyCache} fired by user.
*
* @author Alexey Danilov
*/
public class InterruptedProxyCacheException extends ProxyCacheException {
public InterruptedProxyCacheException(String message) {
super(message);
}
public InterruptedProxyCacheException(String message, Throwable cause) {
super(message, cause);
}
public InterruptedProxyCacheException(Throwable cause) {
super(cause);
}
}

View File

@@ -0,0 +1,38 @@
package com.danikula.videocache;
import android.text.TextUtils;
import java.io.File;
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 static final int MAX_EXTENSION_LENGTH = 4;
private final File cacheDirectory;
public Md5FileNameGenerator(File cacheDirectory) {
this.cacheDirectory = checkNotNull(cacheDirectory);
}
@Override
public File generate(String url) {
checkNotNull(url);
String extension = getExtension(url);
String name = ProxyCacheUtils.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('/');
return dotIndex != -1 && dotIndex > slashIndex && dotIndex + 2 + MAX_EXTENSION_LENGTH > url.length() ?
url.substring(dotIndex + 1, url.length()) : "";
}
}

View File

@@ -9,6 +9,14 @@ final class Preconditions {
return reference;
}
static void checkAllNotNull(Object... references) {
for (Object reference : references) {
if (reference == null) {
throw new NullPointerException();
}
}
}
static <T> T checkNotNull(T reference, String errorMessage) {
if (reference == null) {
throw new NullPointerException(errorMessage);

View File

@@ -1,7 +1,5 @@
package com.danikula.videocache;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import java.util.concurrent.atomic.AtomicInteger;
@@ -18,35 +16,22 @@ import static com.danikula.videocache.ProxyCacheUtils.LOG_TAG;
*
* @author Alexey Danilov (danikula@gmail.com).
*/
public class ProxyCache {
class ProxyCache {
private static final int MAX_READ_SOURCE_ATTEMPTS = 1;
private final Source source;
private final Cache cache;
private final Object wc;
private final Handler handler;
private final Object wc = new Object();
private final Object stopLock = new Object();
private volatile Thread sourceReaderThread;
private volatile boolean stopped;
private final AtomicInteger readSourceErrorsCount;
private CacheListener cacheListener;
private final boolean logEnabled;
public ProxyCache(Source source, Cache cache, boolean logEnabled) {
this.source = checkNotNull(source);
this.cache = checkNotNull(cache);
this.logEnabled = logEnabled;
this.wc = new Object();
this.handler = new Handler(Looper.getMainLooper());
this.readSourceErrorsCount = new AtomicInteger();
}
public ProxyCache(Source source, Cache cache) {
this(source, cache, false);
}
public void setCacheListener(CacheListener cacheListener) {
this.cacheListener = cacheListener;
this.source = checkNotNull(source);
this.cache = checkNotNull(cache);
this.readSourceErrorsCount = new AtomicInteger();
}
public int read(byte[] buffer, long offset, int length) throws ProxyCacheException {
@@ -55,21 +40,9 @@ public class ProxyCache {
while (!cache.isCompleted() && cache.available() < (offset + length) && !stopped) {
readSourceAsync();
waitForSourceData();
checkIsCacheValid();
checkReadSourceErrorsCount();
}
int read = cache.read(buffer, offset, length);
if (isLogEnabled()) {
Log.d(LOG_TAG, "Read data[" + read + " bytes] from cache with offset " + offset + ": " + ProxyCacheUtils.preview(buffer, read));
}
return read;
}
private void checkIsCacheValid() throws ProxyCacheException {
int sourceAvailable = source.available();
if (sourceAvailable > 0 && cache.available() > sourceAvailable) {
throw new ProxyCacheException("Unexpected cache: cache [" + cache.available() + " bytes] > source[" + sourceAvailable + " bytes]");
}
return cache.read(buffer, offset, length);
}
private void checkReadSourceErrorsCount() throws ProxyCacheException {
@@ -81,22 +54,24 @@ public class ProxyCache {
}
public void shutdown() {
try {
stopped = true;
if (sourceReaderThread != null) {
sourceReaderThread.interrupt();
synchronized (stopLock) {
Log.d(LOG_TAG, "Shutdown proxy for " + source);
try {
stopped = true;
if (sourceReaderThread != null) {
sourceReaderThread.interrupt();
}
cache.close();
} catch (ProxyCacheException e) {
onError(e);
}
cache.close();
} catch (ProxyCacheException e) {
onError(e);
}
}
private void readSourceAsync() throws ProxyCacheException {
private synchronized void readSourceAsync() throws ProxyCacheException {
boolean readingInProgress = sourceReaderThread != null && sourceReaderThread.getState() != Thread.State.TERMINATED;
if (!stopped && !cache.isCompleted() && !readingInProgress) {
sourceReaderThread = new Thread(new SourceReaderRunnable(), "Source reader for ProxyCache");
sourceReaderThread = new Thread(new SourceReaderRunnable(), "Source reader for " + source);
sourceReaderThread.start();
}
}
@@ -111,21 +86,17 @@ public class ProxyCache {
}
}
private void notifyNewCacheDataAvailable(final int cachePercentage) {
handler.post(new Runnable() {
@Override
public void run() {
if (cacheListener != null) {
cacheListener.onCacheDataAvailable(cachePercentage);
}
}
});
private void notifyNewCacheDataAvailable(int cachePercentage) {
onCacheAvailable(cachePercentage);
synchronized (wc) {
wc.notifyAll();
}
}
protected void onCacheAvailable(int percents) {
}
private void readSource() {
int cachePercentage = 0;
try {
@@ -133,19 +104,19 @@ public class ProxyCache {
source.open(offset);
byte[] buffer = new byte[ProxyCacheUtils.DEFAULT_BUFFER_SIZE];
int readBytes;
while ((readBytes = source.read(buffer)) != -1 && !Thread.currentThread().isInterrupted() && !stopped) {
if (isLogEnabled()) {
Log.d(LOG_TAG, "Write data[" + readBytes + " bytes] to cache from source with offset " + offset + ": " + ProxyCacheUtils.preview(buffer, readBytes));
while ((readBytes = source.read(buffer)) != -1) {
synchronized (stopLock) {
if (isStopped()) {
return;
}
cache.append(buffer, readBytes);
}
cache.append(buffer, readBytes);
offset += readBytes;
cachePercentage = offset * 100 / source.available();
notifyNewCacheDataAvailable(cachePercentage);
}
if (cache.available() == source.available()) {
cache.complete();
}
tryComplete();
} catch (Throwable e) {
readSourceErrorsCount.incrementAndGet();
onError(e);
@@ -155,6 +126,18 @@ public class ProxyCache {
}
}
private void tryComplete() throws ProxyCacheException {
synchronized (stopLock) {
if (!isStopped() && cache.available() == source.available()) {
cache.complete();
}
}
}
private boolean isStopped() {
return Thread.currentThread().isInterrupted() || stopped;
}
private void closeSource() {
try {
source.close();
@@ -164,12 +147,12 @@ public class ProxyCache {
}
protected final void onError(final Throwable e) {
Log.e(LOG_TAG, "ProxyCache error", e);
handler.post(new ErrorDeliverer(e));
}
protected boolean isLogEnabled() {
return logEnabled;
boolean interruption = e instanceof InterruptedProxyCacheException;
if (interruption) {
Log.d(LOG_TAG, "ProxyCache is interrupted");
} else {
Log.e(LOG_TAG, "ProxyCache error", e);
}
}
private class SourceReaderRunnable implements Runnable {
@@ -179,24 +162,4 @@ public class ProxyCache {
readSource();
}
}
private class ErrorDeliverer implements Runnable {
private final Throwable error;
public ErrorDeliverer(Throwable error) {
this.error = error;
}
@Override
public void run() {
if (error instanceof ProxyCacheException) {
if (cacheListener != null) {
cacheListener.onError((ProxyCacheException) error);
}
} else {
throw new RuntimeException("Unexpected error!", error);
}
}
}
}

View File

@@ -1,10 +1,17 @@
package com.danikula.videocache;
import android.text.TextUtils;
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;
@@ -56,5 +63,48 @@ class ProxyCacheUtils {
}
}
static String encode(String url) {
try {
return URLEncoder.encode(url, "utf-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("Error encoding url", e);
}
}
static String decode(String url) {
try {
return URLDecoder.decode(url, "utf-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("Error decoding url", e);
}
}
static void close(Closeable closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (IOException e) {
Log.e(LOG_TAG, "Error closing resource", e);
}
}
}
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);
}
}
static String bytesToHexString(byte[] bytes) {
StringBuffer sb = new StringBuffer();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
}

View File

@@ -1,22 +1,45 @@
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.4'
}
}
repositories {
maven { url 'https://github.com/danikula/AndroidVideoCache/raw/mvn-repo' }
maven { url 'https://dl.bintray.com/alexeydanilov/maven' }
maven { url 'https://github.com/dahlgren/vpi-aar/raw/master' }
}
apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt'
android {
compileSdkVersion 22
buildToolsVersion '22.0.1'
compileSdkVersion 23
buildToolsVersion '23.0.1'
defaultConfig {
applicationId "com.danikula.videocache.sample"
minSdkVersion 15
targetSdkVersion 22
targetSdkVersion 23
versionCode 1
versionName '1.0'
}
}
dependencies {
compile('com.danikula:videocache:1.0')
apt {
arguments {
androidManifestFile variant.outputs[0].processResources.manifestFile
resourcePackageName android.defaultConfig.applicationId
}
}
dependencies {
// compile project(':library')
compile 'com.android.support:support-v4:23.0.1'
compile 'org.androidannotations:androidannotations-api:3.3.2'
compile 'com.danikula:videocache:2.1.1'
compile 'com.viewpagerindicator:library:2.4.2-SNAPSHOT@aar'
apt 'org.androidannotations:androidannotations:3.3.2'
}

View File

@@ -1,24 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.danikula.videocache.sample">
package="com.danikula.videocache.sample"
xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:name=".App"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@android:style/Theme.Holo.Light.NoActionBar">
<activity
android:name=".VideoActivity"
android:name=".MenuActivity_"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".SingleVideoActivity_" />
<activity android:name=".MultipleVideosActivity_" />
<activity android:name=".VideoGalleryActivity_" />
<activity android:name=".SharedCacheActivity_" />
</application>
</manifest>

View File

@@ -0,0 +1,26 @@
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).
*/
public class App extends Application {
private HttpProxyCacheServer proxy;
public static HttpProxyCacheServer getProxy(Context context) {
App app = (App) context.getApplicationContext();
return app.proxy == null ? (app.proxy = app.newProxy()) : app.proxy;
}
private HttpProxyCacheServer newProxy() {
FileNameGenerator nameGenerator = new Md5FileNameGenerator(getExternalCacheDir());
return new HttpProxyCacheServer(nameGenerator);
}
}

View File

@@ -0,0 +1,139 @@
package com.danikula.videocache.sample;
import android.content.Context;
import android.os.Handler;
import android.os.Message;
import android.support.v4.app.Fragment;
import android.widget.ProgressBar;
import android.widget.VideoView;
import com.danikula.videocache.CacheListener;
import com.danikula.videocache.HttpProxyCacheServer;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.FragmentArg;
import org.androidannotations.annotations.InstanceState;
import org.androidannotations.annotations.SeekBarTouchStop;
import org.androidannotations.annotations.ViewById;
import java.io.File;
@EFragment(R.layout.fragment_video)
public class GalleryVideoFragment extends Fragment implements CacheListener {
private static final String LOG_TAG = "VideoFragment";
@FragmentArg String url;
@FragmentArg String cachePath;
@InstanceState int position;
@InstanceState boolean playerStarted;
@ViewById VideoView videoView;
@ViewById ProgressBar progressBar;
private boolean visibleForUser;
private final VideoProgressUpdater updater = new VideoProgressUpdater();
public static Fragment build(Context context, Video video) {
return build(video.url, video.getCacheFile(context).getAbsolutePath());
}
public static Fragment build(String url, String cachePath) {
return GalleryVideoFragment_.builder()
.url(url)
.cachePath(cachePath)
.build();
}
@AfterViews
void afterViewInjected() {
startProxy();
if (visibleForUser) {
startPlayer();
}
}
private void startPlayer() {
videoView.seekTo(position);
videoView.start();
playerStarted = true;
}
private void startProxy() {
HttpProxyCacheServer proxy = App.getProxy(getActivity());
proxy.registerCacheListener(this, url);
videoView.setVideoPath(proxy.getProxyUrl(url));
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
visibleForUser = isVisibleToUser;
if (videoView != null) {
if (visibleForUser) {
startPlayer();
} else if (playerStarted) {
position = videoView.getCurrentPosition();
videoView.pause();
}
}
}
@Override
public void onResume() {
super.onResume();
updater.start();
}
@Override
public void onPause() {
super.onPause();
updater.stop();
}
@Override
public void onDestroy() {
super.onDestroy();
videoView.stopPlayback();
App.getProxy(getActivity()).unregisterCacheListener(this);
}
@Override
public void onCacheAvailable(File file, String url, int percentsAvailable) {
progressBar.setSecondaryProgress(percentsAvailable);
}
private void updateVideoProgress() {
int videoProgress = videoView.getCurrentPosition() * 100 / videoView.getDuration();
progressBar.setProgress(videoProgress);
}
@SeekBarTouchStop(R.id.progressBar)
void seekVideo() {
int videoPosition = videoView.getDuration() * progressBar.getProgress() / 100;
videoView.seekTo(videoPosition);
}
private final class VideoProgressUpdater extends Handler {
public void start() {
sendEmptyMessage(0);
}
public void stop() {
removeMessages(0);
}
@Override
public void handleMessage(Message msg) {
updateVideoProgress();
sendEmptyMessageDelayed(0, 500);
}
}
}

View File

@@ -0,0 +1,73 @@
package com.danikula.videocache.sample;
import android.content.Intent;
import android.support.annotation.NonNull;
import android.support.v4.app.FragmentActivity;
import android.widget.ArrayAdapter;
import android.widget.ListAdapter;
import android.widget.ListView;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.ItemClick;
import org.androidannotations.annotations.ViewById;
import java.io.File;
import java.util.Arrays;
import java.util.List;
@EActivity(R.layout.activity_menu)
public class MenuActivity extends FragmentActivity {
@ViewById ListView listView;
@AfterViews
void onViewInjected() {
ListAdapter adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, android.R.id.text1, buildListData());
listView.setAdapter(adapter);
}
@NonNull
private List<ListEntry> buildListData() {
return Arrays.asList(
new ListEntry("Single Video", SingleVideoActivity_.class),
new ListEntry("Multiple Videos", MultipleVideosActivity_.class),
new ListEntry("Video Gallery with pre-caching", VideoGalleryActivity_.class),
new ListEntry("Shared Cache", SharedCacheActivity_.class)
);
}
@ItemClick(R.id.listView)
void onListItemClicked(int position) {
ListEntry item = (ListEntry) listView.getAdapter().getItem(position);
startActivity(new Intent(this, item.activityClass));
}
@Click(R.id.cleanCacheButton)
void onClearCacheButtonClick() {
File externalCacheDir = getExternalCacheDir();
if (externalCacheDir != null) {
for (File cacheEntry : externalCacheDir.listFiles()) {
cacheEntry.delete();
}
}
}
private static final class ListEntry {
private final String title;
private final Class activityClass;
public ListEntry(String title, Class activityClass) {
this.title = title;
this.activityClass = activityClass;
}
@Override
public String toString() {
return title;
}
}
}

View File

@@ -0,0 +1,29 @@
package com.danikula.videocache.sample;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import org.androidannotations.annotations.EActivity;
@EActivity(R.layout.activity_multiple_videos)
public class MultipleVideosActivity extends FragmentActivity {
@Override
protected void onCreate(Bundle state) {
super.onCreate(state);
if (state == null) {
addVideoFragment(Video.ORANGE_1, R.id.videoContainer0);
addVideoFragment(Video.ORANGE_2, R.id.videoContainer1);
addVideoFragment(Video.ORANGE_3, R.id.videoContainer2);
addVideoFragment(Video.ORANGE_4, R.id.videoContainer3);
}
}
private void addVideoFragment(Video video, int containerViewId) {
getSupportFragmentManager()
.beginTransaction()
.add(containerViewId, VideoFragment.build(this, video))
.commit();
}
}

View File

@@ -0,0 +1,29 @@
package com.danikula.videocache.sample;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import org.androidannotations.annotations.EActivity;
@EActivity(R.layout.activity_multiple_videos)
public class SharedCacheActivity extends FragmentActivity {
@Override
protected void onCreate(Bundle state) {
super.onCreate(state);
if (state == null) {
addVideoFragment(Video.ORANGE_1, R.id.videoContainer0);
addVideoFragment(Video.ORANGE_1, R.id.videoContainer1);
addVideoFragment(Video.ORANGE_1, R.id.videoContainer2);
addVideoFragment(Video.ORANGE_1, R.id.videoContainer3);
}
}
private void addVideoFragment(Video video, int containerViewId) {
getSupportFragmentManager()
.beginTransaction()
.add(containerViewId, VideoFragment.build(this, video))
.commit();
}
}

View File

@@ -0,0 +1,22 @@
package com.danikula.videocache.sample;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import org.androidannotations.annotations.EActivity;
@EActivity(R.layout.activity_single_video)
public class SingleVideoActivity extends FragmentActivity {
@Override
protected void onCreate(Bundle state) {
super.onCreate(state);
if (state == null) {
getSupportFragmentManager()
.beginTransaction()
.add(R.id.containerView, VideoFragment.build(this, Video.ORANGE_1))
.commit();
}
}
}

View File

@@ -0,0 +1,24 @@
package com.danikula.videocache.sample;
import android.content.Context;
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");
public final String url;
Video(String url) {
this.url = url;
}
public File getCacheFile(Context context) {
return new File(context.getExternalCacheDir(), name());
}
}

View File

@@ -1,75 +0,0 @@
package com.danikula.videocache.sample;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.widget.ProgressBar;
import android.widget.VideoView;
import com.danikula.videocache.Cache;
import com.danikula.videocache.CacheListener;
import com.danikula.videocache.FileCache;
import com.danikula.videocache.HttpProxyCache;
import com.danikula.videocache.HttpUrlSource;
import com.danikula.videocache.ProxyCacheException;
import java.io.File;
public class VideoActivity extends Activity implements CacheListener {
private static final String LOG_TAG = "VideoActivity";
private static final String VIDEO_CACHE_NAME = "devbytes.mp4";
private static final String VIDEO_URL = "https://dl.dropboxusercontent.com/u/15506779/persistent/proxycache/devbytes.mp4";
private VideoView videoView;
private ProgressBar progressBar;
private HttpProxyCache proxyCache;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setUpUi();
playWithCache();
}
private void setUpUi() {
setContentView(R.layout.activity_video);
videoView = (VideoView) findViewById(R.id.videoView);
progressBar = (ProgressBar) findViewById(R.id.progressBar);
progressBar.setMax(100);
}
private void playWithCache() {
try {
Cache cache = new FileCache(new File(getExternalCacheDir(), VIDEO_CACHE_NAME));
HttpUrlSource source = new HttpUrlSource(VIDEO_URL);
proxyCache = new HttpProxyCache(source, cache);
proxyCache.setCacheListener(this);
videoView.setVideoPath(proxyCache.getUrl());
videoView.start();
} catch (ProxyCacheException e) {
// do nothing. onError() handles all errors
}
}
@Override
public void onDestroy() {
super.onDestroy();
if (proxyCache != null) {
proxyCache.shutdown();
}
}
@Override
public void onError(ProxyCacheException e) {
Log.e(LOG_TAG, "Error playing video", e);
}
@Override
public void onCacheDataAvailable(int cachePercentage) {
progressBar.setProgress(cachePercentage);
}
}

View File

@@ -0,0 +1,109 @@
package com.danikula.videocache.sample;
import android.content.Context;
import android.os.Handler;
import android.os.Message;
import android.support.v4.app.Fragment;
import android.widget.ProgressBar;
import android.widget.VideoView;
import com.danikula.videocache.CacheListener;
import com.danikula.videocache.HttpProxyCacheServer;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.FragmentArg;
import org.androidannotations.annotations.SeekBarTouchStop;
import org.androidannotations.annotations.ViewById;
import java.io.File;
@EFragment(R.layout.fragment_video)
public class VideoFragment extends Fragment implements CacheListener {
private static final String LOG_TAG = "VideoFragment";
@FragmentArg String url;
@FragmentArg String cachePath;
@ViewById VideoView videoView;
@ViewById ProgressBar progressBar;
private final VideoProgressUpdater updater = new VideoProgressUpdater();
public static Fragment build(Context context, Video video) {
return build(video.url, video.getCacheFile(context).getAbsolutePath());
}
public static Fragment build(String url, String cachePath) {
return VideoFragment_.builder()
.url(url)
.cachePath(cachePath)
.build();
}
@AfterViews
void afterViewInjected() {
startVideo();
}
private void startVideo() {
HttpProxyCacheServer proxy = App.getProxy(getActivity());
proxy.registerCacheListener(this, url);
videoView.setVideoPath(proxy.getProxyUrl(url));
videoView.start();
}
@Override
public void onResume() {
super.onResume();
updater.start();
}
@Override
public void onPause() {
super.onPause();
updater.stop();
}
@Override
public void onDestroy() {
super.onDestroy();
videoView.stopPlayback();
App.getProxy(getActivity()).unregisterCacheListener(this);
}
@Override
public void onCacheAvailable(File file, String url, int percentsAvailable) {
progressBar.setSecondaryProgress(percentsAvailable);
}
private void updateVideoProgress() {
int videoProgress = videoView.getCurrentPosition() * 100 / videoView.getDuration();
progressBar.setProgress(videoProgress);
}
@SeekBarTouchStop(R.id.progressBar)
void seekVideo() {
int videoPosition = videoView.getDuration() * progressBar.getProgress() / 100;
videoView.seekTo(videoPosition);
}
private final class VideoProgressUpdater extends Handler {
public void start() {
sendEmptyMessage(0);
}
public void stop() {
removeMessages(0);
}
@Override
public void handleMessage(Message msg) {
updateVideoProgress();
sendEmptyMessageDelayed(0, 500);
}
}
}

View File

@@ -0,0 +1,53 @@
package com.danikula.videocache.sample;
import android.content.Context;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentStatePagerAdapter;
import android.support.v4.view.ViewPager;
import com.viewpagerindicator.CirclePageIndicator;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.ViewById;
@EActivity(R.layout.activity_video_gallery)
public class VideoGalleryActivity extends FragmentActivity {
@ViewById ViewPager viewPager;
@ViewById CirclePageIndicator viewPagerIndicator;
@AfterViews
void afterViewInjected() {
ViewsPagerAdapter viewsPagerAdapter = new ViewsPagerAdapter(this);
viewPager.setAdapter(viewsPagerAdapter);
viewPagerIndicator.setViewPager(viewPager);
}
private static final class ViewsPagerAdapter extends FragmentStatePagerAdapter {
private final Context context;
public ViewsPagerAdapter(FragmentActivity activity) {
super(activity.getSupportFragmentManager());
this.context = activity;
}
@Override
public Fragment getItem(int position) {
Video video = Video.values()[position];
return GalleryVideoFragment.build(context, video);
}
@Override
public int getCount() {
return Video.values().length;
}
@Override
public CharSequence getPageTitle(int position) {
return Video.values()[position].name();
}
}
}

View File

@@ -0,0 +1,23 @@
<LinearLayout
android:id="@+id/containerView"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:divider="@android:drawable/divider_horizontal_dark"
android:orientation="vertical"
android:showDividers="middle"
tools:context=".MenuActivity">
<Button
android:id="@+id/cleanCacheButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Clean cache" />
<ListView
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

View File

@@ -0,0 +1,58 @@
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#CCC"
android:orientation="vertical"
android:padding="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="horizontal">
<FrameLayout
android:id="@+id/videoContainer0"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_margin="8dp"
android:layout_weight="1"
android:background="#FFF" />
<FrameLayout
android:id="@+id/videoContainer1"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_margin="8dp"
android:layout_weight="1"
android:background="#FFF" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="horizontal">
<FrameLayout
android:id="@+id/videoContainer2"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_margin="8dp"
android:layout_weight="1"
android:background="#FFF" />
<FrameLayout
android:id="@+id/videoContainer3"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_margin="8dp"
android:layout_weight="1"
android:background="#FFF" />
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,7 @@
<FrameLayout
android:id="@+id/containerView"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".VideoActivity" />

View File

@@ -0,0 +1,23 @@
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.v4.view.ViewPager
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<com.viewpagerindicator.CirclePageIndicator
android:id="@+id/viewPagerIndicator"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
app:fillColor="#800F"
app:radius="10dp"
app:strokeColor="#800F" />
</LinearLayout>

View File

@@ -10,13 +10,14 @@
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ProgressBar
<SeekBar
android:id="@+id/progressBar"
style="@android:style/Widget.Holo.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@android:style/Widget.Holo.ProgressBar.Horizontal"
android:layout_margin="16dp"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true" />
android:layout_centerHorizontal="true"
android:layout_margin="16dp"
android:max="100" />
</RelativeLayout>

View File

@@ -39,7 +39,8 @@ dependencies {
}
testCompile 'com.squareup:fest-android:1.0.0'
testCompile 'com.google.guava:guava-jdk5:17.0'
testCompile('com.danikula:android-garden:2.0.13') {
testCompile('com.danikula:android-garden:2.1.4') {
exclude group: 'com.google.android'
}
testCompile 'org.mockito:mockito-all:1.9.5'
}

View File

@@ -0,0 +1,82 @@
package com.danikula.videocache;
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, emulateSdk = BuildConfig.MIN_SDK_VERSION)
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 testAssertNullRoot() throws Exception {
new Md5FileNameGenerator(null);
fail("Root folder should be not null");
}
@Test(expected = NullPointerException.class)
public void testAssertNullUrl() throws Exception {
FileNameGenerator nameGenerator = new Md5FileNameGenerator(new File("/"));
nameGenerator.generate(null);
fail("Url should be not null");
}
private String generateMd5Name(String rootFolder, String url) {
FileNameGenerator nameGenerator = new Md5FileNameGenerator(new File(rootFolder));
return nameGenerator.generate(url).getAbsolutePath();
}
}

View File

@@ -0,0 +1,76 @@
package com.danikula.videocache;
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.ByteArrayInputStream;
import java.io.InputStream;
import static org.fest.assertions.api.Assertions.assertThat;
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)
public class GetRequestTest {
@Test
public void testPartialHttpGet() throws Exception {
GetRequest getRequest = new GetRequest("" +
"GET /uri HTTP/1.1\n" +
"Host: 127.0.0.1:44684\n" +
"Range: bytes=9860723-" +
"Accept-Encoding: gzip");
assertThat(getRequest.rangeOffset).isEqualTo(9860723);
assertThat(getRequest.uri).isEqualTo("uri");
assertThat(getRequest.partial).isTrue();
}
@Test
public void testNotPartialHttpGet() throws Exception {
GetRequest getRequest = new GetRequest("" +
"GET /uri HTTP/1.1\n" +
"Host: 127.0.0.1:44684\n" +
"Accept-Encoding: gzip");
assertThat(getRequest.rangeOffset).isEqualTo(0);
assertThat(getRequest.uri).isEqualTo("uri");
assertThat(getRequest.partial).isFalse();
}
@Test
public void testReadStream() throws Exception {
String requestString = "GET /uri HTTP/1.1\nRange: bytes=9860723-\n";
InputStream stream = new ByteArrayInputStream(requestString.getBytes());
GetRequest getRequest = GetRequest.read(stream);
assertThat(getRequest.rangeOffset).isEqualTo(9860723);
assertThat(getRequest.uri).isEqualTo("uri");
assertThat(getRequest.partial).isTrue();
}
@Test
public void testMinimal() throws Exception {
GetRequest getRequest = new GetRequest("GET /uri HTTP/1.1");
assertThat(getRequest.rangeOffset).isEqualTo(0);
assertThat(getRequest.uri).isEqualTo("uri");
assertThat(getRequest.partial).isFalse();
}
@Test(expected = IllegalArgumentException.class)
public void testEmpty() throws Exception {
GetRequest getRequest = new GetRequest("");
fail("Empty request");
}
@Test(expected = IllegalArgumentException.class)
public void testInvalid() throws Exception {
GetRequest getRequest = new GetRequest("/uri HTTP/1.1\n");
fail("Invalid request");
}
}

View File

@@ -0,0 +1,150 @@
package com.danikula.videocache;
import android.util.Pair;
import com.danikula.android.garden.io.IoUtils;
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.robolectric.RobolectricGradleTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
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_URL;
import static com.danikula.videocache.support.ProxyCacheTestUtils.getFileContent;
import static com.danikula.videocache.support.ProxyCacheTestUtils.loadAssetFile;
import static com.danikula.videocache.support.ProxyCacheTestUtils.readProxyResponse;
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)
public class HttpProxyCacheServerTest {
@Test
public void testHttpProxyCache() throws Exception {
Pair<File, Response> response = readProxyData(HTTP_DATA_URL);
assertThat(response.second.code).isEqualTo(200);
assertThat(response.second.data).isEqualTo(getFileContent(response.first));
assertThat(response.second.data).isEqualTo(loadAssetFile(ASSETS_DATA_NAME));
}
@Test
public void testProxyContentWithPartialCache() throws Exception {
FileNameGenerator fileNameGenerator = new Md5FileNameGenerator(RuntimeEnvironment.application.getExternalCacheDir());
File file = fileNameGenerator.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);
Response response = readProxyResponse(proxy, HTTP_DATA_URL);
proxy.shutdown();
byte[] expected = loadAssetFile(ASSETS_DATA_NAME);
System.arraycopy(partialData, 0, expected, 0, partialCacheSize);
assertThat(response.data).isEqualTo(expected);
}
@Test
public void testMimeFromResponse() throws Exception {
Pair<File, Response> response = readProxyData("https://dl.dropboxusercontent.com/u/15506779/persistent/proxycache/android");
assertThat(response.second.contentType).isEqualTo("application/octet-stream");
}
@Test
public void testProxyFullResponse() throws Exception {
Pair<File, Response> response = readProxyData(HTTP_DATA_BIG_URL);
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 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;
Pair<File, Response> response = readProxyData(HTTP_DATA_BIG_URL, 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, 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 = 42000;
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, 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));
}
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);
Response response = readProxyResponse(proxy, url, offset);
proxy.shutdown();
return new Pair<>(file, response);
}
private Pair<File, Response> readProxyData(String url) throws IOException {
return readProxyData(url, -1);
}
}

View File

@@ -1,142 +0,0 @@
package com.danikula.videocache;
import com.danikula.android.garden.io.IoUtils;
import com.danikula.videocache.support.AngryHttpUrlSource;
import com.danikula.videocache.support.Response;
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 java.util.Arrays;
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_URL;
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.readProxyResponse;
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)
public class HttpProxyCacheTest {
@Test
public void testHttpProxyCache() throws Exception {
HttpUrlSource source = new HttpUrlSource(HTTP_DATA_URL);
File file = newCacheFile();
HttpProxyCache proxy = new HttpProxyCache(source, new FileCache(file));
Response response = readProxyResponse(proxy);
assertThat(response.code).isEqualTo(200);
assertThat(response.data).isEqualTo(getFileContent(file));
assertThat(response.data).isEqualTo(loadAssetFile(ASSETS_DATA_NAME));
proxy.shutdown();
}
@Test
public void testProxyContentWithPartialCache() throws Exception {
HttpUrlSource source = new HttpUrlSource(HTTP_DATA_URL);
int cacheSize = 1000;
HttpProxyCache proxy = new HttpProxyCache(source, new ByteArrayCache(new byte[cacheSize]));
Response proxyResponse = readProxyResponse(proxy);
byte[] expected = loadAssetFile(ASSETS_DATA_NAME);
Arrays.fill(expected, 0, cacheSize, (byte) 0);
assertThat(proxyResponse.data).isEqualTo(expected);
proxy.shutdown();
}
@Test
public void testMimeFromResponse() throws Exception {
HttpUrlSource source = new HttpUrlSource("https://dl.dropboxusercontent.com/u/15506779/persistent/proxycache/android");
HttpProxyCache proxy = new HttpProxyCache(source, new ByteArrayCache(new byte[0]));
proxy.read(new byte[1], 0, 1);
assertThat(source.getMime()).isEqualTo("application/octet-stream");
proxy.shutdown();
}
@Test
public void testProxyFullResponse() throws Exception {
File file = newCacheFile();
HttpProxyCache proxy = new HttpProxyCache(new HttpUrlSource(HTTP_DATA_BIG_URL), new FileCache(file));
Response response = readProxyResponse(proxy);
assertThat(response.code).isEqualTo(200);
assertThat(response.contentLength).isEqualTo(HTTP_DATA_BIG_SIZE);
assertThat(response.contentType).isEqualTo("image/jpeg");
assertThat(response.headers.containsKey("Accept-Ranges")).isTrue();
assertThat(response.headers.get("Accept-Ranges").get(0)).isEqualTo("bytes");
assertThat(response.headers.containsKey("Content-Range")).isFalse();
assertThat(response.data).isEqualTo(getFileContent(file));
assertThat(response.data).isEqualTo(loadAssetFile(ASSETS_DATA_BIG_NAME));
proxy.shutdown();
}
@Test
public void testProxyPartialResponse() throws Exception {
int offset = 42000;
File file = newCacheFile();
HttpProxyCache proxy = new HttpProxyCache(new HttpUrlSource(HTTP_DATA_BIG_URL), new FileCache(file));
Response response = readProxyResponse(proxy, offset);
assertThat(response.code).isEqualTo(206);
assertThat(response.contentLength).isEqualTo(HTTP_DATA_BIG_SIZE - offset);
assertThat(response.contentType).isEqualTo("image/jpeg");
assertThat(response.headers.containsKey("Accept-Ranges")).isTrue();
assertThat(response.headers.get("Accept-Ranges").get(0)).isEqualTo("bytes");
assertThat(response.headers.containsKey("Content-Range")).isTrue();
String rangeHeader = String.format("bytes %d-%d/%d", offset, HTTP_DATA_BIG_SIZE, HTTP_DATA_BIG_SIZE);
assertThat(response.headers.get("Content-Range").get(0)).isEqualTo(rangeHeader);
byte[] expectedData = Arrays.copyOfRange(loadAssetFile(ASSETS_DATA_BIG_NAME), offset, HTTP_DATA_BIG_SIZE);
assertThat(response.data).isEqualTo(expectedData);
assertThat(getFileContent(file)).isEqualTo(loadAssetFile(ASSETS_DATA_BIG_NAME));
proxy.shutdown();
}
@Test
public void testAppendCache() throws Exception {
byte[] cachedPortion = generate(1200);
File file = newCacheFile();
File partialFile = new File(file.getParentFile(), file.getName() + ".download");
IoUtils.saveToFile(cachedPortion, partialFile);
Cache cache = new FileCache(partialFile);
assertThat(cache.isCompleted()).isFalse();
HttpProxyCache proxy = new HttpProxyCache(new HttpUrlSource(HTTP_DATA_BIG_URL), cache);
readProxyResponse(proxy);
proxy.shutdown();
assertThat(cache.isCompleted()).isTrue();
byte[] expectedData = loadAssetFile(ASSETS_DATA_BIG_NAME);
System.arraycopy(cachedPortion, 0, expectedData, 0, cachedPortion.length);
assertThat(file.length()).isEqualTo(HTTP_DATA_BIG_SIZE);
assertThat(expectedData).isEqualTo(getFileContent(file));
}
@Test
public void testNoTouchSource() throws Exception {
File file = newCacheFile();
IoUtils.saveToFile(loadAssetFile(ASSETS_DATA_BIG_NAME), file);
FileCache cache = new FileCache(file);
HttpProxyCache proxy = new HttpProxyCache(new HttpUrlSource(HTTP_DATA_BIG_URL), cache);
Response response = readProxyResponse(proxy);
proxy.shutdown();
assertThat(response.code).isEqualTo(200);
proxy = new HttpProxyCache(new AngryHttpUrlSource(HTTP_DATA_BIG_URL, "image/jpeg"), new FileCache(file));
readProxyResponse(proxy);
assertThat(response.code).isEqualTo(200);
}
}

View File

@@ -15,9 +15,14 @@ import static com.danikula.videocache.support.ProxyCacheTestUtils.ASSETS_DATA_BI
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;
/**
* @author Alexey Danilov (danikula@gmail.com).
@@ -61,6 +66,64 @@ public class HttpUrlSourceTest {
assertThat(source.available()).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.available();
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];
source.read(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];
source.read(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];
source.read(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")
@Test
public void testMimeByUrl() throws Exception {

View File

@@ -1,9 +1,10 @@
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.test.BuildConfig;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricGradleTestRunner;
@@ -89,17 +90,6 @@ public class ProxyCacheTest {
assertThat(fetchedData).isEqualTo(sourceCopy);
}
@Test(expected = ProxyCacheException.class)
public void testNoMoreSource() throws Exception {
int sourceSize = 942;
int cacheSize = 6157;
ByteArraySource source = new ByteArraySource(generate(sourceSize));
ByteArrayCache cache = new ByteArrayCache(generate(cacheSize));
ProxyCache proxyCache = new ProxyCache(source, cache);
proxyCache.read(new byte[sourceSize + cacheSize], sourceSize + cacheSize + 1, 10);
Assert.fail();
}
@Test
public void testProxyWithPhlegmaticSource() throws Exception {
int dataSize = 100000;
@@ -178,4 +168,18 @@ public class ProxyCacheTest {
proxyCache.read(new byte[5], 19999, 5);
assertThat(cache.isCompleted()).isTrue();
}
@Test
public void testNoTouchSource() throws Exception {
int dataSize = 2000;
byte[] data = generate(dataSize);
File file = newCacheFile();
IoUtils.saveToFile(data, file);
ProxyCache proxyCache = new ProxyCache(new AngryHttpUrlSource(), new FileCache(file));
byte[] readData = new byte[dataSize];
proxyCache.read(readData, 0, dataSize);
assertThat(readData).isEqualTo(data);
}
}

View File

@@ -1,20 +1,14 @@
package com.danikula.videocache.support;
import android.text.TextUtils;
import com.danikula.videocache.HttpUrlSource;
import com.danikula.videocache.ProxyCacheException;
import com.danikula.videocache.Source;
/**
* {@link HttpUrlSource} that throws exception in all methods.
* {@link Source} that throws exception in all methods.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
public class AngryHttpUrlSource extends HttpUrlSource {
public AngryHttpUrlSource(String url, String mime) {
super(url, mime);
}
public class AngryHttpUrlSource implements Source {
@Override
public int available() throws ProxyCacheException {
@@ -35,12 +29,4 @@ public class AngryHttpUrlSource extends HttpUrlSource {
public int read(byte[] buffer) throws ProxyCacheException {
throw new IllegalStateException();
}
public String getMime() throws ProxyCacheException {
String mime = super.getMime();
if (!TextUtils.isEmpty(mime)) {
return mime;
}
throw new IllegalStateException();
}
}

View File

@@ -1,7 +1,7 @@
package com.danikula.videocache.support;
import com.danikula.android.garden.io.IoUtils;
import com.danikula.videocache.HttpProxyCache;
import com.danikula.videocache.HttpProxyCacheServer;
import com.google.common.io.Files;
import org.robolectric.RuntimeEnvironment;
@@ -21,7 +21,11 @@ import java.util.UUID;
public class ProxyCacheTestUtils {
public static final String HTTP_DATA_URL = "https://dl.dropboxusercontent.com/u/15506779/persistent/proxycache/android.jpg";
public static final String HTTP_DATA_URL_ONE_REDIRECT = "http://bit.ly/1V5PeY5";
public static final String HTTP_DATA_URL_3_REDIRECTS = "http://bit.ly/1KvVmgZ";
public static final String HTTP_DATA_URL_6_REDIRECTS = "http://ow.ly/SugRH";
public static final String HTTP_DATA_BIG_URL = "https://dl.dropboxusercontent.com/u/15506779/persistent/proxycache/phones.jpg";
public static final String HTTP_DATA_BIG_URL_ONE_REDIRECT = "http://bit.ly/1iJ69yA";
public static final String ASSETS_DATA_NAME = "android.jpg";
public static final String ASSETS_DATA_BIG_NAME = "phones.jpg";
public static final int HTTP_DATA_SIZE = 4768;
@@ -31,13 +35,13 @@ public class ProxyCacheTestUtils {
return Files.asByteSource(file).read();
}
public static Response readProxyResponse(HttpProxyCache proxy) throws IOException {
return readProxyResponse(proxy, -1);
public static Response readProxyResponse(HttpProxyCacheServer proxy, String url) throws IOException {
return readProxyResponse(proxy, url, -1);
}
public static Response readProxyResponse(HttpProxyCache proxy, int offset) throws IOException {
URL url = new URL(proxy.getUrl());
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
public static Response readProxyResponse(HttpProxyCacheServer proxy, String url, int offset) throws IOException {
URL proxiedUrl = new URL(proxy.getProxyUrl(url));
HttpURLConnection connection = (HttpURLConnection) proxiedUrl.openConnection();
try {
if (offset >= 0) {
connection.setRequestProperty("Range", "bytes=" + offset + "-");