diff --git a/library/build.gradle b/library/build.gradle
index 89de2d7..aa90848 100644
--- a/library/build.gradle
+++ b/library/build.gradle
@@ -26,7 +26,7 @@ publish {
userOrg = 'alexeydanilov'
groupId = 'com.danikula'
artifactId = 'videocache'
- publishVersion = '1.0.1'
+ publishVersion = '2.0.7'
description = 'Cache support for android VideoView'
website = 'https://github.com/danikula/AndroidVideoCache'
}
diff --git a/library/src/main/java/com/danikula/videocache/ByteArrayCache.java b/library/src/main/java/com/danikula/videocache/ByteArrayCache.java
index 3b1aae0..be16aa0 100644
--- a/library/src/main/java/com/danikula/videocache/ByteArrayCache.java
+++ b/library/src/main/java/com/danikula/videocache/ByteArrayCache.java
@@ -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) {
diff --git a/library/src/main/java/com/danikula/videocache/CacheListener.java b/library/src/main/java/com/danikula/videocache/CacheListener.java
index a4e507f..e4813ff 100644
--- a/library/src/main/java/com/danikula/videocache/CacheListener.java
+++ b/library/src/main/java/com/danikula/videocache/CacheListener.java
@@ -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);
}
diff --git a/library/src/main/java/com/danikula/videocache/FileCache.java b/library/src/main/java/com/danikula/videocache/FileCache.java
index 46aea23..a99034c 100644
--- a/library/src/main/java/com/danikula/videocache/FileCache.java
+++ b/library/src/main/java/com/danikula/videocache/FileCache.java
@@ -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);
}
+
}
diff --git a/library/src/main/java/com/danikula/videocache/FileNameGenerator.java b/library/src/main/java/com/danikula/videocache/FileNameGenerator.java
new file mode 100644
index 0000000..afc7c6d
--- /dev/null
+++ b/library/src/main/java/com/danikula/videocache/FileNameGenerator.java
@@ -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);
+
+}
diff --git a/library/src/main/java/com/danikula/videocache/GetRequest.java b/library/src/main/java/com/danikula/videocache/GetRequest.java
new file mode 100644
index 0000000..b154da0
--- /dev/null
+++ b/library/src/main/java/com/danikula/videocache/GetRequest.java
@@ -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 +
+ '}';
+ }
+}
diff --git a/library/src/main/java/com/danikula/videocache/HttpProxyCache.java b/library/src/main/java/com/danikula/videocache/HttpProxyCache.java
index 7b97fea..bd99b85 100644
--- a/library/src/main/java/com/danikula/videocache/HttpProxyCache.java
+++ b/library/src/main/java/com/danikula/videocache/HttpProxyCache.java
@@ -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:
- *
- * 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();
- * }
- * }
- *
+ * {@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);
}
}
}
diff --git a/library/src/main/java/com/danikula/videocache/HttpProxyCacheServer.java b/library/src/main/java/com/danikula/videocache/HttpProxyCacheServer.java
new file mode 100644
index 0000000..a9b6d94
--- /dev/null
+++ b/library/src/main/java/com/danikula/videocache/HttpProxyCacheServer.java
@@ -0,0 +1,240 @@
+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:
+ *
+ * public onCreate(Bundle state) {
+ * super.onCreate(state);
+ *
+ * HttpProxyCacheServer proxy = getProxy();
+ * String proxyUrl = proxy.getProxyUrl(VIDEO_URL);
+ * videoView.setVideoPath(proxyUrl);
+ * }
+ *
+ * private HttpProxyCacheServer getProxy() {
+ * // should return single instance of HttpProxyCacheServer shared for whole app.
+ * }
+ *
+ *
+ * @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 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);
+ }
+ }
+
+ 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 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);
+ }
+ }
+
+}
diff --git a/library/src/main/java/com/danikula/videocache/HttpProxyCacheServerClients.java b/library/src/main/java/com/danikula/videocache/HttpProxyCacheServerClients.java
new file mode 100644
index 0000000..333f0fe
--- /dev/null
+++ b/library/src/main/java/com/danikula/videocache/HttpProxyCacheServerClients.java
@@ -0,0 +1,102 @@
+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 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 {
+ proxyCache = proxyCache == null ? newHttpProxyCache() : proxyCache;
+ try {
+ clientsCount.incrementAndGet();
+ proxyCache.processRequest(request, socket);
+ } finally {
+ int count = clientsCount.decrementAndGet();
+ if (count <= 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);
+ }
+
+ 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 listeners;
+
+ public UiListenerHandler(String url, List 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);
+ }
+ }
+ }
+}
diff --git a/library/src/main/java/com/danikula/videocache/HttpUrlSource.java b/library/src/main/java/com/danikula/videocache/HttpUrlSource.java
index 61af99e..20379ae 100644
--- a/library/src/main/java/com/danikula/videocache/HttpUrlSource.java
+++ b/library/src/main/java/com/danikula/videocache/HttpUrlSource.java
@@ -5,10 +5,11 @@ import android.util.Log;
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.LOG_TAG;
import static java.net.HttpURLConnection.HTTP_OK;
import static java.net.HttpURLConnection.HTTP_PARTIAL;
@@ -19,7 +20,7 @@ import static java.net.HttpURLConnection.HTTP_PARTIAL;
*/
public class HttpUrlSource implements Source {
- final String url;
+ public final String url;
private HttpURLConnection connection;
private InputStream inputStream;
private volatile int available = Integer.MIN_VALUE;
@@ -45,23 +46,23 @@ 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);
+ 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();
inputStream = connection.getInputStream();
- readSourceAvailableBytes(connection, offset);
+ available = readSourceAvailableBytes(connection, offset);
} 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) throws IOException {
int contentLength = connection.getContentLength();
int responseCode = connection.getResponseCode();
- available = responseCode == HTTP_OK ? contentLength :
+ return responseCode == HTTP_OK ? contentLength :
responseCode == HTTP_PARTIAL ? contentLength + offset :
available;
}
@@ -80,20 +81,22 @@ 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;
try {
urlConnection = (HttpURLConnection) new URL(url).openConnection();
urlConnection.setRequestMethod("HEAD");
available = urlConnection.getContentLength();
mime = urlConnection.getContentType();
- Log.d(ProxyCacheUtils.LOG_TAG, "Content-Length of " + url + " is " + available + " bytes, mime is " + mime);
+ Log.i(LOG_TAG, "Info read: " + this);
} catch (IOException e) {
throw new ProxyCacheException("Error fetching Content-Length from " + url);
} finally {
@@ -109,4 +112,13 @@ public class HttpUrlSource implements Source {
}
return mime;
}
+
+ @Override
+ public String toString() {
+ return "HttpUrlSource{" +
+ "url='" + url + '\'' +
+ ", available=" + available +
+ ", mime='" + mime + '\'' +
+ '}';
+ }
}
diff --git a/library/src/main/java/com/danikula/videocache/InterruptedProxyCacheException.java b/library/src/main/java/com/danikula/videocache/InterruptedProxyCacheException.java
new file mode 100644
index 0000000..f316082
--- /dev/null
+++ b/library/src/main/java/com/danikula/videocache/InterruptedProxyCacheException.java
@@ -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);
+ }
+}
diff --git a/library/src/main/java/com/danikula/videocache/Md5FileNameGenerator.java b/library/src/main/java/com/danikula/videocache/Md5FileNameGenerator.java
new file mode 100644
index 0000000..2fad4c5
--- /dev/null
+++ b/library/src/main/java/com/danikula/videocache/Md5FileNameGenerator.java
@@ -0,0 +1,56 @@
+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();
+ }
+}
diff --git a/library/src/main/java/com/danikula/videocache/Preconditions.java b/library/src/main/java/com/danikula/videocache/Preconditions.java
index 2de77df..bff4193 100644
--- a/library/src/main/java/com/danikula/videocache/Preconditions.java
+++ b/library/src/main/java/com/danikula/videocache/Preconditions.java
@@ -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 checkNotNull(T reference, String errorMessage) {
if (reference == null) {
throw new NullPointerException(errorMessage);
diff --git a/library/src/main/java/com/danikula/videocache/ProxyCache.java b/library/src/main/java/com/danikula/videocache/ProxyCache.java
index 3c56b89..6cbf5cf 100644
--- a/library/src/main/java/com/danikula/videocache/ProxyCache.java
+++ b/library/src/main/java/com/danikula/videocache/ProxyCache.java
@@ -1,8 +1,5 @@
package com.danikula.videocache;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Message;
import android.util.Log;
import java.util.concurrent.atomic.AtomicInteger;
@@ -19,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 ListenerHandler 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 ListenerHandler();
- 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 {
@@ -59,11 +43,7 @@ public class ProxyCache {
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;
+ return cache.read(buffer, offset, length);
}
private void checkIsCacheValid() throws ProxyCacheException {
@@ -82,14 +62,17 @@ 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);
}
}
@@ -112,13 +95,16 @@ public class ProxyCache {
}
private void notifyNewCacheDataAvailable(int cachePercentage) {
- handler.deliverCachePercentage(cachePercentage);
+ onCacheAvailable(cachePercentage);
synchronized (wc) {
wc.notifyAll();
}
}
+ protected void onCacheAvailable(int percents){
+ }
+
private void readSource() {
int cachePercentage = 0;
try {
@@ -126,19 +112,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);
@@ -148,6 +134,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();
@@ -157,12 +155,12 @@ public class ProxyCache {
}
protected final void onError(final Throwable e) {
- Log.e(LOG_TAG, "ProxyCache error", e);
- handler.deliverError(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 {
@@ -172,55 +170,4 @@ public class ProxyCache {
readSource();
}
}
-
- private final class ListenerHandler extends Handler {
-
- private static final int MSG_ERROR = 1;
- private static final int MSG_CACHE_PERCENTAGE = 2;
-
- public ListenerHandler() {
- super(Looper.getMainLooper());
- }
-
- public void deliverCachePercentage(int percents) {
- if (cacheListener != null) {
- send(MSG_CACHE_PERCENTAGE, percents, null);
- }
- }
-
- public void deliverError(Throwable error) {
- if (isFatalError(error) || cacheListener != null) {
- send(MSG_ERROR, 0, error);
- }
- }
-
- private boolean isFatalError(Throwable error) {
- return !(error instanceof ProxyCacheException);
- }
-
- private void send(int what, int arg1, Object data) {
- Message message = obtainMessage(what);
- message.arg1 = arg1;
- message.obj = data;
- sendMessage(message);
- }
-
- @Override
- public void handleMessage(Message msg) {
- switch (msg.what) {
- case MSG_CACHE_PERCENTAGE:
- cacheListener.onCacheDataAvailable(msg.arg1);
- break;
- case MSG_ERROR:
- Throwable error = (Throwable) msg.obj;
- if (isFatalError(error)) {
- throw new RuntimeException("Unexpected error!", error);
- }
- cacheListener.onError((ProxyCacheException) error);
- break;
- default:
- throw new RuntimeException("Unknown message " + msg);
- }
- }
- }
}
diff --git a/library/src/main/java/com/danikula/videocache/ProxyCacheUtils.java b/library/src/main/java/com/danikula/videocache/ProxyCacheUtils.java
index 02a09ab..3a61f7b 100644
--- a/library/src/main/java/com/danikula/videocache/ProxyCacheUtils.java
+++ b/library/src/main/java/com/danikula/videocache/ProxyCacheUtils.java
@@ -5,6 +5,9 @@ import android.webkit.MimeTypeMap;
import java.io.File;
import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
import java.util.Arrays;
import static com.danikula.videocache.Preconditions.checkArgument;
@@ -56,5 +59,19 @@ 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);
+ }
+ }
}
diff --git a/sample/build.gradle b/sample/build.gradle
index 96443a3..8ffffc2 100644
--- a/sample/build.gradle
+++ b/sample/build.gradle
@@ -8,7 +8,7 @@ buildscript {
}
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' }
}
@@ -36,9 +36,10 @@ apt {
}
dependencies {
+// compile project(':library')
compile 'com.android.support:support-v4:23.0.0'
compile 'org.androidannotations:androidannotations-api:3.3.2'
- compile 'com.danikula:videocache:1.0'
+ compile 'com.danikula:videocache:2.0.7'
compile 'com.viewpagerindicator:library:2.4.2-SNAPSHOT@aar'
apt 'org.androidannotations:androidannotations:3.3.2'
}
diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml
index 3b38e1d..3ba1f44 100644
--- a/sample/src/main/AndroidManifest.xml
+++ b/sample/src/main/AndroidManifest.xml
@@ -7,6 +7,7 @@
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 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 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 testProxyPartialResponse() throws Exception {
+ int offset = 42000;
+ Pair 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));
+ }
+
+ private Pair 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 readProxyData(String url) throws IOException {
+ return readProxyData(url, -1);
+ }
+}
diff --git a/test/src/test/java/com/danikula/videocache/HttpProxyCacheTest.java b/test/src/test/java/com/danikula/videocache/HttpProxyCacheTest.java
deleted file mode 100644
index 8672a83..0000000
--- a/test/src/test/java/com/danikula/videocache/HttpProxyCacheTest.java
+++ /dev/null
@@ -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);
- }
-}
diff --git a/test/src/test/java/com/danikula/videocache/ProxyCacheTest.java b/test/src/test/java/com/danikula/videocache/ProxyCacheTest.java
index bff1e69..3445f91 100644
--- a/test/src/test/java/com/danikula/videocache/ProxyCacheTest.java
+++ b/test/src/test/java/com/danikula/videocache/ProxyCacheTest.java
@@ -1,5 +1,7 @@
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;
@@ -178,4 +180,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);
+ }
}
diff --git a/test/src/test/java/com/danikula/videocache/support/AngryHttpUrlSource.java b/test/src/test/java/com/danikula/videocache/support/AngryHttpUrlSource.java
index 3a4ce12..985dc94 100644
--- a/test/src/test/java/com/danikula/videocache/support/AngryHttpUrlSource.java
+++ b/test/src/test/java/com/danikula/videocache/support/AngryHttpUrlSource.java
@@ -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();
- }
}
diff --git a/test/src/test/java/com/danikula/videocache/support/ProxyCacheTestUtils.java b/test/src/test/java/com/danikula/videocache/support/ProxyCacheTestUtils.java
index 0b20c29..0eac519 100644
--- a/test/src/test/java/com/danikula/videocache/support/ProxyCacheTestUtils.java
+++ b/test/src/test/java/com/danikula/videocache/support/ProxyCacheTestUtils.java
@@ -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;
@@ -31,13 +31,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 + "-");