commit 70baa564c059be17fe4e806ffd09ae8173f369eb Author: Alexey Danilov Date: Wed Apr 1 14:24:35 2015 +0300 :tada: VideoCache 1.0 released diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..47d5e74 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +*.iml +.DS_Store +.gradle + +/.idea +/build +/local.properties +/gradle.properties +/library/build +/sample/build +/test/build diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..eb35ea4 --- /dev/null +++ b/build.gradle @@ -0,0 +1,14 @@ +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:1.0.0-rc4' + } +} + +allprojects { + repositories { + mavenCentral() + } +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..8c0fb64 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..0c44860 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Wed Apr 10 15:27:10 PDT 2013 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=http\://services.gradle.org/distributions/gradle-2.2.1-all.zip diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..91a7e26 --- /dev/null +++ b/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/library/build.gradle b/library/build.gradle new file mode 100644 index 0000000..752dcbf --- /dev/null +++ b/library/build.gradle @@ -0,0 +1,5 @@ +apply plugin: 'java' + +dependencies { + compile 'com.google.android:android:1.6_r2' +} \ No newline at end of file diff --git a/library/src/main/java/com/danikula/videocache/ByteArrayCache.java b/library/src/main/java/com/danikula/videocache/ByteArrayCache.java new file mode 100644 index 0000000..3b1aae0 --- /dev/null +++ b/library/src/main/java/com/danikula/videocache/ByteArrayCache.java @@ -0,0 +1,67 @@ +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. + * + * @author Alexey Danilov (danikula@gmail.com). + */ +public class ByteArrayCache implements Cache { + + private volatile byte[] data; + private volatile boolean completed; + + public ByteArrayCache() { + this(new byte[0]); + } + + public ByteArrayCache(byte[] data) { + this.data = Preconditions.checkNotNull(data); + } + + + @Override + public int read(byte[] buffer, long offset, int length) throws ProxyCacheException { + if (offset >= data.length) { + return -1; + } + if (offset > Integer.MAX_VALUE) { + throw new IllegalArgumentException("Too long offset for memory cache " + offset); + } + return new ByteArrayInputStream(data).read(buffer, (int) offset, length); + } + + @Override + public int available() throws ProxyCacheException { + return data.length; + } + + @Override + public void append(byte[] newData, int length) throws ProxyCacheException { + Preconditions.checkNotNull(data); + Preconditions.checkArgument(length >= 0 && length <= newData.length); + + byte[] appendedData = Arrays.copyOf(data, data.length + length); + System.arraycopy(newData, 0, appendedData, data.length, length); + data = appendedData; + } + + @Override + public void close() throws ProxyCacheException { + } + + @Override + public void complete() { + completed = true; + } + + @Override + public boolean isCompleted() { + return completed; + } +} diff --git a/library/src/main/java/com/danikula/videocache/ByteArraySource.java b/library/src/main/java/com/danikula/videocache/ByteArraySource.java new file mode 100644 index 0000000..d66efea --- /dev/null +++ b/library/src/main/java/com/danikula/videocache/ByteArraySource.java @@ -0,0 +1,39 @@ +package com.danikula.videocache; + +import java.io.ByteArrayInputStream; + +/** + * Simple memory based {@link Source} implementation. + * + * @author Alexey Danilov (danikula@gmail.com). + */ +public class ByteArraySource implements Source { + + private final byte[] data; + private ByteArrayInputStream arrayInputStream; + + public ByteArraySource(byte[] data) { + this.data = data; + } + + @Override + public int read(byte[] buffer) throws ProxyCacheException { + return arrayInputStream.read(buffer, 0, buffer.length); + } + + @Override + public int available() throws ProxyCacheException { + return data.length; + } + + @Override + public void open(int offset) throws ProxyCacheException { + arrayInputStream = new ByteArrayInputStream(data); + arrayInputStream.skip(offset); + } + + @Override + public void close() throws ProxyCacheException { + } +} + diff --git a/library/src/main/java/com/danikula/videocache/Cache.java b/library/src/main/java/com/danikula/videocache/Cache.java new file mode 100644 index 0000000..961d980 --- /dev/null +++ b/library/src/main/java/com/danikula/videocache/Cache.java @@ -0,0 +1,21 @@ +package com.danikula.videocache; + +/** + * Cache for proxy. + * + * @author Alexey Danilov (danikula@gmail.com). + */ +public interface Cache { + + int available() throws ProxyCacheException; + + int read(byte[] buffer, long offset, int length) throws ProxyCacheException; + + void append(byte[] data, int length) throws ProxyCacheException; + + void close() throws ProxyCacheException; + + void complete() throws ProxyCacheException; + + boolean isCompleted(); +} diff --git a/library/src/main/java/com/danikula/videocache/CacheListener.java b/library/src/main/java/com/danikula/videocache/CacheListener.java new file mode 100644 index 0000000..a4e507f --- /dev/null +++ b/library/src/main/java/com/danikula/videocache/CacheListener.java @@ -0,0 +1,10 @@ +package com.danikula.videocache; + +/** + * @author Egor Makovsky (yahor.makouski@gmail.com). + */ +public interface CacheListener { + void onError(ProxyCacheException e); + + void onCacheDataAvailable(int cachePercentage); +} diff --git a/library/src/main/java/com/danikula/videocache/FileCache.java b/library/src/main/java/com/danikula/videocache/FileCache.java new file mode 100644 index 0000000..d1600b9 --- /dev/null +++ b/library/src/main/java/com/danikula/videocache/FileCache.java @@ -0,0 +1,111 @@ +package com.danikula.videocache; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; + +import static com.danikula.videocache.Preconditions.checkNotNull; + +/** + * {@link Cache} that uses file for storing data. + * + * @author Alexey Danilov (danikula@gmail.com). + */ +public class FileCache implements Cache { + + private static final String TEMP_POSTFIX = ".download"; + + private RandomAccessFile dataFile; + private File file; + + public FileCache(File file) throws ProxyCacheException { + try { + Preconditions.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"); + } + } catch (IOException e) { + throw new ProxyCacheException("Error using file " + file + " as disc cache", e); + } + } + + @Override + public synchronized int available() throws ProxyCacheException { + try { + return (int) dataFile.length(); + } catch (IOException e) { + throw new ProxyCacheException("Error reading length of file " + dataFile, e); + } + } + + @Override + public synchronized int read(byte[] buffer, long offset, int length) throws ProxyCacheException { + try { + dataFile.seek(offset); + return dataFile.read(buffer, 0, length); + } catch (IOException e) { + String format = "Error reading %d bytes with offset %d from file[%d bytes] to buffer[%d bytes]"; + throw new ProxyCacheException(String.format(format, length, offset, available(), buffer.length), e); + } + } + + @Override + public synchronized void append(byte[] data, int length) throws ProxyCacheException { + try { + if (isCompleted()) { + throw new ProxyCacheException("Error append cache: cache file " + file + " is completed!"); + } + dataFile.seek(available()); + dataFile.write(data, 0, length); + } catch (IOException e) { + String format = "Error writing %d bytes to %s from buffer with size %d"; + throw new ProxyCacheException(String.format(format, length, dataFile, data.length), e); + } + } + + @Override + public synchronized void close() throws ProxyCacheException { + try { + dataFile.close(); + } catch (IOException e) { + throw new ProxyCacheException("Error closing file " + file, e); + } + } + + @Override + public synchronized void complete() throws ProxyCacheException { + if (isCompleted()) { + return; + } + + close(); + String fileName = file.getName().substring(0, file.getName().length() - TEMP_POSTFIX.length()); + File completedFile = new File(file.getParentFile(), fileName); + boolean renamed = file.renameTo(completedFile); + if (!renamed) { + throw new ProxyCacheException("Error renaming file " + file + " to " + completedFile + " for completion!"); + } + file = completedFile; + try { + dataFile = new RandomAccessFile(file, "r"); + } catch (IOException e) { + throw new ProxyCacheException("Error opening " + file + " as disc cache", e); + } + } + + @Override + public synchronized boolean isCompleted() { + return !isTempFile(file); + } + + private boolean isTempFile(File file) { + return file.getName().endsWith(TEMP_POSTFIX); + } +} diff --git a/library/src/main/java/com/danikula/videocache/HttpProxyCache.java b/library/src/main/java/com/danikula/videocache/HttpProxyCache.java new file mode 100644 index 0000000..7b97fea --- /dev/null +++ b/library/src/main/java/com/danikula/videocache/HttpProxyCache.java @@ -0,0 +1,240 @@ +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();
+ *     }
+ * }
+ * 
+ * + * @author Alexey Danilov (danikula@gmail.com). + */ +public 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 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; + 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); + } + } + + public HttpProxyCache(HttpUrlSource source, Cache cache) throws ProxyCacheException { + this(source, cache, false); + } + + 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 { + 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; + 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); + 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; + } + 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(); + boolean mimeKnown = !TextUtils.isEmpty(mime); + int length = cache.isCompleted() ? cache.available() : httpUrlSource.available(); + boolean lengthKnown = length >= 0; + long contentLength = partial ? length - offset : length; + return new StringBuilder() + .append(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(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(); + } + } +} diff --git a/library/src/main/java/com/danikula/videocache/HttpUrlSource.java b/library/src/main/java/com/danikula/videocache/HttpUrlSource.java new file mode 100644 index 0000000..61af99e --- /dev/null +++ b/library/src/main/java/com/danikula/videocache/HttpUrlSource.java @@ -0,0 +1,112 @@ +package com.danikula.videocache; + +import android.text.TextUtils; +import android.util.Log; + +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; + +import static com.danikula.videocache.Preconditions.checkNotNull; +import static java.net.HttpURLConnection.HTTP_OK; +import static java.net.HttpURLConnection.HTTP_PARTIAL; + +/** + * {@link Source} that uses http resource as source for {@link ProxyCache}. + * + * @author Alexey Danilov (danikula@gmail.com). + */ +public class HttpUrlSource implements Source { + + final String url; + private HttpURLConnection connection; + private InputStream inputStream; + private volatile int available = Integer.MIN_VALUE; + private volatile String mime; + + public HttpUrlSource(String url) { + this(url, ProxyCacheUtils.getSupposablyMime(url)); + } + + public HttpUrlSource(String url, String mime) { + this.url = Preconditions.checkNotNull(url); + this.mime = mime; + } + + @Override + public int available() throws ProxyCacheException { + if (available == Integer.MIN_VALUE) { + fetchContentInfo(); + } + return available; + } + + @Override + public void open(int offset) throws ProxyCacheException { + try { + Log.d(ProxyCacheUtils.LOG_TAG, "Open connection " + (offset > 0 ? " with offset " + offset : "") + " to " + url); + connection = (HttpURLConnection) new URL(url).openConnection(); + if (offset > 0) { + connection.setRequestProperty("Range", "bytes=" + offset + "-"); + } + mime = connection.getContentType(); + inputStream = connection.getInputStream(); + 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 { + int contentLength = connection.getContentLength(); + int responseCode = connection.getResponseCode(); + available = responseCode == HTTP_OK ? contentLength : + responseCode == HTTP_PARTIAL ? contentLength + offset : + available; + } + + @Override + public void close() throws ProxyCacheException { + if (connection != null) { + connection.disconnect(); + } + } + + @Override + public int read(byte[] buffer) throws ProxyCacheException { + if (inputStream == null) { + throw new ProxyCacheException("Error reading data from " + url + ": connection is absent!"); + } + try { + return inputStream.read(buffer, 0, buffer.length); + } 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); + 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); + } catch (IOException e) { + throw new ProxyCacheException("Error fetching Content-Length from " + url); + } finally { + if (urlConnection != null) { + urlConnection.disconnect(); + } + } + } + + public String getMime() throws ProxyCacheException { + if (TextUtils.isEmpty(mime)) { + fetchContentInfo(); + } + return mime; + } +} diff --git a/library/src/main/java/com/danikula/videocache/Preconditions.java b/library/src/main/java/com/danikula/videocache/Preconditions.java new file mode 100644 index 0000000..2de77df --- /dev/null +++ b/library/src/main/java/com/danikula/videocache/Preconditions.java @@ -0,0 +1,30 @@ +package com.danikula.videocache; + +final class Preconditions { + + static T checkNotNull(T reference) { + if (reference == null) { + throw new NullPointerException(); + } + return reference; + } + + static T checkNotNull(T reference, String errorMessage) { + if (reference == null) { + throw new NullPointerException(errorMessage); + } + return reference; + } + + static void checkArgument(boolean expression) { + if (!expression) { + throw new IllegalArgumentException(); + } + } + + static void checkArgument(boolean expression, String errorMessage) { + if (!expression) { + throw new IllegalArgumentException(errorMessage); + } + } +} diff --git a/library/src/main/java/com/danikula/videocache/ProxyCache.java b/library/src/main/java/com/danikula/videocache/ProxyCache.java new file mode 100644 index 0000000..36938dd --- /dev/null +++ b/library/src/main/java/com/danikula/videocache/ProxyCache.java @@ -0,0 +1,202 @@ +package com.danikula.videocache; + +import android.os.Handler; +import android.os.Looper; +import android.util.Log; + +import java.util.concurrent.atomic.AtomicInteger; + +import static com.danikula.videocache.Preconditions.checkNotNull; +import static com.danikula.videocache.ProxyCacheUtils.LOG_TAG; + +/** + * Proxy for {@link Source} with caching support ({@link Cache}). + *

+ * Can be used only for sources with persistent data (that doesn't change with time). + * Method {@link #read(byte[], long, int)} will be blocked while fetching data from source. + * Useful for streaming something with caching e.g. streaming video/audio etc. + * + * @author Alexey Danilov (danikula@gmail.com). + */ +public class ProxyCache { + + private static final int MAX_READ_SOURCE_ATTEMPTS = 1; + + private final Source source; + private final Cache cache; + private final Object wc; + private final Handler handler; + private volatile Thread sourceReaderThread; + private volatile boolean stopped; + private final AtomicInteger readSourceErrorsCount; + private CacheListener cacheListener; + private final boolean logEnabled; + + public ProxyCache(Source source, Cache cache, boolean logEnabled) { + this.source = checkNotNull(source); + this.cache = checkNotNull(cache); + this.logEnabled = logEnabled; + this.wc = new Object(); + this.handler = new Handler(Looper.getMainLooper()); + this.readSourceErrorsCount = new AtomicInteger(); + } + + public ProxyCache(Source source, Cache cache) { + this(source, cache, false); + } + + public void setCacheListener(CacheListener cacheListener) { + this.cacheListener = cacheListener; + } + + public int read(byte[] buffer, long offset, int length) throws ProxyCacheException { + ProxyCacheUtils.assertBuffer(buffer, offset, length); + + while (!cache.isCompleted() && cache.available() < (offset + length) && !stopped) { + readSourceAsync(); + waitForSourceData(); + checkIsCacheValid(); + checkReadSourceErrorsCount(); + } + int read = cache.read(buffer, offset, length); + if (isLogEnabled()) { + Log.d(LOG_TAG, "Read data[" + read + " bytes] from cache with offset " + offset + ": " + ProxyCacheUtils.preview(buffer, read)); + } + return read; + } + + private void checkIsCacheValid() throws ProxyCacheException { + int sourceAvailable = source.available(); + if (sourceAvailable > 0 && cache.available() > sourceAvailable) { + throw new ProxyCacheException("Unexpected cache: cache [" + cache.available() + " bytes] > source[" + sourceAvailable + " bytes]"); + } + } + + private void checkReadSourceErrorsCount() throws ProxyCacheException { + int errorsCount = readSourceErrorsCount.get(); + if (errorsCount >= MAX_READ_SOURCE_ATTEMPTS) { + readSourceErrorsCount.set(0); + throw new ProxyCacheException("Error reading source " + errorsCount + " times"); + } + } + + public void shutdown() { + try { + stopped = true; + if (sourceReaderThread != null) { + sourceReaderThread.interrupt(); + } + cache.close(); + } catch (ProxyCacheException e) { + onError(e); + } + } + + private void readSourceAsync() throws ProxyCacheException { + + boolean readingInProgress = sourceReaderThread != null && sourceReaderThread.getState() != Thread.State.TERMINATED; + if (!stopped && !cache.isCompleted() && !readingInProgress) { + sourceReaderThread = new Thread(new SourceReaderRunnable(), "Source reader for ProxyCache"); + sourceReaderThread.start(); + } + } + + private void waitForSourceData() throws ProxyCacheException { + synchronized (wc) { + try { + wc.wait(1000); + } catch (InterruptedException e) { + throw new ProxyCacheException("Waiting source data is interrupted!", e); + } + } + } + + private void notifyNewCacheDataAvailable(final int cachePercentage) { + handler.post(new Runnable() { + @Override + public void run() { + if (cacheListener != null) { + cacheListener.onCacheDataAvailable(cachePercentage); + } + } + }); + + synchronized (wc) { + wc.notifyAll(); + } + } + + private void readSource() { + int cachePercentage = 0; + try { + int offset = cache.available(); + 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)); + } + cache.append(buffer, readBytes); + offset += readBytes; + cachePercentage = offset * 100 / source.available(); + + notifyNewCacheDataAvailable(cachePercentage); + } + if (cache.available() == source.available()) { + cache.complete(); + } + } catch (Throwable e) { + readSourceErrorsCount.incrementAndGet(); + onError(e); + } finally { + closeSource(); + notifyNewCacheDataAvailable(cachePercentage); + } + } + + private void closeSource() { + try { + source.close(); + } catch (ProxyCacheException e) { + onError(new ProxyCacheException("Error closing source " + source, e)); + } + } + + protected final void onError(final Throwable e) { + Log.e(LOG_TAG, "ProxyCache error", e); + handler.post(new ErrorDeliverer(e)); + } + + protected boolean isLogEnabled() { + return logEnabled; + } + + private class SourceReaderRunnable implements Runnable { + + @Override + public void run() { + readSource(); + } + } + + private class ErrorDeliverer implements Runnable { + + private final Throwable error; + + public ErrorDeliverer(Throwable error) { + this.error = error; + } + + @Override + public void run() { + if (error instanceof ProxyCacheException) { + if (cacheListener != null) { + cacheListener.onError((ProxyCacheException) error); + } + } else { + throw new RuntimeException("Unexpected error!", error); + } + } + } +} diff --git a/library/src/main/java/com/danikula/videocache/ProxyCacheException.java b/library/src/main/java/com/danikula/videocache/ProxyCacheException.java new file mode 100644 index 0000000..3b72a14 --- /dev/null +++ b/library/src/main/java/com/danikula/videocache/ProxyCacheException.java @@ -0,0 +1,21 @@ +package com.danikula.videocache; + +/** + * Indicates any error in work of {@link ProxyCache}. + * + * @author Alexey Danilov + */ +public class ProxyCacheException extends Exception { + + public ProxyCacheException(String message) { + super(message); + } + + public ProxyCacheException(String message, Throwable cause) { + super(message, cause); + } + + public ProxyCacheException(Throwable cause) { + super(cause); + } +} diff --git a/library/src/main/java/com/danikula/videocache/ProxyCacheUtils.java b/library/src/main/java/com/danikula/videocache/ProxyCacheUtils.java new file mode 100644 index 0000000..02a09ab --- /dev/null +++ b/library/src/main/java/com/danikula/videocache/ProxyCacheUtils.java @@ -0,0 +1,60 @@ +package com.danikula.videocache; + +import android.text.TextUtils; +import android.webkit.MimeTypeMap; + +import java.io.File; +import java.io.IOException; +import java.util.Arrays; + +import static com.danikula.videocache.Preconditions.checkArgument; +import static com.danikula.videocache.Preconditions.checkNotNull; + +/** + * Just simple utils. + * + * @author Alexey Danilov (danikula@gmail.com). + */ +class ProxyCacheUtils { + + static final String LOG_TAG = "ProxyCache"; + static final int DEFAULT_BUFFER_SIZE = 8 * 1024; + static final int MAX_ARRAY_PREVIEW = 16; + + static String getSupposablyMime(String url) { + MimeTypeMap mimes = MimeTypeMap.getSingleton(); + String extension = MimeTypeMap.getFileExtensionFromUrl(url); + return TextUtils.isEmpty(extension) ? null : mimes.getMimeTypeFromExtension(extension); + } + + static void assertBuffer(byte[] buffer, long offset, int length) { + checkNotNull(buffer, "Buffer must be not null!"); + checkArgument(offset >= 0, "Data offset must be positive!"); + checkArgument(length >= 0 && length <= buffer.length, "Length must be in range [0..buffer.length]"); + } + + static String preview(byte[] data, int length) { + int previewLength = Math.min(MAX_ARRAY_PREVIEW, Math.max(length, 0)); + byte[] dataRange = Arrays.copyOfRange(data, 0, previewLength); + String preview = Arrays.toString(dataRange); + if (previewLength < length) { + preview = preview.substring(0, preview.length() - 1) + ", ...]"; + } + return preview; + } + + static void createDirectory(File directory) throws IOException { + checkNotNull(directory, "File must be not null!"); + if (directory.exists()) { + checkArgument(directory.isDirectory(), "File is not directory!"); + } else { + boolean isCreated = directory.mkdirs(); + if (!isCreated) { + String error = String.format("Directory %s can't be created", directory.getAbsolutePath()); + throw new IOException(error); + } + } + } + + +} diff --git a/library/src/main/java/com/danikula/videocache/Source.java b/library/src/main/java/com/danikula/videocache/Source.java new file mode 100644 index 0000000..98d1648 --- /dev/null +++ b/library/src/main/java/com/danikula/videocache/Source.java @@ -0,0 +1,17 @@ +package com.danikula.videocache; + +/** + * Source for proxy. + * + * @author Alexey Danilov (danikula@gmail.com). + */ +public interface Source { + + int available() throws ProxyCacheException; + + void open(int offset) throws ProxyCacheException; + + void close() throws ProxyCacheException; + + int read(byte[] buffer) throws ProxyCacheException; +} diff --git a/sample/build.gradle b/sample/build.gradle new file mode 100644 index 0000000..904d68a --- /dev/null +++ b/sample/build.gradle @@ -0,0 +1,19 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 22 + buildToolsVersion "22.0.1" + + defaultConfig { + applicationId "com.danikula.videocache.sample" + minSdkVersion 15 + targetSdkVersion 22 + versionCode 1 + versionName "1.0" + } +} + +dependencies { + compile 'com.android.support:appcompat-v7:22.0.0' + compile project(':library') +} diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a08536e --- /dev/null +++ b/sample/src/main/AndroidManifest.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + diff --git a/sample/src/main/java/com/danikula/videocache/sample/VideoActivity.java b/sample/src/main/java/com/danikula/videocache/sample/VideoActivity.java new file mode 100644 index 0000000..d345f43 --- /dev/null +++ b/sample/src/main/java/com/danikula/videocache/sample/VideoActivity.java @@ -0,0 +1,75 @@ +package com.danikula.videocache.sample; + +import android.os.Bundle; +import android.support.v7.app.ActionBarActivity; +import android.util.Log; +import android.widget.ProgressBar; +import android.widget.VideoView; + +import com.danikula.videocache.Cache; +import com.danikula.videocache.CacheListener; +import com.danikula.videocache.FileCache; +import com.danikula.videocache.HttpProxyCache; +import com.danikula.videocache.HttpUrlSource; +import com.danikula.videocache.ProxyCacheException; + +import java.io.File; + +public class VideoActivity extends ActionBarActivity implements CacheListener { + + private static final String LOG_TAG = "VideoActivity"; + private static final String VIDEO_CACHE_NAME = "devbytes.mp4"; + private static final String VIDEO_URL = "https://dl.dropboxusercontent.com/u/15506779/persistent/proxycache/devbytes.mp4"; + + private VideoView videoView; + private ProgressBar progressBar; + private HttpProxyCache proxyCache; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setUpUi(); + playWithCache(); + } + + private void setUpUi() { + setContentView(R.layout.activity_video); + videoView = (VideoView) findViewById(R.id.videoView); + progressBar = (ProgressBar) findViewById(R.id.progressBar); + progressBar.setMax(100); + } + + private void playWithCache() { + try { + Cache cache = new FileCache(new File(getExternalCacheDir(), VIDEO_CACHE_NAME)); + HttpUrlSource source = new HttpUrlSource(VIDEO_URL); + proxyCache = new HttpProxyCache(source, cache); + proxyCache.setCacheListener(this); + videoView.setVideoPath(proxyCache.getUrl()); + videoView.start(); + } catch (ProxyCacheException e) { + // do nothing. onError() handles all errors + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + + if (proxyCache != null) { + proxyCache.shutdown(); + } + } + + @Override + public void onError(ProxyCacheException e) { + Log.e(LOG_TAG, "Error playing video", e); + } + + @Override + public void onCacheDataAvailable(int cachePercentage) { + progressBar.setProgress(cachePercentage); + } + +} diff --git a/sample/src/main/res/layout/activity_video.xml b/sample/src/main/res/layout/activity_video.xml new file mode 100644 index 0000000..0934988 --- /dev/null +++ b/sample/src/main/res/layout/activity_video.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png b/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..324e72c Binary files /dev/null and b/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/sample/src/main/res/values/strings.xml b/sample/src/main/res/values/strings.xml new file mode 100644 index 0000000..2a6f560 --- /dev/null +++ b/sample/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + VideoCacheSample + diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..c83799c --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +include ':sample', ':library', ':test' diff --git a/test/build.gradle b/test/build.gradle new file mode 100644 index 0000000..5914bc9 --- /dev/null +++ b/test/build.gradle @@ -0,0 +1,48 @@ +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath 'com.github.jcandksolutions.gradle:android-unit-test:2.1.1' + } +} + +repositories { + maven { url 'https://github.com/danikula/android-garden/raw/mvn-repo' } +} + +apply plugin: 'com.android.application' + +android { + compileSdkVersion 21 + buildToolsVersion '22.0.1' + + defaultConfig { + applicationId 'com.danikula.proxycache.test' + minSdkVersion 9 + targetSdkVersion 18 // Robolectric doesn't support API 19-21 + versionCode 1 + versionName '0.1' + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_7 + targetCompatibility JavaVersion.VERSION_1_7 + } +} + +apply plugin: 'android-unit-test' + +// to fix AS 0.8.9+ issue (https://github.com/evant/android-studio-unit-test-plugin Troubleshooting) +tasks.findByName("assembleDebug").dependsOn("testDebugClasses") + +dependencies { + compile project (':library') + + testCompile 'junit:junit:4.10' + testCompile 'org.robolectric:robolectric:2.4' + testCompile 'com.squareup:fest-android:1.0.0' + testCompile 'com.google.guava:guava-jdk5:17.0' + testCompile('com.danikula:android-garden:2.0.11-SNAPSHOT') { + exclude group: 'com.google.android' + } +} diff --git a/test/src/main/AndroidManifest.xml b/test/src/main/AndroidManifest.xml new file mode 100644 index 0000000..d667938 --- /dev/null +++ b/test/src/main/AndroidManifest.xml @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/test/src/main/assets/android.jpg b/test/src/main/assets/android.jpg new file mode 100644 index 0000000..a66fd81 Binary files /dev/null and b/test/src/main/assets/android.jpg differ diff --git a/test/src/main/assets/phones.jpg b/test/src/main/assets/phones.jpg new file mode 100644 index 0000000..d29f5a5 Binary files /dev/null and b/test/src/main/assets/phones.jpg differ diff --git a/test/src/test/java/com/danikula/videocache/FileCacheTest.java b/test/src/test/java/com/danikula/videocache/FileCacheTest.java new file mode 100644 index 0000000..a42a64e --- /dev/null +++ b/test/src/test/java/com/danikula/videocache/FileCacheTest.java @@ -0,0 +1,145 @@ +package com.danikula.videocache; + +import com.danikula.android.garden.io.Files; +import com.danikula.android.garden.io.IoUtils; + +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.io.File; +import java.util.Arrays; + +import static com.danikula.videocache.support.ProxyCacheTestUtils.ASSETS_DATA_NAME; +import static com.danikula.videocache.support.ProxyCacheTestUtils.generate; +import static com.danikula.videocache.support.ProxyCacheTestUtils.getFileContent; +import static com.danikula.videocache.support.ProxyCacheTestUtils.getTempFile; +import static com.danikula.videocache.support.ProxyCacheTestUtils.loadAssetFile; +import static com.danikula.videocache.support.ProxyCacheTestUtils.newCacheFile; +import static org.fest.assertions.api.Assertions.assertThat; + +/** + * @author Alexey Danilov (danikula@gmail.com). + */ +@Config(manifest = "src/main/AndroidManifest.xml") +@RunWith(RobolectricTestRunner.class) +public class FileCacheTest { + + @Test + public void testWriteReadDiscCache() throws Exception { + int firstPortionLength = 10000; + byte[] firstDataPortion = generate(firstPortionLength); + File file = newCacheFile(); + Cache fileCache = new FileCache(file); + + fileCache.append(firstDataPortion, firstDataPortion.length); + byte[] readData = new byte[firstPortionLength]; + fileCache.read(readData, 0, firstPortionLength); + assertThat(readData).isEqualTo(firstDataPortion); + byte[] fileContent = getFileContent(getTempFile(file)); + assertThat(readData).isEqualTo(fileContent); + } + + @Test + public void testFileCacheCompletion() throws Exception { + File file = newCacheFile(); + File tempFile = getTempFile(file); + Cache fileCache = new FileCache(file); + assertThat(file.exists()).isFalse(); + assertThat(tempFile.exists()).isTrue(); + + int dataSize = 345; + fileCache.append(generate(dataSize), dataSize); + fileCache.complete(); + + assertThat(file.exists()).isTrue(); + assertThat(tempFile.exists()).isFalse(); + assertThat(file.length()).isEqualTo(dataSize); + } + + @Test(expected = ProxyCacheException.class) + public void testErrorAppendFileCacheAfterCompletion() throws Exception { + Cache fileCache = new FileCache(newCacheFile()); + fileCache.append(generate(20), 10); + fileCache.complete(); + fileCache.append(generate(20), 10); + Assert.fail(); + } + + @Test + public void testAppendDiscCache() throws Exception { + File file = newCacheFile(); + Cache fileCache = new FileCache(file); + + int firstPortionLength = 10000; + byte[] firstDataPortion = generate(firstPortionLength); + fileCache.append(firstDataPortion, firstDataPortion.length); + + int secondPortionLength = 30000; + byte[] secondDataPortion = generate(secondPortionLength * 2); + fileCache.append(secondDataPortion, secondPortionLength); + + byte[] wroteSecondPortion = Arrays.copyOfRange(secondDataPortion, 0, secondPortionLength); + byte[] readData = new byte[secondPortionLength]; + fileCache.read(readData, firstPortionLength, secondPortionLength); + assertThat(readData).isEqualTo(wroteSecondPortion); + + readData = new byte[fileCache.available()]; + fileCache.read(readData, 0, readData.length); + byte[] fileContent = getFileContent(getTempFile(file)); + assertThat(readData).isEqualTo(fileContent); + } + + @Test + public void testIsFileCacheCompleted() throws Exception { + File file = newCacheFile(); + File partialFile = new File(file.getParentFile(), file.getName() + ".download"); + IoUtils.saveToFile(loadAssetFile(ASSETS_DATA_NAME), partialFile); + Cache fileCache = new FileCache(partialFile); + + assertThat(file.exists()).isFalse(); + assertThat(partialFile.exists()).isTrue(); + assertThat(fileCache.isCompleted()).isFalse(); + + fileCache.complete(); + + assertThat(file.exists()).isTrue(); + assertThat(partialFile.exists()).isFalse(); + assertThat(fileCache.isCompleted()).isTrue(); + assertThat(partialFile.exists()).isFalse(); + assertThat(new FileCache(file).isCompleted()).isTrue(); + } + + @Test(expected = ProxyCacheException.class) + public void testErrorWritingCompletedCache() throws Exception { + File file = newCacheFile(); + IoUtils.saveToFile(loadAssetFile(ASSETS_DATA_NAME), file); + FileCache fileCache = new FileCache(file); + fileCache.append(generate(100), 20); + Assert.fail(); + } + + @Test(expected = ProxyCacheException.class) + public void testErrorWritingAfterCompletion() throws Exception { + File file = newCacheFile(); + File partialFile = new File(file.getParentFile(), file.getName() + ".download"); + IoUtils.saveToFile(loadAssetFile(ASSETS_DATA_NAME), partialFile); + FileCache fileCache = new FileCache(partialFile); + fileCache.complete(); + fileCache.append(generate(100), 20); + Assert.fail(); + } + + @Ignore("How to emulate file error?") + @Test(expected = ProxyCacheException.class) + public void testFileErrorForDiscCache() throws Exception { + File file = new File("/system/data.bin"); + FileCache fileCache = new FileCache(file); + Files.delete(file); + fileCache.available(); + Assert.fail(); + } +} diff --git a/test/src/test/java/com/danikula/videocache/HttpProxyCacheTest.java b/test/src/test/java/com/danikula/videocache/HttpProxyCacheTest.java new file mode 100644 index 0000000..0110bda --- /dev/null +++ b/test/src/test/java/com/danikula/videocache/HttpProxyCacheTest.java @@ -0,0 +1,141 @@ +package com.danikula.videocache; + +import com.danikula.android.garden.io.IoUtils; +import com.danikula.videocache.support.AngryHttpUrlSource; +import com.danikula.videocache.support.Response; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +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). + */ +@Config(manifest = "src/main/AndroidManifest.xml") +@RunWith(RobolectricTestRunner.class) +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/HttpUrlSourceTest.java b/test/src/test/java/com/danikula/videocache/HttpUrlSourceTest.java new file mode 100644 index 0000000..7e90cee --- /dev/null +++ b/test/src/test/java/com/danikula/videocache/HttpUrlSourceTest.java @@ -0,0 +1,68 @@ +package com.danikula.videocache; + +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.io.ByteArrayOutputStream; +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.loadAssetFile; +import static org.fest.assertions.api.Assertions.assertThat; + +/** + * @author Alexey Danilov (danikula@gmail.com). + */ +@Config(manifest = "src/main/AndroidManifest.xml") +@RunWith(RobolectricTestRunner.class) +public class HttpUrlSourceTest { + + @Test + public void testHttpUrlSourceRange() throws Exception { + int offset = 1000; + int length = 10; + Source source = new HttpUrlSource(HTTP_DATA_URL); + source.open(offset); + byte[] readData = new byte[length]; + source.read(readData); + source.close(); + byte[] expectedData = Arrays.copyOfRange(loadAssetFile(ASSETS_DATA_NAME), offset, offset + length); + assertThat(readData).isEqualTo(expectedData); + } + + @Test + public void testHttpUrlSourceWithOffset() throws Exception { + int offset = 30000; + Source source = new HttpUrlSource(HTTP_DATA_BIG_URL); + source.open(offset); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + int read; + byte[] buffer = new byte[3000]; + while ((read = (source.read(buffer))) != -1) { + outputStream.write(buffer, 0, read); + } + source.close(); + byte[] expectedData = Arrays.copyOfRange(loadAssetFile(ASSETS_DATA_BIG_NAME), offset, HTTP_DATA_BIG_SIZE); + assertThat(outputStream.toByteArray()).isEqualTo(expectedData); + } + + @Test + public void testFetchContentLength() throws Exception { + Source source = new HttpUrlSource(HTTP_DATA_URL); + assertThat(source.available()).isEqualTo(loadAssetFile(ASSETS_DATA_NAME).length); + } + + @Ignore("Seems Robolectric bug: MimeTypeMap.getFileExtensionFromUrl always returns null") + @Test + public void testMimeByUrl() throws Exception { + assertThat(new HttpUrlSource("http://mysite.by/video.mp4").getMime()).isEqualTo("video/mp4"); + assertThat(new HttpUrlSource(HTTP_DATA_URL).getMime()).isEqualTo("image/jpeg"); + } +} diff --git a/test/src/test/java/com/danikula/videocache/ProxyCacheTest.java b/test/src/test/java/com/danikula/videocache/ProxyCacheTest.java new file mode 100644 index 0000000..e0d9f9e --- /dev/null +++ b/test/src/test/java/com/danikula/videocache/ProxyCacheTest.java @@ -0,0 +1,180 @@ +package com.danikula.videocache; + +import com.danikula.videocache.support.PhlegmaticByteArraySource; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.io.File; +import java.util.Arrays; +import java.util.Random; +import java.util.concurrent.TimeUnit; + +import static com.danikula.videocache.support.ProxyCacheTestUtils.ASSETS_DATA_NAME; +import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_SIZE; +import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_URL; +import static com.danikula.videocache.support.ProxyCacheTestUtils.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 org.fest.assertions.api.Assertions.assertThat; + +/** + * @author Alexey Danilov (danikula@gmail.com). + */ +@Config(manifest = "/src/main/AndroidManifest.xml") +@RunWith(RobolectricTestRunner.class) +public class ProxyCacheTest { + + @Test + public void testNoCache() throws Exception { + byte[] sourceData = generate(345); + ProxyCache proxyCache = new ProxyCache(new ByteArraySource(sourceData), new ByteArrayCache()); + + byte[] buffer = new byte[sourceData.length]; + int read = proxyCache.read(buffer, 0, sourceData.length); + + assertThat(read).isEqualTo(sourceData.length); + assertThat(buffer).isEqualTo(sourceData); + } + + @Test + public void testAllFromCacheNoSource() throws Exception { + byte[] cacheData = generate(34564); + ProxyCache proxyCache = new ProxyCache(new ByteArraySource(new byte[0]), new ByteArrayCache(cacheData)); + + byte[] buffer = new byte[cacheData.length + 100]; + int read = proxyCache.read(buffer, 0, cacheData.length); + byte[] readData = Arrays.copyOfRange(buffer, 0, cacheData.length); + + assertThat(read).isEqualTo(cacheData.length); + assertThat(readData).isEqualTo(cacheData); + } + + @Test + public void testMergeSourceAndCache() throws Exception { + byte[] sourceData = generate(2345); + byte[] cacheData = generate(1048); + ProxyCache proxyCache = new ProxyCache(new ByteArraySource(sourceData), new ByteArrayCache(cacheData)); + + byte[] buffer = new byte[sourceData.length]; + int read = proxyCache.read(buffer, 0, sourceData.length); + + byte[] expected = new byte[sourceData.length]; + System.arraycopy(cacheData, 0, expected, 0, cacheData.length); + System.arraycopy(sourceData, cacheData.length, expected, cacheData.length, sourceData.length - cacheData.length); + assertThat(read).isEqualTo(sourceData.length); + assertThat(buffer).isEqualTo(expected); + } + + @Test + public void testReuseCache() throws Exception { + int size = 20000; + byte[] sourceData = generate(size); + Cache cache = new ByteArrayCache(); + ProxyCache proxyCache = new ProxyCache(new ByteArraySource(sourceData), cache); + byte[] fetchedData = new byte[size]; + proxyCache.read(fetchedData, 0, size); + assertThat(fetchedData).isEqualTo(sourceData); + + byte[] sourceCopy = Arrays.copyOf(sourceData, size); + byte[] newSource = generate(size); + proxyCache = new ProxyCache(new ByteArraySource(newSource), cache); + Arrays.fill(fetchedData, (byte) 0); + proxyCache.read(fetchedData, 0, size); + assertThat(fetchedData).isEqualTo(sourceCopy); + } + + @Test(expected = ProxyCacheException.class) + public void testNoMoreSource() throws Exception { + int sourceSize = 942; + int cacheSize = 6157; + ByteArraySource source = new ByteArraySource(generate(sourceSize)); + ByteArrayCache cache = new ByteArrayCache(generate(cacheSize)); + ProxyCache proxyCache = new ProxyCache(source, cache); + proxyCache.read(new byte[sourceSize + cacheSize], sourceSize + cacheSize + 1, 10); + Assert.fail(); + } + + @Test + public void testProxyWithPhlegmaticSource() throws Exception { + int dataSize = 100000; + byte[] sourceData = generate(dataSize); + Source source = new PhlegmaticByteArraySource(sourceData, 200); + ProxyCache proxyCache = new ProxyCache(source, new FileCache(newCacheFile())); + byte[] readData = new byte[dataSize]; + proxyCache.read(readData, 0, dataSize); + assertThat(readData).isEqualTo(sourceData); + } + + + @Test + public void testReadEnd() throws Exception { + int capacity = 5323; + Source source = new PhlegmaticByteArraySource(generate(capacity), 200); + Cache cache = new FileCache(newCacheFile()); + ProxyCache proxyCache = new ProxyCache(source, cache); + proxyCache.read(new byte[1], capacity - 1, 1); + TimeUnit.MILLISECONDS.sleep(200); // wait for completion + assertThat(cache.isCompleted()).isTrue(); + } + + @Test + public void testReadRandomParts() throws Exception { + int dataSize = 123456; + byte[] sourceData = generate(dataSize); + Source source = new PhlegmaticByteArraySource(sourceData, 300); + File file = newCacheFile(); + Cache cache = new FileCache(file); + ProxyCache proxyCache = new ProxyCache(source, cache); + Random random = new Random(System.currentTimeMillis()); + for (int i = 0; i < 100; i++) { + int offset = random.nextInt(dataSize); + int bufferSize = random.nextInt(dataSize / 4); + bufferSize = Math.min(bufferSize, dataSize - offset); + byte[] buffer = new byte[bufferSize]; + proxyCache.read(buffer, offset, bufferSize); + byte[] dataPortion = Arrays.copyOfRange(sourceData, offset, offset + bufferSize); + assertThat(buffer).isEqualTo(dataPortion); + } + proxyCache.read(new byte[1], dataSize - 1, 1); + TimeUnit.MILLISECONDS.sleep(200); // wait for completion + assertThat(cache.isCompleted()).isTrue(); + assertThat(sourceData).isEqualTo(getFileContent(file)); + } + + @Test + public void testLoadingHttpData() throws Exception { + Source source = new HttpUrlSource(HTTP_DATA_URL); + ProxyCache proxyCache = new ProxyCache(source, new FileCache(newCacheFile())); + byte[] remoteData = new byte[HTTP_DATA_SIZE]; + proxyCache.read(remoteData, 0, HTTP_DATA_SIZE); + proxyCache.shutdown(); + + assertThat(remoteData).isEqualTo(loadAssetFile(ASSETS_DATA_NAME)); + } + + @Test + public void testReadMoreThanAvailable() throws Exception { + byte[] data = generate(20000); + Cache fileCache = new FileCache(newCacheFile()); + ProxyCache proxyCache = new ProxyCache(new ByteArraySource(data), fileCache); + + byte[] buffer = new byte[15000]; + proxyCache.read(buffer, 18000, buffer.length); + byte[] expectedData = new byte[15000]; + System.arraycopy(data, 18000, expectedData, 0, 2000); + assertThat(buffer).isEqualTo(expectedData); + } + + @Test + public void testCompletion() throws Exception { + Cache cache = new FileCache(newCacheFile()); + ProxyCache proxyCache = new ProxyCache(new ByteArraySource(generate(20000)), cache); + proxyCache.read(new byte[5], 19999, 5); + assertThat(cache.isCompleted()).isTrue(); + } +} diff --git a/test/src/test/java/com/danikula/videocache/support/AngryHttpUrlSource.java b/test/src/test/java/com/danikula/videocache/support/AngryHttpUrlSource.java new file mode 100644 index 0000000..3a4ce12 --- /dev/null +++ b/test/src/test/java/com/danikula/videocache/support/AngryHttpUrlSource.java @@ -0,0 +1,46 @@ +package com.danikula.videocache.support; + +import android.text.TextUtils; + +import com.danikula.videocache.HttpUrlSource; +import com.danikula.videocache.ProxyCacheException; + +/** + * {@link HttpUrlSource} 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); + } + + @Override + public int available() throws ProxyCacheException { + throw new IllegalStateException(); + } + + @Override + public void open(int offset) throws ProxyCacheException { + throw new IllegalStateException(); + } + + @Override + public void close() throws ProxyCacheException { + throw new IllegalStateException(); + } + + @Override + public int read(byte[] buffer) throws ProxyCacheException { + throw new IllegalStateException(); + } + + 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/PhlegmaticByteArraySource.java b/test/src/test/java/com/danikula/videocache/support/PhlegmaticByteArraySource.java new file mode 100644 index 0000000..dee5ac1 --- /dev/null +++ b/test/src/test/java/com/danikula/videocache/support/PhlegmaticByteArraySource.java @@ -0,0 +1,31 @@ +package com.danikula.videocache.support; + +import com.danikula.videocache.ByteArraySource; +import com.danikula.videocache.ProxyCacheException; + +import java.util.Random; + +/** + * @author Alexey Danilov (danikula@gmail.com). + */ +public class PhlegmaticByteArraySource extends ByteArraySource { + + private final Random delayGenerator; + private final int maxDelayMs; + + public PhlegmaticByteArraySource(byte[] data, int maxDelayMs) { + super(data); + this.maxDelayMs = maxDelayMs; + this.delayGenerator = new Random(System.currentTimeMillis()); + } + + @Override + public int read(byte[] buffer) throws ProxyCacheException { + try { + Thread.sleep(delayGenerator.nextInt(maxDelayMs)); + } catch (InterruptedException e) { + throw new ProxyCacheException("Error sleeping", e); + } + return super.read(buffer); + } +} diff --git a/test/src/test/java/com/danikula/videocache/support/ProxyCacheTestUtils.java b/test/src/test/java/com/danikula/videocache/support/ProxyCacheTestUtils.java new file mode 100644 index 0000000..798b441 --- /dev/null +++ b/test/src/test/java/com/danikula/videocache/support/ProxyCacheTestUtils.java @@ -0,0 +1,74 @@ +package com.danikula.videocache.support; + +import com.danikula.android.garden.io.IoUtils; +import com.danikula.videocache.HttpProxyCache; +import com.google.common.io.Files; + +import org.robolectric.Robolectric; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Random; +import java.util.UUID; + +/** + * @author Alexey Danilov (danikula@gmail.com). + */ +public class ProxyCacheTestUtils { + + public static final String HTTP_DATA_URL = "https://dl.dropboxusercontent.com/u/15506779/persistent/proxycache/android.jpg"; + public static final String HTTP_DATA_BIG_URL = "https://dl.dropboxusercontent.com/u/15506779/persistent/proxycache/phones.jpg"; + public static final String ASSETS_DATA_NAME = "android.jpg"; + public static final String ASSETS_DATA_BIG_NAME = "phones.jpg"; + public static final int HTTP_DATA_SIZE = 4768; + public static final int HTTP_DATA_BIG_SIZE = 94363; + + public static byte[] getFileContent(File file) throws IOException { + return Files.asByteSource(file).read(); + } + + public static Response readProxyResponse(HttpProxyCache proxy) throws IOException { + return readProxyResponse(proxy, -1); + } + + public static Response readProxyResponse(HttpProxyCache proxy, int offset) throws IOException { + URL url = new URL(proxy.getUrl()); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + try { + if (offset >= 0) { + connection.setRequestProperty("Range", "bytes=" + offset + "-"); + } + return new Response(connection); + } finally { + connection.disconnect(); + } + } + + public static byte[] loadAssetFile(String name) throws IOException { + InputStream in = Robolectric.application.getResources().getAssets().open(name); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + IoUtils.copy(in, out); + IoUtils.closeSilently(in); + IoUtils.closeSilently(out); + return out.toByteArray(); + } + + public static File getTempFile(File file) { + return new File(file.getParentFile(), file.getName() + ".download"); + } + + public static File newCacheFile() { + return new File(Robolectric.application.getCacheDir(), UUID.randomUUID().toString()); + } + + public static byte[] generate(int capacity) { + Random random = new Random(System.currentTimeMillis()); + byte[] result = new byte[capacity]; + random.nextBytes(result); + return result; + } +} diff --git a/test/src/test/java/com/danikula/videocache/support/Response.java b/test/src/test/java/com/danikula/videocache/support/Response.java new file mode 100644 index 0000000..6b68b64 --- /dev/null +++ b/test/src/test/java/com/danikula/videocache/support/Response.java @@ -0,0 +1,25 @@ +package com.danikula.videocache.support; + +import com.google.common.io.ByteStreams; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.util.List; +import java.util.Map; + +public class Response { + + public final int code; + public final byte[] data; + public final int contentLength; + public final String contentType; + public final Map> headers; + + public Response(HttpURLConnection connection) throws IOException { + this.code = connection.getResponseCode(); + this.contentLength = connection.getContentLength(); + this.contentType = connection.getContentType(); + this.headers = connection.getHeaderFields(); + this.data = ByteStreams.toByteArray(connection.getInputStream()); + } +} \ No newline at end of file