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 + "-");