mirror of
https://github.com/zhigang1992/AndroidVideoCache.git
synced 2026-04-30 12:52:28 +08:00
🎉 VideoCache 1.0 released
This commit is contained in:
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
*.iml
|
||||
.DS_Store
|
||||
.gradle
|
||||
|
||||
/.idea
|
||||
/build
|
||||
/local.properties
|
||||
/gradle.properties
|
||||
/library/build
|
||||
/sample/build
|
||||
/test/build
|
||||
14
build.gradle
Normal file
14
build.gradle
Normal file
@@ -0,0 +1,14 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:1.0.0-rc4'
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -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
|
||||
164
gradlew
vendored
Executable file
164
gradlew
vendored
Executable file
@@ -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 "$@"
|
||||
5
library/build.gradle
Normal file
5
library/build.gradle
Normal file
@@ -0,0 +1,5 @@
|
||||
apply plugin: 'java'
|
||||
|
||||
dependencies {
|
||||
compile 'com.google.android:android:1.6_r2'
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
|
||||
21
library/src/main/java/com/danikula/videocache/Cache.java
Normal file
21
library/src/main/java/com/danikula/videocache/Cache.java
Normal file
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
111
library/src/main/java/com/danikula/videocache/FileCache.java
Normal file
111
library/src/main/java/com/danikula/videocache/FileCache.java
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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:
|
||||
* <pre><code>
|
||||
* HttpProxyCache proxy;
|
||||
* public onCreate(Bundle state) {
|
||||
* super.onCreate(state);
|
||||
* ...
|
||||
* try{
|
||||
* HttpUrlSource source = new HttpUrlSource(YOUR_VIDEO_URI);
|
||||
* Cache cache = new FileCache(new File(context.getCacheDir(), "video.mp4"));
|
||||
* proxy = new HttpProxyCache(source, cache);
|
||||
* videoView.setVideoPath(proxy.getUrl());
|
||||
* } catch(ProxyCacheException e) {
|
||||
* Log.e(LOG_TAG, "Error playing video", e);
|
||||
* }
|
||||
* }
|
||||
* public onDestroy(){
|
||||
* super.onDestroy();
|
||||
*
|
||||
* if (proxy != null) {
|
||||
* proxy.shutdown();
|
||||
* }
|
||||
* }
|
||||
* <code/></pre>
|
||||
*
|
||||
* @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();
|
||||
}
|
||||
}
|
||||
}
|
||||
112
library/src/main/java/com/danikula/videocache/HttpUrlSource.java
Normal file
112
library/src/main/java/com/danikula/videocache/HttpUrlSource.java
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.danikula.videocache;
|
||||
|
||||
final class Preconditions {
|
||||
|
||||
static <T> T checkNotNull(T reference) {
|
||||
if (reference == null) {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
return reference;
|
||||
}
|
||||
|
||||
static <T> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
202
library/src/main/java/com/danikula/videocache/ProxyCache.java
Normal file
202
library/src/main/java/com/danikula/videocache/ProxyCache.java
Normal file
@@ -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}).
|
||||
* <p/>
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
17
library/src/main/java/com/danikula/videocache/Source.java
Normal file
17
library/src/main/java/com/danikula/videocache/Source.java
Normal file
@@ -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;
|
||||
}
|
||||
19
sample/build.gradle
Normal file
19
sample/build.gradle
Normal file
@@ -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')
|
||||
}
|
||||
24
sample/src/main/AndroidManifest.xml
Normal file
24
sample/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.danikula.videocache.sample">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
|
||||
<activity
|
||||
android:name=".VideoActivity"
|
||||
android:label="@string/app_name">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
22
sample/src/main/res/layout/activity_video.xml
Normal file
22
sample/src/main/res/layout/activity_video.xml
Normal file
@@ -0,0 +1,22 @@
|
||||
<RelativeLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".VideoActivity">
|
||||
|
||||
<VideoView
|
||||
android:id="@+id/videoView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
style="@android:style/Widget.Holo.ProgressBar.Horizontal"
|
||||
android:layout_margin="16dp"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_centerHorizontal="true" />
|
||||
|
||||
</RelativeLayout>
|
||||
BIN
sample/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
sample/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.5 KiB |
3
sample/src/main/res/values/strings.xml
Normal file
3
sample/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">VideoCacheSample</string>
|
||||
</resources>
|
||||
1
settings.gradle
Normal file
1
settings.gradle
Normal file
@@ -0,0 +1 @@
|
||||
include ':sample', ':library', ':test'
|
||||
48
test/build.gradle
Normal file
48
test/build.gradle
Normal file
@@ -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'
|
||||
}
|
||||
}
|
||||
10
test/src/main/AndroidManifest.xml
Normal file
10
test/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.danikula.videocache.test">
|
||||
|
||||
<uses-sdk android:minSdkVersion="18" />
|
||||
|
||||
<application />
|
||||
|
||||
</manifest>
|
||||
BIN
test/src/main/assets/android.jpg
Normal file
BIN
test/src/main/assets/android.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.7 KiB |
BIN
test/src/main/assets/phones.jpg
Normal file
BIN
test/src/main/assets/phones.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 92 KiB |
145
test/src/test/java/com/danikula/videocache/FileCacheTest.java
Normal file
145
test/src/test/java/com/danikula/videocache/FileCacheTest.java
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
180
test/src/test/java/com/danikula/videocache/ProxyCacheTest.java
Normal file
180
test/src/test/java/com/danikula/videocache/ProxyCacheTest.java
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<String, List<String>> 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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user