mirror of
https://github.com/zhigang1992/AndroidVideoCache.git
synced 2026-04-29 12:25:34 +08:00
allow to use shared proxy with shared cache for multiple clients
This commit is contained in:
@@ -26,7 +26,7 @@ publish {
|
|||||||
userOrg = 'alexeydanilov'
|
userOrg = 'alexeydanilov'
|
||||||
groupId = 'com.danikula'
|
groupId = 'com.danikula'
|
||||||
artifactId = 'videocache'
|
artifactId = 'videocache'
|
||||||
publishVersion = '1.0.1'
|
publishVersion = '2.0.7'
|
||||||
description = 'Cache support for android VideoView'
|
description = 'Cache support for android VideoView'
|
||||||
website = 'https://github.com/danikula/AndroidVideoCache'
|
website = 'https://github.com/danikula/AndroidVideoCache'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,6 @@ package com.danikula.videocache;
|
|||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
import static com.danikula.videocache.Preconditions.checkArgument;
|
|
||||||
import static com.danikula.videocache.Preconditions.checkNotNull;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple memory based {@link Cache} implementation.
|
* Simple memory based {@link Cache} implementation.
|
||||||
*
|
*
|
||||||
@@ -24,7 +21,6 @@ public class ByteArrayCache implements Cache {
|
|||||||
this.data = Preconditions.checkNotNull(data);
|
this.data = Preconditions.checkNotNull(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int read(byte[] buffer, long offset, int length) throws ProxyCacheException {
|
public int read(byte[] buffer, long offset, int length) throws ProxyCacheException {
|
||||||
if (offset >= data.length) {
|
if (offset >= data.length) {
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
package com.danikula.videocache;
|
package com.danikula.videocache;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Egor Makovsky (yahor.makouski@gmail.com).
|
* Listener for cache availability.
|
||||||
|
*
|
||||||
|
* @author Egor Makovsky (yahor.makouski@gmail.com)
|
||||||
|
* @author Alexey Danilov (danikula@gmail.com).
|
||||||
*/
|
*/
|
||||||
public interface CacheListener {
|
public interface CacheListener {
|
||||||
void onError(ProxyCacheException e);
|
|
||||||
|
|
||||||
void onCacheDataAvailable(int cachePercentage);
|
void onCacheAvailable(File cacheFile, String url, int percentsAvailable);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,22 +15,16 @@ public class FileCache implements Cache {
|
|||||||
|
|
||||||
private static final String TEMP_POSTFIX = ".download";
|
private static final String TEMP_POSTFIX = ".download";
|
||||||
|
|
||||||
|
public File file;
|
||||||
private RandomAccessFile dataFile;
|
private RandomAccessFile dataFile;
|
||||||
private File file;
|
|
||||||
|
|
||||||
public FileCache(File file) throws ProxyCacheException {
|
public FileCache(File file) throws ProxyCacheException {
|
||||||
try {
|
try {
|
||||||
checkNotNull(file);
|
checkNotNull(file);
|
||||||
boolean partialFile = isTempFile(file);
|
ProxyCacheUtils.createDirectory(file.getParentFile());
|
||||||
boolean completed = file.exists() && !partialFile;
|
boolean completed = file.exists();
|
||||||
if (completed) {
|
this.file = completed ? file : new File(file.getParentFile(), file.getName() + TEMP_POSTFIX);
|
||||||
this.dataFile = new RandomAccessFile(file, "r");
|
this.dataFile = new RandomAccessFile(this.file, completed ? "r" : "rw");
|
||||||
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) {
|
} catch (IOException e) {
|
||||||
throw new ProxyCacheException("Error using file " + file + " as disc cache", e);
|
throw new ProxyCacheException("Error using file " + file + " as disc cache", e);
|
||||||
}
|
}
|
||||||
@@ -41,7 +35,7 @@ public class FileCache implements Cache {
|
|||||||
try {
|
try {
|
||||||
return (int) dataFile.length();
|
return (int) dataFile.length();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new ProxyCacheException("Error reading length of file " + dataFile, e);
|
throw new ProxyCacheException("Error reading length of file " + file, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,4 +111,5 @@ public class FileCache implements Cache {
|
|||||||
private boolean isTempFile(File file) {
|
private boolean isTempFile(File file) {
|
||||||
return file.getName().endsWith(TEMP_POSTFIX);
|
return file.getName().endsWith(TEMP_POSTFIX);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.danikula.videocache;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generator for files to be used for caching.
|
||||||
|
*
|
||||||
|
* @author Alexey Danilov (danikula@gmail.com).
|
||||||
|
*/
|
||||||
|
public interface FileNameGenerator {
|
||||||
|
|
||||||
|
File generate(String url);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package com.danikula.videocache;
|
||||||
|
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import static com.danikula.videocache.Preconditions.checkNotNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model for Http GET request.
|
||||||
|
*
|
||||||
|
* @author Alexey Danilov (danikula@gmail.com).
|
||||||
|
*/
|
||||||
|
class GetRequest {
|
||||||
|
|
||||||
|
private static final Pattern RANGE_HEADER_PATTERN = Pattern.compile("[R,r]ange:[ ]?bytes=(\\d*)-");
|
||||||
|
private static final Pattern URL_PATTERN = Pattern.compile("GET /(.*) HTTP");
|
||||||
|
|
||||||
|
public final String uri;
|
||||||
|
public final long rangeOffset;
|
||||||
|
public final boolean partial;
|
||||||
|
|
||||||
|
public GetRequest(String request) {
|
||||||
|
checkNotNull(request);
|
||||||
|
long offset = findRangeOffset(request);
|
||||||
|
this.rangeOffset = Math.max(0, offset);
|
||||||
|
this.partial = offset >= 0;
|
||||||
|
this.uri = findUri(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GetRequest read(InputStream inputStream) throws IOException {
|
||||||
|
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
|
||||||
|
StringBuilder stringRequest = new StringBuilder();
|
||||||
|
String line;
|
||||||
|
while (!TextUtils.isEmpty(line = reader.readLine())) { // until new line (headers ending)
|
||||||
|
stringRequest.append(line).append('\n');
|
||||||
|
}
|
||||||
|
return new GetRequest(stringRequest.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private long findRangeOffset(String request) {
|
||||||
|
Matcher matcher = RANGE_HEADER_PATTERN.matcher(request);
|
||||||
|
if (matcher.find()) {
|
||||||
|
String rangeValue = matcher.group(1);
|
||||||
|
return Long.parseLong(rangeValue);
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String findUri(String request) {
|
||||||
|
Matcher matcher = URL_PATTERN.matcher(request);
|
||||||
|
if (matcher.find()) {
|
||||||
|
return matcher.group(1);
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("Invalid request `" + request + "`: url not found!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "GetRequest{" +
|
||||||
|
"uri='" + uri + '\'' +
|
||||||
|
", rangeOffset=" + rangeOffset +
|
||||||
|
", partial=" + partial +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,240 +1,76 @@
|
|||||||
package com.danikula.videocache;
|
package com.danikula.videocache;
|
||||||
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import java.io.BufferedOutputStream;
|
import java.io.BufferedOutputStream;
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.net.ServerSocket;
|
|
||||||
import java.net.Socket;
|
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.
|
* {@link ProxyCache} that read http url and writes data to {@link Socket}
|
||||||
* 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).
|
* @author Alexey Danilov (danikula@gmail.com).
|
||||||
*/
|
*/
|
||||||
public class HttpProxyCache extends ProxyCache {
|
class HttpProxyCache extends ProxyCache {
|
||||||
|
|
||||||
private static final int CLIENT_COUNT = 3;
|
private final HttpUrlSource source;
|
||||||
private static final Pattern RANGE_HEADER_PATTERN = Pattern.compile("[R,r]ange:[ ]?bytes=(\\d*)-");
|
private final FileCache cache;
|
||||||
private static final String PROXY_HOST = "127.0.0.1";
|
private CacheListener listener;
|
||||||
|
|
||||||
private final HttpUrlSource httpUrlSource;
|
public HttpProxyCache(HttpUrlSource source, FileCache cache) {
|
||||||
private final Cache cache;
|
super(source, 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.cache = cache;
|
||||||
this.executorService = Executors.newFixedThreadPool(CLIENT_COUNT);
|
this.source = source;
|
||||||
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 {
|
public void registerCacheListener(CacheListener cacheListener) {
|
||||||
this(source, cache, false);
|
this.listener = cacheListener;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getUrl() {
|
public void processRequest(GetRequest request, Socket socket) throws IOException, ProxyCacheException {
|
||||||
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());
|
OutputStream out = new BufferedOutputStream(socket.getOutputStream());
|
||||||
byte[] buffer = new byte[ProxyCacheUtils.DEFAULT_BUFFER_SIZE];
|
byte[] buffer = new byte[ProxyCacheUtils.DEFAULT_BUFFER_SIZE];
|
||||||
int readBytes;
|
int readBytes;
|
||||||
long offset = Math.max(rangeOffset, 0);
|
|
||||||
boolean headersWrote = false;
|
boolean headersWrote = false;
|
||||||
|
long offset = request.rangeOffset;
|
||||||
while ((readBytes = read(buffer, offset, buffer.length)) != -1) {
|
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
|
// tiny optimization: to prevent HEAD request in source for content-length. content-length 'll available after reading source
|
||||||
if (!headersWrote) {
|
if (!headersWrote) {
|
||||||
writeResponseHeaders(out, rangeOffset);
|
String responseHeaders = newResponseHeaders(request);
|
||||||
|
out.write(responseHeaders.getBytes("UTF-8"));
|
||||||
headersWrote = true;
|
headersWrote = true;
|
||||||
}
|
}
|
||||||
out.write(buffer, 0, readBytes);
|
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;
|
offset += readBytes;
|
||||||
|
if (cache.isCompleted()) {
|
||||||
|
onCacheAvailable(100);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
out.flush();
|
out.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void writeResponseHeaders(OutputStream out, long rangeOffset) throws IOException, ProxyCacheException {
|
private String newResponseHeaders(GetRequest request) throws IOException, ProxyCacheException {
|
||||||
String responseHeaders = newResponseHeaders(rangeOffset);
|
String mime = source.getMime();
|
||||||
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);
|
boolean mimeKnown = !TextUtils.isEmpty(mime);
|
||||||
int length = cache.isCompleted() ? cache.available() : httpUrlSource.available();
|
int length = cache.isCompleted() ? cache.available() : source.available();
|
||||||
boolean lengthKnown = length >= 0;
|
boolean lengthKnown = length >= 0;
|
||||||
long contentLength = partial ? length - offset : length;
|
long contentLength = request.partial ? length - request.rangeOffset : length;
|
||||||
|
boolean addRange = lengthKnown && request.partial;
|
||||||
return new StringBuilder()
|
return new StringBuilder()
|
||||||
.append(partial ? "HTTP/1.1 206 PARTIAL CONTENT\n" : "HTTP/1.1 200 OK\n")
|
.append(request.partial ? "HTTP/1.1 206 PARTIAL CONTENT\n" : "HTTP/1.1 200 OK\n")
|
||||||
.append("Accept-Ranges: bytes\n")
|
.append("Accept-Ranges: bytes\n")
|
||||||
.append(lengthKnown ? String.format("Content-Length: %d\n", contentLength) : "")
|
.append(lengthKnown ? String.format("Content-Length: %d\n", contentLength) : "")
|
||||||
.append(lengthKnown && partial ? String.format("Content-Range: bytes %d-%d/%d\n", offset, length, length) : "")
|
.append(addRange ? String.format("Content-Range: bytes %d-%d/%d\n", request.rangeOffset, length, length) : "")
|
||||||
.append(mimeKnown ? String.format("Content-Type: %s\n", mime) : "")
|
.append(mimeKnown ? String.format("Content-Type: %s\n", mime) : "")
|
||||||
.append("\n") // headers end
|
.append("\n") // headers end
|
||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private String readRequest(InputStream inputStream) throws IOException {
|
@Override
|
||||||
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
|
protected void onCacheAvailable(int percents) {
|
||||||
StringBuilder str = new StringBuilder();
|
if (listener != null) {
|
||||||
String line;
|
listener.onCacheAvailable(cache.file, source.url, percents);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,240 @@
|
|||||||
|
package com.danikula.videocache;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.ServerSocket;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.net.SocketException;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
|
import static com.danikula.videocache.Preconditions.checkAllNotNull;
|
||||||
|
import static com.danikula.videocache.Preconditions.checkNotNull;
|
||||||
|
import static com.danikula.videocache.ProxyCacheUtils.LOG_TAG;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple lightweight proxy server with file caching support that handles HTTP requests.
|
||||||
|
* Typical usage:
|
||||||
|
* <pre><code>
|
||||||
|
* public onCreate(Bundle state) {
|
||||||
|
* super.onCreate(state);
|
||||||
|
* <p/>
|
||||||
|
* HttpProxyCacheServer proxy = getProxy();
|
||||||
|
* String proxyUrl = proxy.getProxyUrl(VIDEO_URL);
|
||||||
|
* videoView.setVideoPath(proxyUrl);
|
||||||
|
* }
|
||||||
|
* <p/>
|
||||||
|
* private HttpProxyCacheServer getProxy() {
|
||||||
|
* // should return single instance of HttpProxyCacheServer shared for whole app.
|
||||||
|
* }
|
||||||
|
* <code/></pre>
|
||||||
|
*
|
||||||
|
* @author Alexey Danilov (danikula@gmail.com).
|
||||||
|
*/
|
||||||
|
public class HttpProxyCacheServer {
|
||||||
|
|
||||||
|
private static final String PROXY_HOST = "127.0.0.1";
|
||||||
|
|
||||||
|
private final Object clientsLock = new Object();
|
||||||
|
private final ExecutorService socketProcessor = Executors.newFixedThreadPool(8);
|
||||||
|
private final Map<String, HttpProxyCacheServerClients> clientsMap = new ConcurrentHashMap<>();
|
||||||
|
private final ServerSocket serverSocket;
|
||||||
|
private final int port;
|
||||||
|
private final Thread waitConnectionThread;
|
||||||
|
private final FileNameGenerator fileNameGenerator;
|
||||||
|
|
||||||
|
public HttpProxyCacheServer(FileNameGenerator fileNameGenerator) {
|
||||||
|
this.fileNameGenerator = checkNotNull(fileNameGenerator);
|
||||||
|
try {
|
||||||
|
InetAddress inetAddress = InetAddress.getByName(PROXY_HOST);
|
||||||
|
this.serverSocket = new ServerSocket(0, 8, inetAddress);
|
||||||
|
this.port = serverSocket.getLocalPort();
|
||||||
|
CountDownLatch startSignal = new CountDownLatch(1);
|
||||||
|
this.waitConnectionThread = new Thread(new WaitRequestsRunnable(startSignal));
|
||||||
|
this.waitConnectionThread.start();
|
||||||
|
startSignal.await(); // freeze thread, wait for server starts
|
||||||
|
} catch (IOException | InterruptedException e) {
|
||||||
|
socketProcessor.shutdown();
|
||||||
|
throw new IllegalStateException("Error starting local proxy server", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getProxyUrl(String url) {
|
||||||
|
return String.format("http://%s:%d/%s", PROXY_HOST, port, ProxyCacheUtils.encode(url));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void registerCacheListener(CacheListener cacheListener, String url) {
|
||||||
|
checkAllNotNull(cacheListener, url);
|
||||||
|
synchronized (clientsLock) {
|
||||||
|
try {
|
||||||
|
getClients(url).registerCacheListener(cacheListener);
|
||||||
|
} catch (ProxyCacheException e) {
|
||||||
|
Log.d(LOG_TAG, "Error registering cache listener", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unregisterCacheListener(CacheListener cacheListener, String url) {
|
||||||
|
checkAllNotNull(cacheListener, url);
|
||||||
|
synchronized (clientsLock) {
|
||||||
|
try {
|
||||||
|
getClients(url).unregisterCacheListener(cacheListener);
|
||||||
|
} catch (ProxyCacheException e) {
|
||||||
|
Log.d(LOG_TAG, "Error registering cache listener", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unregisterCacheListener(CacheListener cacheListener) {
|
||||||
|
checkNotNull(cacheListener);
|
||||||
|
synchronized (clientsLock) {
|
||||||
|
for (HttpProxyCacheServerClients clients : clientsMap.values()) {
|
||||||
|
clients.unregisterCacheListener(cacheListener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shutdown() {
|
||||||
|
Log.i(LOG_TAG, "Shutdown proxy server");
|
||||||
|
|
||||||
|
shutdownClients();
|
||||||
|
|
||||||
|
waitConnectionThread.interrupt();
|
||||||
|
try {
|
||||||
|
if (!serverSocket.isClosed()) {
|
||||||
|
serverSocket.close();
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
onError(new ProxyCacheException("Error shutting down proxy server", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void shutdownClients() {
|
||||||
|
synchronized (clientsLock) {
|
||||||
|
for (HttpProxyCacheServerClients clients : clientsMap.values()) {
|
||||||
|
clients.shutdown();
|
||||||
|
}
|
||||||
|
clientsMap.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void waitForRequest() {
|
||||||
|
try {
|
||||||
|
while (!Thread.currentThread().isInterrupted()) {
|
||||||
|
Socket socket = serverSocket.accept();
|
||||||
|
Log.d(LOG_TAG, "Accept new socket " + socket);
|
||||||
|
socketProcessor.submit(new SocketProcessorRunnable(socket));
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
onError(new ProxyCacheException("Error during waiting connection", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processSocket(Socket socket) {
|
||||||
|
try {
|
||||||
|
GetRequest request = GetRequest.read(socket.getInputStream());
|
||||||
|
Log.i(LOG_TAG, "Request to cache proxy:" + request);
|
||||||
|
String url = ProxyCacheUtils.decode(request.uri);
|
||||||
|
HttpProxyCacheServerClients clients = getClients(url);
|
||||||
|
clients.processRequest(request, socket);
|
||||||
|
} catch (SocketException e) {
|
||||||
|
// There is no way to determine that client closed connection http://stackoverflow.com/a/10241044/999458
|
||||||
|
// So just to prevent log flooding don't log stacktrace
|
||||||
|
Log.d(LOG_TAG, "Client communication problem. It seems client closed connection");
|
||||||
|
} catch (ProxyCacheException | IOException e) {
|
||||||
|
onError(new ProxyCacheException("Error processing request", e));
|
||||||
|
} finally {
|
||||||
|
releaseSocket(socket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpProxyCacheServerClients getClients(String url) throws ProxyCacheException {
|
||||||
|
synchronized (clientsLock) {
|
||||||
|
HttpProxyCacheServerClients clients = clientsMap.get(url);
|
||||||
|
if (clients == null) {
|
||||||
|
clients = new HttpProxyCacheServerClients(url, fileNameGenerator);
|
||||||
|
clientsMap.put(url, clients);
|
||||||
|
}
|
||||||
|
return clients;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void releaseSocket(Socket socket) {
|
||||||
|
closeSocketInput(socket);
|
||||||
|
closeSocketOutput(socket);
|
||||||
|
closeSocket(socket);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void closeSocketInput(Socket socket) {
|
||||||
|
try {
|
||||||
|
if (!socket.isInputShutdown()) {
|
||||||
|
socket.shutdownInput();
|
||||||
|
}
|
||||||
|
} catch (SocketException e) {
|
||||||
|
// There is no way to determine that client closed connection http://stackoverflow.com/a/10241044/999458
|
||||||
|
// So just to prevent log flooding don't log stacktrace
|
||||||
|
Log.d(LOG_TAG, "Error closing client's input stream: it seems client closed connection");
|
||||||
|
} catch (IOException e) {
|
||||||
|
onError(new ProxyCacheException("Error closing socket input stream", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void closeSocketOutput(Socket socket) {
|
||||||
|
try {
|
||||||
|
if (socket.isOutputShutdown()) {
|
||||||
|
socket.shutdownOutput();
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
onError(new ProxyCacheException("Error closing socket output stream", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void closeSocket(Socket socket) {
|
||||||
|
try {
|
||||||
|
if (!socket.isClosed()) {
|
||||||
|
socket.close();
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
onError(new ProxyCacheException("Error closing socket", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onError(Throwable e) {
|
||||||
|
Log.e(LOG_TAG, "HttpProxyCacheServer error", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class WaitRequestsRunnable implements Runnable {
|
||||||
|
|
||||||
|
private final CountDownLatch startSignal;
|
||||||
|
|
||||||
|
public WaitRequestsRunnable(CountDownLatch startSignal) {
|
||||||
|
this.startSignal = startSignal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
startSignal.countDown();
|
||||||
|
waitForRequest();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class SocketProcessorRunnable implements Runnable {
|
||||||
|
|
||||||
|
private final Socket socket;
|
||||||
|
|
||||||
|
public SocketProcessorRunnable(Socket socket) {
|
||||||
|
this.socket = socket;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
processSocket(socket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
package com.danikula.videocache;
|
||||||
|
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.os.Message;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
import static com.danikula.videocache.Preconditions.checkNotNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Client for {@link HttpProxyCacheServer}
|
||||||
|
*
|
||||||
|
* @author Alexey Danilov (danikula@gmail.com).
|
||||||
|
*/
|
||||||
|
final class HttpProxyCacheServerClients {
|
||||||
|
|
||||||
|
private final AtomicInteger clientsCount = new AtomicInteger(0);
|
||||||
|
private final String url;
|
||||||
|
private volatile HttpProxyCache proxyCache;
|
||||||
|
private final List<CacheListener> listeners = new CopyOnWriteArrayList<>();
|
||||||
|
private final FileNameGenerator fileNameGenerator;
|
||||||
|
private final CacheListener uiCacheListener;
|
||||||
|
|
||||||
|
public HttpProxyCacheServerClients(String url, FileNameGenerator fileNameGenerator) {
|
||||||
|
this.url = checkNotNull(url);
|
||||||
|
this.fileNameGenerator = checkNotNull(fileNameGenerator);
|
||||||
|
this.uiCacheListener = new UiListenerHandler(url, listeners);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void processRequest(GetRequest request, Socket socket) throws ProxyCacheException, IOException {
|
||||||
|
proxyCache = proxyCache == null ? newHttpProxyCache() : proxyCache;
|
||||||
|
try {
|
||||||
|
clientsCount.incrementAndGet();
|
||||||
|
proxyCache.processRequest(request, socket);
|
||||||
|
} finally {
|
||||||
|
int count = clientsCount.decrementAndGet();
|
||||||
|
if (count <= 0) {
|
||||||
|
proxyCache.shutdown();
|
||||||
|
proxyCache = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void registerCacheListener(CacheListener cacheListener) {
|
||||||
|
listeners.add(cacheListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unregisterCacheListener(CacheListener cacheListener) {
|
||||||
|
listeners.remove(cacheListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shutdown() {
|
||||||
|
listeners.clear();
|
||||||
|
if (proxyCache != null) {
|
||||||
|
proxyCache.registerCacheListener(null);
|
||||||
|
proxyCache.shutdown();
|
||||||
|
proxyCache = null;
|
||||||
|
}
|
||||||
|
clientsCount.set(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpProxyCache newHttpProxyCache() throws ProxyCacheException {
|
||||||
|
HttpUrlSource source = new HttpUrlSource(url);
|
||||||
|
FileCache cache = new FileCache(fileNameGenerator.generate(url));
|
||||||
|
HttpProxyCache httpProxyCache = new HttpProxyCache(source, cache);
|
||||||
|
httpProxyCache.registerCacheListener(uiCacheListener);
|
||||||
|
return httpProxyCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class UiListenerHandler extends Handler implements CacheListener {
|
||||||
|
|
||||||
|
private final String url;
|
||||||
|
private final List<CacheListener> listeners;
|
||||||
|
|
||||||
|
public UiListenerHandler(String url, List<CacheListener> listeners) {
|
||||||
|
super(Looper.getMainLooper());
|
||||||
|
this.url = url;
|
||||||
|
this.listeners = listeners;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCacheAvailable(File file, String url, int percentsAvailable) {
|
||||||
|
Message message = obtainMessage();
|
||||||
|
message.arg1 = percentsAvailable;
|
||||||
|
message.obj = file;
|
||||||
|
sendMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleMessage(Message msg) {
|
||||||
|
for (CacheListener cacheListener : listeners) {
|
||||||
|
cacheListener.onCacheAvailable((File) msg.obj, url, msg.arg1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,10 +5,11 @@ import android.util.Log;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.io.InterruptedIOException;
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
|
||||||
import static com.danikula.videocache.Preconditions.checkNotNull;
|
import static com.danikula.videocache.ProxyCacheUtils.LOG_TAG;
|
||||||
import static java.net.HttpURLConnection.HTTP_OK;
|
import static java.net.HttpURLConnection.HTTP_OK;
|
||||||
import static java.net.HttpURLConnection.HTTP_PARTIAL;
|
import static java.net.HttpURLConnection.HTTP_PARTIAL;
|
||||||
|
|
||||||
@@ -19,7 +20,7 @@ import static java.net.HttpURLConnection.HTTP_PARTIAL;
|
|||||||
*/
|
*/
|
||||||
public class HttpUrlSource implements Source {
|
public class HttpUrlSource implements Source {
|
||||||
|
|
||||||
final String url;
|
public final String url;
|
||||||
private HttpURLConnection connection;
|
private HttpURLConnection connection;
|
||||||
private InputStream inputStream;
|
private InputStream inputStream;
|
||||||
private volatile int available = Integer.MIN_VALUE;
|
private volatile int available = Integer.MIN_VALUE;
|
||||||
@@ -45,23 +46,23 @@ public class HttpUrlSource implements Source {
|
|||||||
@Override
|
@Override
|
||||||
public void open(int offset) throws ProxyCacheException {
|
public void open(int offset) throws ProxyCacheException {
|
||||||
try {
|
try {
|
||||||
Log.d(ProxyCacheUtils.LOG_TAG, "Open connection " + (offset > 0 ? " with offset " + offset : "") + " to " + url);
|
Log.d(LOG_TAG, "Open connection " + (offset > 0 ? " with offset " + offset : "") + " to " + url);
|
||||||
connection = (HttpURLConnection) new URL(url).openConnection();
|
connection = (HttpURLConnection) new URL(url).openConnection();
|
||||||
if (offset > 0) {
|
if (offset > 0) {
|
||||||
connection.setRequestProperty("Range", "bytes=" + offset + "-");
|
connection.setRequestProperty("Range", "bytes=" + offset + "-");
|
||||||
}
|
}
|
||||||
mime = connection.getContentType();
|
mime = connection.getContentType();
|
||||||
inputStream = connection.getInputStream();
|
inputStream = connection.getInputStream();
|
||||||
readSourceAvailableBytes(connection, offset);
|
available = readSourceAvailableBytes(connection, offset);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new ProxyCacheException("Error opening connection for " + url + " with offset " + offset, e);
|
throw new ProxyCacheException("Error opening connection for " + url + " with offset " + offset, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void readSourceAvailableBytes(HttpURLConnection connection, int offset) throws IOException {
|
private int readSourceAvailableBytes(HttpURLConnection connection, int offset) throws IOException {
|
||||||
int contentLength = connection.getContentLength();
|
int contentLength = connection.getContentLength();
|
||||||
int responseCode = connection.getResponseCode();
|
int responseCode = connection.getResponseCode();
|
||||||
available = responseCode == HTTP_OK ? contentLength :
|
return responseCode == HTTP_OK ? contentLength :
|
||||||
responseCode == HTTP_PARTIAL ? contentLength + offset :
|
responseCode == HTTP_PARTIAL ? contentLength + offset :
|
||||||
available;
|
available;
|
||||||
}
|
}
|
||||||
@@ -80,20 +81,22 @@ public class HttpUrlSource implements Source {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return inputStream.read(buffer, 0, buffer.length);
|
return inputStream.read(buffer, 0, buffer.length);
|
||||||
|
} catch (InterruptedIOException e) {
|
||||||
|
throw new InterruptedProxyCacheException("Reading source " + url + " is interrupted", e);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new ProxyCacheException("Error reading data from " + url, e);
|
throw new ProxyCacheException("Error reading data from " + url, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void fetchContentInfo() throws ProxyCacheException {
|
private void fetchContentInfo() throws ProxyCacheException {
|
||||||
Log.d(ProxyCacheUtils.LOG_TAG, "Read content info from " + url);
|
Log.d(LOG_TAG, "Read content info from " + url);
|
||||||
HttpURLConnection urlConnection = null;
|
HttpURLConnection urlConnection = null;
|
||||||
try {
|
try {
|
||||||
urlConnection = (HttpURLConnection) new URL(url).openConnection();
|
urlConnection = (HttpURLConnection) new URL(url).openConnection();
|
||||||
urlConnection.setRequestMethod("HEAD");
|
urlConnection.setRequestMethod("HEAD");
|
||||||
available = urlConnection.getContentLength();
|
available = urlConnection.getContentLength();
|
||||||
mime = urlConnection.getContentType();
|
mime = urlConnection.getContentType();
|
||||||
Log.d(ProxyCacheUtils.LOG_TAG, "Content-Length of " + url + " is " + available + " bytes, mime is " + mime);
|
Log.i(LOG_TAG, "Info read: " + this);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new ProxyCacheException("Error fetching Content-Length from " + url);
|
throw new ProxyCacheException("Error fetching Content-Length from " + url);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -109,4 +112,13 @@ public class HttpUrlSource implements Source {
|
|||||||
}
|
}
|
||||||
return mime;
|
return mime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "HttpUrlSource{" +
|
||||||
|
"url='" + url + '\'' +
|
||||||
|
", available=" + available +
|
||||||
|
", mime='" + mime + '\'' +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.danikula.videocache;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates interruption error in work of {@link ProxyCache} fired by user.
|
||||||
|
*
|
||||||
|
* @author Alexey Danilov
|
||||||
|
*/
|
||||||
|
public class InterruptedProxyCacheException extends ProxyCacheException {
|
||||||
|
|
||||||
|
public InterruptedProxyCacheException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public InterruptedProxyCacheException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public InterruptedProxyCacheException(Throwable cause) {
|
||||||
|
super(cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package com.danikula.videocache;
|
||||||
|
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
|
||||||
|
import static com.danikula.videocache.Preconditions.checkNotNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of {@link FileNameGenerator} that uses MD5 of url as file name
|
||||||
|
*
|
||||||
|
* @author Alexey Danilov (danikula@gmail.com).
|
||||||
|
*/
|
||||||
|
public class Md5FileNameGenerator implements FileNameGenerator {
|
||||||
|
|
||||||
|
private final File cacheDirectory;
|
||||||
|
|
||||||
|
public Md5FileNameGenerator(File cacheDirectory) {
|
||||||
|
this.cacheDirectory = checkNotNull(cacheDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public File generate(String url) {
|
||||||
|
checkNotNull(url);
|
||||||
|
String extension = getExtension(url);
|
||||||
|
String name = computeMD5(url);
|
||||||
|
name = TextUtils.isEmpty(extension) ? name : name + "." + extension;
|
||||||
|
return new File(cacheDirectory, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getExtension(String url) {
|
||||||
|
int dotIndex = url.lastIndexOf('.');
|
||||||
|
int slashIndex = url.lastIndexOf(File.separator);
|
||||||
|
return dotIndex != -1 && dotIndex > slashIndex ? url.substring(dotIndex + 1, url.length()) : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String computeMD5(String string) {
|
||||||
|
try {
|
||||||
|
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
|
||||||
|
byte[] digestBytes = messageDigest.digest(string.getBytes());
|
||||||
|
return bytesToHexString(digestBytes);
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new IllegalStateException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String bytesToHexString(byte[] bytes) {
|
||||||
|
StringBuffer sb = new StringBuffer();
|
||||||
|
for (byte b : bytes) {
|
||||||
|
sb.append(String.format("%02x", b));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,14 @@ final class Preconditions {
|
|||||||
return reference;
|
return reference;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void checkAllNotNull(Object... references) {
|
||||||
|
for (Object reference : references) {
|
||||||
|
if (reference == null) {
|
||||||
|
throw new NullPointerException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static <T> T checkNotNull(T reference, String errorMessage) {
|
static <T> T checkNotNull(T reference, String errorMessage) {
|
||||||
if (reference == null) {
|
if (reference == null) {
|
||||||
throw new NullPointerException(errorMessage);
|
throw new NullPointerException(errorMessage);
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
package com.danikula.videocache;
|
package com.danikula.videocache;
|
||||||
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.Looper;
|
|
||||||
import android.os.Message;
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
@@ -19,35 +16,22 @@ import static com.danikula.videocache.ProxyCacheUtils.LOG_TAG;
|
|||||||
*
|
*
|
||||||
* @author Alexey Danilov (danikula@gmail.com).
|
* @author Alexey Danilov (danikula@gmail.com).
|
||||||
*/
|
*/
|
||||||
public class ProxyCache {
|
class ProxyCache {
|
||||||
|
|
||||||
private static final int MAX_READ_SOURCE_ATTEMPTS = 1;
|
private static final int MAX_READ_SOURCE_ATTEMPTS = 1;
|
||||||
|
|
||||||
private final Source source;
|
private final Source source;
|
||||||
private final Cache cache;
|
private final Cache cache;
|
||||||
private final Object wc;
|
private final Object wc = new Object();
|
||||||
private final ListenerHandler handler;
|
private final Object stopLock = new Object();
|
||||||
private volatile Thread sourceReaderThread;
|
private volatile Thread sourceReaderThread;
|
||||||
private volatile boolean stopped;
|
private volatile boolean stopped;
|
||||||
private final AtomicInteger readSourceErrorsCount;
|
private final AtomicInteger readSourceErrorsCount;
|
||||||
private CacheListener cacheListener;
|
|
||||||
private final boolean logEnabled;
|
|
||||||
|
|
||||||
public ProxyCache(Source source, Cache cache, boolean logEnabled) {
|
|
||||||
this.source = checkNotNull(source);
|
|
||||||
this.cache = checkNotNull(cache);
|
|
||||||
this.logEnabled = logEnabled;
|
|
||||||
this.wc = new Object();
|
|
||||||
this.handler = new ListenerHandler();
|
|
||||||
this.readSourceErrorsCount = new AtomicInteger();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ProxyCache(Source source, Cache cache) {
|
public ProxyCache(Source source, Cache cache) {
|
||||||
this(source, cache, false);
|
this.source = checkNotNull(source);
|
||||||
}
|
this.cache = checkNotNull(cache);
|
||||||
|
this.readSourceErrorsCount = new AtomicInteger();
|
||||||
public void setCacheListener(CacheListener cacheListener) {
|
|
||||||
this.cacheListener = cacheListener;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public int read(byte[] buffer, long offset, int length) throws ProxyCacheException {
|
public int read(byte[] buffer, long offset, int length) throws ProxyCacheException {
|
||||||
@@ -59,11 +43,7 @@ public class ProxyCache {
|
|||||||
checkIsCacheValid();
|
checkIsCacheValid();
|
||||||
checkReadSourceErrorsCount();
|
checkReadSourceErrorsCount();
|
||||||
}
|
}
|
||||||
int read = cache.read(buffer, offset, length);
|
return 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 {
|
private void checkIsCacheValid() throws ProxyCacheException {
|
||||||
@@ -82,14 +62,17 @@ public class ProxyCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void shutdown() {
|
public void shutdown() {
|
||||||
try {
|
synchronized (stopLock) {
|
||||||
stopped = true;
|
Log.d(LOG_TAG, "Shutdown proxy for " + source);
|
||||||
if (sourceReaderThread != null) {
|
try {
|
||||||
sourceReaderThread.interrupt();
|
stopped = true;
|
||||||
|
if (sourceReaderThread != null) {
|
||||||
|
sourceReaderThread.interrupt();
|
||||||
|
}
|
||||||
|
cache.close();
|
||||||
|
} catch (ProxyCacheException e) {
|
||||||
|
onError(e);
|
||||||
}
|
}
|
||||||
cache.close();
|
|
||||||
} catch (ProxyCacheException e) {
|
|
||||||
onError(e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,13 +95,16 @@ public class ProxyCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void notifyNewCacheDataAvailable(int cachePercentage) {
|
private void notifyNewCacheDataAvailable(int cachePercentage) {
|
||||||
handler.deliverCachePercentage(cachePercentage);
|
onCacheAvailable(cachePercentage);
|
||||||
|
|
||||||
synchronized (wc) {
|
synchronized (wc) {
|
||||||
wc.notifyAll();
|
wc.notifyAll();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void onCacheAvailable(int percents){
|
||||||
|
}
|
||||||
|
|
||||||
private void readSource() {
|
private void readSource() {
|
||||||
int cachePercentage = 0;
|
int cachePercentage = 0;
|
||||||
try {
|
try {
|
||||||
@@ -126,19 +112,19 @@ public class ProxyCache {
|
|||||||
source.open(offset);
|
source.open(offset);
|
||||||
byte[] buffer = new byte[ProxyCacheUtils.DEFAULT_BUFFER_SIZE];
|
byte[] buffer = new byte[ProxyCacheUtils.DEFAULT_BUFFER_SIZE];
|
||||||
int readBytes;
|
int readBytes;
|
||||||
while ((readBytes = source.read(buffer)) != -1 && !Thread.currentThread().isInterrupted() && !stopped) {
|
while ((readBytes = source.read(buffer)) != -1) {
|
||||||
if (isLogEnabled()) {
|
synchronized (stopLock) {
|
||||||
Log.d(LOG_TAG, "Write data[" + readBytes + " bytes] to cache from source with offset " + offset + ": " + ProxyCacheUtils.preview(buffer, readBytes));
|
if (isStopped()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cache.append(buffer, readBytes);
|
||||||
}
|
}
|
||||||
cache.append(buffer, readBytes);
|
|
||||||
offset += readBytes;
|
offset += readBytes;
|
||||||
cachePercentage = offset * 100 / source.available();
|
cachePercentage = offset * 100 / source.available();
|
||||||
|
|
||||||
notifyNewCacheDataAvailable(cachePercentage);
|
notifyNewCacheDataAvailable(cachePercentage);
|
||||||
}
|
}
|
||||||
if (cache.available() == source.available()) {
|
tryComplete();
|
||||||
cache.complete();
|
|
||||||
}
|
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
readSourceErrorsCount.incrementAndGet();
|
readSourceErrorsCount.incrementAndGet();
|
||||||
onError(e);
|
onError(e);
|
||||||
@@ -148,6 +134,18 @@ public class ProxyCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void tryComplete() throws ProxyCacheException {
|
||||||
|
synchronized (stopLock) {
|
||||||
|
if (!isStopped() && cache.available() == source.available()) {
|
||||||
|
cache.complete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isStopped() {
|
||||||
|
return Thread.currentThread().isInterrupted() || stopped;
|
||||||
|
}
|
||||||
|
|
||||||
private void closeSource() {
|
private void closeSource() {
|
||||||
try {
|
try {
|
||||||
source.close();
|
source.close();
|
||||||
@@ -157,12 +155,12 @@ public class ProxyCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected final void onError(final Throwable e) {
|
protected final void onError(final Throwable e) {
|
||||||
Log.e(LOG_TAG, "ProxyCache error", e);
|
boolean interruption = e instanceof InterruptedProxyCacheException;
|
||||||
handler.deliverError(e);
|
if (interruption) {
|
||||||
}
|
Log.d(LOG_TAG, "ProxyCache is interrupted");
|
||||||
|
} else {
|
||||||
protected boolean isLogEnabled() {
|
Log.e(LOG_TAG, "ProxyCache error", e);
|
||||||
return logEnabled;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class SourceReaderRunnable implements Runnable {
|
private class SourceReaderRunnable implements Runnable {
|
||||||
@@ -172,55 +170,4 @@ public class ProxyCache {
|
|||||||
readSource();
|
readSource();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class ListenerHandler extends Handler {
|
|
||||||
|
|
||||||
private static final int MSG_ERROR = 1;
|
|
||||||
private static final int MSG_CACHE_PERCENTAGE = 2;
|
|
||||||
|
|
||||||
public ListenerHandler() {
|
|
||||||
super(Looper.getMainLooper());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void deliverCachePercentage(int percents) {
|
|
||||||
if (cacheListener != null) {
|
|
||||||
send(MSG_CACHE_PERCENTAGE, percents, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void deliverError(Throwable error) {
|
|
||||||
if (isFatalError(error) || cacheListener != null) {
|
|
||||||
send(MSG_ERROR, 0, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isFatalError(Throwable error) {
|
|
||||||
return !(error instanceof ProxyCacheException);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void send(int what, int arg1, Object data) {
|
|
||||||
Message message = obtainMessage(what);
|
|
||||||
message.arg1 = arg1;
|
|
||||||
message.obj = data;
|
|
||||||
sendMessage(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleMessage(Message msg) {
|
|
||||||
switch (msg.what) {
|
|
||||||
case MSG_CACHE_PERCENTAGE:
|
|
||||||
cacheListener.onCacheDataAvailable(msg.arg1);
|
|
||||||
break;
|
|
||||||
case MSG_ERROR:
|
|
||||||
Throwable error = (Throwable) msg.obj;
|
|
||||||
if (isFatalError(error)) {
|
|
||||||
throw new RuntimeException("Unexpected error!", error);
|
|
||||||
}
|
|
||||||
cacheListener.onError((ProxyCacheException) error);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new RuntimeException("Unknown message " + msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import android.webkit.MimeTypeMap;
|
|||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.net.URLDecoder;
|
||||||
|
import java.net.URLEncoder;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
import static com.danikula.videocache.Preconditions.checkArgument;
|
import static com.danikula.videocache.Preconditions.checkArgument;
|
||||||
@@ -56,5 +59,19 @@ class ProxyCacheUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static String encode(String url) {
|
||||||
|
try {
|
||||||
|
return URLEncoder.encode(url, "utf-8");
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
throw new RuntimeException("Error encoding url", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static String decode(String url) {
|
||||||
|
try {
|
||||||
|
return URLDecoder.decode(url, "utf-8");
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
throw new RuntimeException("Error decoding url", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ buildscript {
|
|||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
maven { url 'https://github.com/danikula/AndroidVideoCache/raw/mvn-repo' }
|
maven { url 'https://dl.bintray.com/alexeydanilov/maven' }
|
||||||
maven { url 'https://github.com/dahlgren/vpi-aar/raw/master' }
|
maven { url 'https://github.com/dahlgren/vpi-aar/raw/master' }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,9 +36,10 @@ apt {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
// compile project(':library')
|
||||||
compile 'com.android.support:support-v4:23.0.0'
|
compile 'com.android.support:support-v4:23.0.0'
|
||||||
compile 'org.androidannotations:androidannotations-api:3.3.2'
|
compile 'org.androidannotations:androidannotations-api:3.3.2'
|
||||||
compile 'com.danikula:videocache:1.0'
|
compile 'com.danikula:videocache:2.0.7'
|
||||||
compile 'com.viewpagerindicator:library:2.4.2-SNAPSHOT@aar'
|
compile 'com.viewpagerindicator:library:2.4.2-SNAPSHOT@aar'
|
||||||
apt 'org.androidannotations:androidannotations:3.3.2'
|
apt 'org.androidannotations:androidannotations:3.3.2'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
|
android:name=".App"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
|
|||||||
26
sample/src/main/java/com/danikula/videocache/sample/App.java
Normal file
26
sample/src/main/java/com/danikula/videocache/sample/App.java
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package com.danikula.videocache.sample;
|
||||||
|
|
||||||
|
import android.app.Application;
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import com.danikula.videocache.FileNameGenerator;
|
||||||
|
import com.danikula.videocache.HttpProxyCacheServer;
|
||||||
|
import com.danikula.videocache.Md5FileNameGenerator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Alexey Danilov (danikula@gmail.com).
|
||||||
|
*/
|
||||||
|
public class App extends Application {
|
||||||
|
|
||||||
|
private HttpProxyCacheServer proxy;
|
||||||
|
|
||||||
|
public static HttpProxyCacheServer getProxy(Context context) {
|
||||||
|
App app = (App) context.getApplicationContext();
|
||||||
|
return app.proxy == null ? (app.proxy = app.newProxy()) : app.proxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpProxyCacheServer newProxy() {
|
||||||
|
FileNameGenerator nameGenerator = new Md5FileNameGenerator(getExternalCacheDir());
|
||||||
|
return new HttpProxyCacheServer(nameGenerator);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,16 +4,11 @@ import android.content.Context;
|
|||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Message;
|
import android.os.Message;
|
||||||
import android.support.v4.app.Fragment;
|
import android.support.v4.app.Fragment;
|
||||||
import android.util.Log;
|
|
||||||
import android.widget.ProgressBar;
|
import android.widget.ProgressBar;
|
||||||
import android.widget.VideoView;
|
import android.widget.VideoView;
|
||||||
|
|
||||||
import com.danikula.videocache.Cache;
|
|
||||||
import com.danikula.videocache.CacheListener;
|
import com.danikula.videocache.CacheListener;
|
||||||
import com.danikula.videocache.FileCache;
|
import com.danikula.videocache.HttpProxyCacheServer;
|
||||||
import com.danikula.videocache.HttpProxyCache;
|
|
||||||
import com.danikula.videocache.HttpUrlSource;
|
|
||||||
import com.danikula.videocache.ProxyCacheException;
|
|
||||||
|
|
||||||
import org.androidannotations.annotations.AfterViews;
|
import org.androidannotations.annotations.AfterViews;
|
||||||
import org.androidannotations.annotations.EFragment;
|
import org.androidannotations.annotations.EFragment;
|
||||||
@@ -40,7 +35,6 @@ public class GalleryVideoFragment extends Fragment implements CacheListener {
|
|||||||
|
|
||||||
private boolean visibleForUser;
|
private boolean visibleForUser;
|
||||||
|
|
||||||
private HttpProxyCache proxyCache;
|
|
||||||
private final VideoProgressUpdater updater = new VideoProgressUpdater();
|
private final VideoProgressUpdater updater = new VideoProgressUpdater();
|
||||||
|
|
||||||
public static Fragment build(Context context, Video video) {
|
public static Fragment build(Context context, Video video) {
|
||||||
@@ -70,18 +64,11 @@ public class GalleryVideoFragment extends Fragment implements CacheListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void startProxy() {
|
private void startProxy() {
|
||||||
try {
|
HttpProxyCacheServer proxy = App.getProxy(getActivity());
|
||||||
Cache cache = new FileCache(new File(cachePath));
|
proxy.registerCacheListener(this, url);
|
||||||
HttpUrlSource source = new HttpUrlSource(url);
|
videoView.setVideoPath(proxy.getProxyUrl(url));
|
||||||
proxyCache = new HttpProxyCache(source, cache);
|
|
||||||
proxyCache.setCacheListener(this);
|
|
||||||
videoView.setVideoPath(proxyCache.getUrl());
|
|
||||||
} catch (ProxyCacheException e) {
|
|
||||||
// do nothing. onError() handles all errors
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// just for videos gallery
|
|
||||||
@Override
|
@Override
|
||||||
public void setUserVisibleHint(boolean isVisibleToUser) {
|
public void setUserVisibleHint(boolean isVisibleToUser) {
|
||||||
super.setUserVisibleHint(isVisibleToUser);
|
super.setUserVisibleHint(isVisibleToUser);
|
||||||
@@ -113,19 +100,12 @@ public class GalleryVideoFragment extends Fragment implements CacheListener {
|
|||||||
public void onDestroy() {
|
public void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
|
|
||||||
if (proxyCache != null) {
|
App.getProxy(getActivity()).unregisterCacheListener(this);
|
||||||
proxyCache.shutdown();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onError(ProxyCacheException e) {
|
public void onCacheAvailable(File file, String url, int percentsAvailable) {
|
||||||
Log.e(LOG_TAG, "Error playing video", e);
|
progressBar.setSecondaryProgress(percentsAvailable);
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCacheDataAvailable(int cachePercentage) {
|
|
||||||
progressBar.setSecondaryProgress(cachePercentage);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateVideoProgress() {
|
private void updateVideoProgress() {
|
||||||
@@ -152,7 +132,7 @@ public class GalleryVideoFragment extends Fragment implements CacheListener {
|
|||||||
@Override
|
@Override
|
||||||
public void handleMessage(Message msg) {
|
public void handleMessage(Message msg) {
|
||||||
updateVideoProgress();
|
updateVideoProgress();
|
||||||
sendEmptyMessageDelayed(0, 200);
|
sendEmptyMessageDelayed(0, 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,16 +4,11 @@ import android.content.Context;
|
|||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Message;
|
import android.os.Message;
|
||||||
import android.support.v4.app.Fragment;
|
import android.support.v4.app.Fragment;
|
||||||
import android.util.Log;
|
|
||||||
import android.widget.ProgressBar;
|
import android.widget.ProgressBar;
|
||||||
import android.widget.VideoView;
|
import android.widget.VideoView;
|
||||||
|
|
||||||
import com.danikula.videocache.Cache;
|
|
||||||
import com.danikula.videocache.CacheListener;
|
import com.danikula.videocache.CacheListener;
|
||||||
import com.danikula.videocache.FileCache;
|
import com.danikula.videocache.HttpProxyCacheServer;
|
||||||
import com.danikula.videocache.HttpProxyCache;
|
|
||||||
import com.danikula.videocache.HttpUrlSource;
|
|
||||||
import com.danikula.videocache.ProxyCacheException;
|
|
||||||
|
|
||||||
import org.androidannotations.annotations.AfterViews;
|
import org.androidannotations.annotations.AfterViews;
|
||||||
import org.androidannotations.annotations.EFragment;
|
import org.androidannotations.annotations.EFragment;
|
||||||
@@ -34,7 +29,6 @@ public class VideoFragment extends Fragment implements CacheListener {
|
|||||||
@ViewById VideoView videoView;
|
@ViewById VideoView videoView;
|
||||||
@ViewById ProgressBar progressBar;
|
@ViewById ProgressBar progressBar;
|
||||||
|
|
||||||
private HttpProxyCache proxyCache;
|
|
||||||
private final VideoProgressUpdater updater = new VideoProgressUpdater();
|
private final VideoProgressUpdater updater = new VideoProgressUpdater();
|
||||||
|
|
||||||
public static Fragment build(Context context, Video video) {
|
public static Fragment build(Context context, Video video) {
|
||||||
@@ -54,16 +48,10 @@ public class VideoFragment extends Fragment implements CacheListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void startVideo() {
|
private void startVideo() {
|
||||||
try {
|
HttpProxyCacheServer proxy = App.getProxy(getActivity());
|
||||||
Cache cache = new FileCache(new File(cachePath));
|
proxy.registerCacheListener(this, url);
|
||||||
HttpUrlSource source = new HttpUrlSource(url);
|
videoView.setVideoPath(proxy.getProxyUrl(url));
|
||||||
proxyCache = new HttpProxyCache(source, cache);
|
videoView.start();
|
||||||
proxyCache.setCacheListener(this);
|
|
||||||
videoView.setVideoPath(proxyCache.getUrl());
|
|
||||||
videoView.start();
|
|
||||||
} catch (ProxyCacheException e) {
|
|
||||||
// do nothing. onError() handles all errors
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -82,19 +70,12 @@ public class VideoFragment extends Fragment implements CacheListener {
|
|||||||
public void onDestroy() {
|
public void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
|
|
||||||
if (proxyCache != null) {
|
App.getProxy(getActivity()).unregisterCacheListener(this);
|
||||||
proxyCache.shutdown();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onError(ProxyCacheException e) {
|
public void onCacheAvailable(File file, String url, int percentsAvailable) {
|
||||||
Log.e(LOG_TAG, "Error playing video", e);
|
progressBar.setSecondaryProgress(percentsAvailable);
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCacheDataAvailable(int cachePercentage) {
|
|
||||||
progressBar.setSecondaryProgress(cachePercentage);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateVideoProgress() {
|
private void updateVideoProgress() {
|
||||||
@@ -121,7 +102,7 @@ public class VideoFragment extends Fragment implements CacheListener {
|
|||||||
@Override
|
@Override
|
||||||
public void handleMessage(Message msg) {
|
public void handleMessage(Message msg) {
|
||||||
updateVideoProgress();
|
updateVideoProgress();
|
||||||
sendEmptyMessageDelayed(0, 200);
|
sendEmptyMessageDelayed(0, 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,4 +42,5 @@ dependencies {
|
|||||||
testCompile('com.danikula:android-garden:2.1.4') {
|
testCompile('com.danikula:android-garden:2.1.4') {
|
||||||
exclude group: 'com.google.android'
|
exclude group: 'com.google.android'
|
||||||
}
|
}
|
||||||
|
testCompile 'org.mockito:mockito-all:1.9.5'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package com.danikula.videocache;
|
||||||
|
|
||||||
|
import com.danikula.videocache.test.BuildConfig;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.robolectric.RobolectricGradleTestRunner;
|
||||||
|
import org.robolectric.annotation.Config;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
import static org.fest.assertions.api.Assertions.assertThat;
|
||||||
|
import static org.fest.assertions.api.Assertions.fail;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Alexey Danilov (danikula@gmail.com).
|
||||||
|
*/
|
||||||
|
@RunWith(RobolectricGradleTestRunner.class)
|
||||||
|
@Config(constants = BuildConfig.class, emulateSdk = BuildConfig.MIN_SDK_VERSION)
|
||||||
|
public class GetRequestTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPartialHttpGet() throws Exception {
|
||||||
|
GetRequest getRequest = new GetRequest("" +
|
||||||
|
"GET /uri HTTP/1.1\n" +
|
||||||
|
"Host: 127.0.0.1:44684\n" +
|
||||||
|
"Range: bytes=9860723-" +
|
||||||
|
"Accept-Encoding: gzip");
|
||||||
|
assertThat(getRequest.rangeOffset).isEqualTo(9860723);
|
||||||
|
assertThat(getRequest.uri).isEqualTo("uri");
|
||||||
|
assertThat(getRequest.partial).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNotPartialHttpGet() throws Exception {
|
||||||
|
GetRequest getRequest = new GetRequest("" +
|
||||||
|
"GET /uri HTTP/1.1\n" +
|
||||||
|
"Host: 127.0.0.1:44684\n" +
|
||||||
|
"Accept-Encoding: gzip");
|
||||||
|
assertThat(getRequest.rangeOffset).isEqualTo(0);
|
||||||
|
assertThat(getRequest.uri).isEqualTo("uri");
|
||||||
|
assertThat(getRequest.partial).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testReadStream() throws Exception {
|
||||||
|
String requestString = "GET /uri HTTP/1.1\nRange: bytes=9860723-\n";
|
||||||
|
InputStream stream = new ByteArrayInputStream(requestString.getBytes());
|
||||||
|
GetRequest getRequest = GetRequest.read(stream);
|
||||||
|
assertThat(getRequest.rangeOffset).isEqualTo(9860723);
|
||||||
|
assertThat(getRequest.uri).isEqualTo("uri");
|
||||||
|
assertThat(getRequest.partial).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMinimal() throws Exception {
|
||||||
|
GetRequest getRequest = new GetRequest("GET /uri HTTP/1.1");
|
||||||
|
assertThat(getRequest.rangeOffset).isEqualTo(0);
|
||||||
|
assertThat(getRequest.uri).isEqualTo("uri");
|
||||||
|
assertThat(getRequest.partial).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalArgumentException.class)
|
||||||
|
public void testEmpty() throws Exception {
|
||||||
|
GetRequest getRequest = new GetRequest("");
|
||||||
|
fail("Empty request");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalArgumentException.class)
|
||||||
|
public void testInvalid() throws Exception {
|
||||||
|
GetRequest getRequest = new GetRequest("/uri HTTP/1.1\n");
|
||||||
|
fail("Invalid request");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
package com.danikula.videocache;
|
||||||
|
|
||||||
|
import android.util.Pair;
|
||||||
|
|
||||||
|
import com.danikula.android.garden.io.IoUtils;
|
||||||
|
import com.danikula.videocache.support.ProxyCacheTestUtils;
|
||||||
|
import com.danikula.videocache.support.Response;
|
||||||
|
import com.danikula.videocache.test.BuildConfig;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.robolectric.RobolectricGradleTestRunner;
|
||||||
|
import org.robolectric.RuntimeEnvironment;
|
||||||
|
import org.robolectric.annotation.Config;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import static com.danikula.videocache.support.ProxyCacheTestUtils.ASSETS_DATA_BIG_NAME;
|
||||||
|
import static com.danikula.videocache.support.ProxyCacheTestUtils.ASSETS_DATA_NAME;
|
||||||
|
import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_BIG_SIZE;
|
||||||
|
import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_BIG_URL;
|
||||||
|
import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_URL;
|
||||||
|
import static com.danikula.videocache.support.ProxyCacheTestUtils.getFileContent;
|
||||||
|
import static com.danikula.videocache.support.ProxyCacheTestUtils.loadAssetFile;
|
||||||
|
import static com.danikula.videocache.support.ProxyCacheTestUtils.readProxyResponse;
|
||||||
|
import static org.fest.assertions.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Alexey Danilov (danikula@gmail.com).
|
||||||
|
*/
|
||||||
|
@RunWith(RobolectricGradleTestRunner.class)
|
||||||
|
@Config(constants = BuildConfig.class, emulateSdk = BuildConfig.MIN_SDK_VERSION)
|
||||||
|
public class HttpProxyCacheServerTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testHttpProxyCache() throws Exception {
|
||||||
|
Pair<File, Response> response = readProxyData(HTTP_DATA_URL);
|
||||||
|
|
||||||
|
assertThat(response.second.code).isEqualTo(200);
|
||||||
|
assertThat(response.second.data).isEqualTo(getFileContent(response.first));
|
||||||
|
assertThat(response.second.data).isEqualTo(loadAssetFile(ASSETS_DATA_NAME));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testProxyContentWithPartialCache() throws Exception {
|
||||||
|
FileNameGenerator fileNameGenerator = new Md5FileNameGenerator(RuntimeEnvironment.application.getExternalCacheDir());
|
||||||
|
File file = fileNameGenerator.generate(HTTP_DATA_URL);
|
||||||
|
int partialCacheSize = 1000;
|
||||||
|
byte[] partialData = ProxyCacheTestUtils.generate(partialCacheSize);
|
||||||
|
File partialCacheFile = ProxyCacheTestUtils.getTempFile(file);
|
||||||
|
IoUtils.saveToFile(partialData, partialCacheFile);
|
||||||
|
|
||||||
|
HttpProxyCacheServer proxy = new HttpProxyCacheServer(fileNameGenerator);
|
||||||
|
Response response = readProxyResponse(proxy, HTTP_DATA_URL);
|
||||||
|
proxy.shutdown();
|
||||||
|
|
||||||
|
byte[] expected = loadAssetFile(ASSETS_DATA_NAME);
|
||||||
|
System.arraycopy(partialData, 0, expected, 0, partialCacheSize);
|
||||||
|
assertThat(response.data).isEqualTo(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMimeFromResponse() throws Exception {
|
||||||
|
Pair<File, Response> response = readProxyData("https://dl.dropboxusercontent.com/u/15506779/persistent/proxycache/android");
|
||||||
|
assertThat(response.second.contentType).isEqualTo("application/octet-stream");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testProxyFullResponse() throws Exception {
|
||||||
|
Pair<File, Response> response = readProxyData(HTTP_DATA_BIG_URL);
|
||||||
|
|
||||||
|
assertThat(response.second.code).isEqualTo(200);
|
||||||
|
assertThat(response.second.contentLength).isEqualTo(HTTP_DATA_BIG_SIZE);
|
||||||
|
assertThat(response.second.contentType).isEqualTo("image/jpeg");
|
||||||
|
assertThat(response.second.headers.containsKey("Accept-Ranges")).isTrue();
|
||||||
|
assertThat(response.second.headers.get("Accept-Ranges").get(0)).isEqualTo("bytes");
|
||||||
|
assertThat(response.second.headers.containsKey("Content-Range")).isFalse();
|
||||||
|
assertThat(response.second.data).isEqualTo(getFileContent(response.first));
|
||||||
|
assertThat(response.second.data).isEqualTo(loadAssetFile(ASSETS_DATA_BIG_NAME));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testProxyPartialResponse() throws Exception {
|
||||||
|
int offset = 42000;
|
||||||
|
Pair<File, Response> response = readProxyData(HTTP_DATA_BIG_URL, offset);
|
||||||
|
|
||||||
|
assertThat(response.second.code).isEqualTo(206);
|
||||||
|
assertThat(response.second.contentLength).isEqualTo(HTTP_DATA_BIG_SIZE - offset);
|
||||||
|
assertThat(response.second.contentType).isEqualTo("image/jpeg");
|
||||||
|
assertThat(response.second.headers.containsKey("Accept-Ranges")).isTrue();
|
||||||
|
assertThat(response.second.headers.get("Accept-Ranges").get(0)).isEqualTo("bytes");
|
||||||
|
assertThat(response.second.headers.containsKey("Content-Range")).isTrue();
|
||||||
|
String rangeHeader = String.format("bytes %d-%d/%d", offset, HTTP_DATA_BIG_SIZE, HTTP_DATA_BIG_SIZE);
|
||||||
|
assertThat(response.second.headers.get("Content-Range").get(0)).isEqualTo(rangeHeader);
|
||||||
|
byte[] expectedData = Arrays.copyOfRange(loadAssetFile(ASSETS_DATA_BIG_NAME), offset, HTTP_DATA_BIG_SIZE);
|
||||||
|
assertThat(response.second.data).isEqualTo(expectedData);
|
||||||
|
assertThat(getFileContent(response.first)).isEqualTo(loadAssetFile(ASSETS_DATA_BIG_NAME));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Pair<File, Response> readProxyData(String url, int offset) throws IOException {
|
||||||
|
File externalCacheDir = RuntimeEnvironment.application.getExternalCacheDir();
|
||||||
|
FileNameGenerator fileNameGenerator = new Md5FileNameGenerator(externalCacheDir);
|
||||||
|
File file = fileNameGenerator.generate(url);
|
||||||
|
HttpProxyCacheServer proxy = new HttpProxyCacheServer(fileNameGenerator);
|
||||||
|
|
||||||
|
Response response = readProxyResponse(proxy, url, offset);
|
||||||
|
proxy.shutdown();
|
||||||
|
|
||||||
|
return new Pair<>(file, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Pair<File, Response> readProxyData(String url) throws IOException {
|
||||||
|
return readProxyData(url, -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
package com.danikula.videocache;
|
|
||||||
|
|
||||||
import com.danikula.android.garden.io.IoUtils;
|
|
||||||
import com.danikula.videocache.support.AngryHttpUrlSource;
|
|
||||||
import com.danikula.videocache.support.Response;
|
|
||||||
import com.danikula.videocache.test.BuildConfig;
|
|
||||||
|
|
||||||
import org.junit.Test;
|
|
||||||
import org.junit.runner.RunWith;
|
|
||||||
import org.robolectric.RobolectricGradleTestRunner;
|
|
||||||
import org.robolectric.annotation.Config;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
import static com.danikula.videocache.support.ProxyCacheTestUtils.ASSETS_DATA_BIG_NAME;
|
|
||||||
import static com.danikula.videocache.support.ProxyCacheTestUtils.ASSETS_DATA_NAME;
|
|
||||||
import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_BIG_SIZE;
|
|
||||||
import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_BIG_URL;
|
|
||||||
import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_URL;
|
|
||||||
import static com.danikula.videocache.support.ProxyCacheTestUtils.generate;
|
|
||||||
import static com.danikula.videocache.support.ProxyCacheTestUtils.getFileContent;
|
|
||||||
import static com.danikula.videocache.support.ProxyCacheTestUtils.loadAssetFile;
|
|
||||||
import static com.danikula.videocache.support.ProxyCacheTestUtils.newCacheFile;
|
|
||||||
import static com.danikula.videocache.support.ProxyCacheTestUtils.readProxyResponse;
|
|
||||||
import static org.fest.assertions.api.Assertions.assertThat;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Alexey Danilov (danikula@gmail.com).
|
|
||||||
*/
|
|
||||||
@RunWith(RobolectricGradleTestRunner.class)
|
|
||||||
@Config(constants = BuildConfig.class, emulateSdk = BuildConfig.MIN_SDK_VERSION)
|
|
||||||
public class HttpProxyCacheTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testHttpProxyCache() throws Exception {
|
|
||||||
HttpUrlSource source = new HttpUrlSource(HTTP_DATA_URL);
|
|
||||||
File file = newCacheFile();
|
|
||||||
HttpProxyCache proxy = new HttpProxyCache(source, new FileCache(file));
|
|
||||||
Response response = readProxyResponse(proxy);
|
|
||||||
assertThat(response.code).isEqualTo(200);
|
|
||||||
assertThat(response.data).isEqualTo(getFileContent(file));
|
|
||||||
assertThat(response.data).isEqualTo(loadAssetFile(ASSETS_DATA_NAME));
|
|
||||||
proxy.shutdown();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testProxyContentWithPartialCache() throws Exception {
|
|
||||||
HttpUrlSource source = new HttpUrlSource(HTTP_DATA_URL);
|
|
||||||
int cacheSize = 1000;
|
|
||||||
HttpProxyCache proxy = new HttpProxyCache(source, new ByteArrayCache(new byte[cacheSize]));
|
|
||||||
|
|
||||||
Response proxyResponse = readProxyResponse(proxy);
|
|
||||||
byte[] expected = loadAssetFile(ASSETS_DATA_NAME);
|
|
||||||
Arrays.fill(expected, 0, cacheSize, (byte) 0);
|
|
||||||
assertThat(proxyResponse.data).isEqualTo(expected);
|
|
||||||
proxy.shutdown();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testMimeFromResponse() throws Exception {
|
|
||||||
HttpUrlSource source = new HttpUrlSource("https://dl.dropboxusercontent.com/u/15506779/persistent/proxycache/android");
|
|
||||||
HttpProxyCache proxy = new HttpProxyCache(source, new ByteArrayCache(new byte[0]));
|
|
||||||
proxy.read(new byte[1], 0, 1);
|
|
||||||
assertThat(source.getMime()).isEqualTo("application/octet-stream");
|
|
||||||
proxy.shutdown();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testProxyFullResponse() throws Exception {
|
|
||||||
File file = newCacheFile();
|
|
||||||
HttpProxyCache proxy = new HttpProxyCache(new HttpUrlSource(HTTP_DATA_BIG_URL), new FileCache(file));
|
|
||||||
Response response = readProxyResponse(proxy);
|
|
||||||
|
|
||||||
assertThat(response.code).isEqualTo(200);
|
|
||||||
assertThat(response.contentLength).isEqualTo(HTTP_DATA_BIG_SIZE);
|
|
||||||
assertThat(response.contentType).isEqualTo("image/jpeg");
|
|
||||||
assertThat(response.headers.containsKey("Accept-Ranges")).isTrue();
|
|
||||||
assertThat(response.headers.get("Accept-Ranges").get(0)).isEqualTo("bytes");
|
|
||||||
assertThat(response.headers.containsKey("Content-Range")).isFalse();
|
|
||||||
assertThat(response.data).isEqualTo(getFileContent(file));
|
|
||||||
assertThat(response.data).isEqualTo(loadAssetFile(ASSETS_DATA_BIG_NAME));
|
|
||||||
proxy.shutdown();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testProxyPartialResponse() throws Exception {
|
|
||||||
int offset = 42000;
|
|
||||||
File file = newCacheFile();
|
|
||||||
HttpProxyCache proxy = new HttpProxyCache(new HttpUrlSource(HTTP_DATA_BIG_URL), new FileCache(file));
|
|
||||||
Response response = readProxyResponse(proxy, offset);
|
|
||||||
|
|
||||||
assertThat(response.code).isEqualTo(206);
|
|
||||||
assertThat(response.contentLength).isEqualTo(HTTP_DATA_BIG_SIZE - offset);
|
|
||||||
assertThat(response.contentType).isEqualTo("image/jpeg");
|
|
||||||
assertThat(response.headers.containsKey("Accept-Ranges")).isTrue();
|
|
||||||
assertThat(response.headers.get("Accept-Ranges").get(0)).isEqualTo("bytes");
|
|
||||||
assertThat(response.headers.containsKey("Content-Range")).isTrue();
|
|
||||||
String rangeHeader = String.format("bytes %d-%d/%d", offset, HTTP_DATA_BIG_SIZE, HTTP_DATA_BIG_SIZE);
|
|
||||||
assertThat(response.headers.get("Content-Range").get(0)).isEqualTo(rangeHeader);
|
|
||||||
byte[] expectedData = Arrays.copyOfRange(loadAssetFile(ASSETS_DATA_BIG_NAME), offset, HTTP_DATA_BIG_SIZE);
|
|
||||||
assertThat(response.data).isEqualTo(expectedData);
|
|
||||||
assertThat(getFileContent(file)).isEqualTo(loadAssetFile(ASSETS_DATA_BIG_NAME));
|
|
||||||
proxy.shutdown();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testAppendCache() throws Exception {
|
|
||||||
byte[] cachedPortion = generate(1200);
|
|
||||||
File file = newCacheFile();
|
|
||||||
File partialFile = new File(file.getParentFile(), file.getName() + ".download");
|
|
||||||
IoUtils.saveToFile(cachedPortion, partialFile);
|
|
||||||
Cache cache = new FileCache(partialFile);
|
|
||||||
assertThat(cache.isCompleted()).isFalse();
|
|
||||||
|
|
||||||
HttpProxyCache proxy = new HttpProxyCache(new HttpUrlSource(HTTP_DATA_BIG_URL), cache);
|
|
||||||
readProxyResponse(proxy);
|
|
||||||
proxy.shutdown();
|
|
||||||
|
|
||||||
assertThat(cache.isCompleted()).isTrue();
|
|
||||||
|
|
||||||
byte[] expectedData = loadAssetFile(ASSETS_DATA_BIG_NAME);
|
|
||||||
System.arraycopy(cachedPortion, 0, expectedData, 0, cachedPortion.length);
|
|
||||||
assertThat(file.length()).isEqualTo(HTTP_DATA_BIG_SIZE);
|
|
||||||
assertThat(expectedData).isEqualTo(getFileContent(file));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testNoTouchSource() throws Exception {
|
|
||||||
File file = newCacheFile();
|
|
||||||
IoUtils.saveToFile(loadAssetFile(ASSETS_DATA_BIG_NAME), file);
|
|
||||||
FileCache cache = new FileCache(file);
|
|
||||||
HttpProxyCache proxy = new HttpProxyCache(new HttpUrlSource(HTTP_DATA_BIG_URL), cache);
|
|
||||||
Response response = readProxyResponse(proxy);
|
|
||||||
proxy.shutdown();
|
|
||||||
assertThat(response.code).isEqualTo(200);
|
|
||||||
|
|
||||||
proxy = new HttpProxyCache(new AngryHttpUrlSource(HTTP_DATA_BIG_URL, "image/jpeg"), new FileCache(file));
|
|
||||||
readProxyResponse(proxy);
|
|
||||||
assertThat(response.code).isEqualTo(200);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.danikula.videocache;
|
package com.danikula.videocache;
|
||||||
|
|
||||||
|
import com.danikula.android.garden.io.IoUtils;
|
||||||
|
import com.danikula.videocache.support.AngryHttpUrlSource;
|
||||||
import com.danikula.videocache.support.PhlegmaticByteArraySource;
|
import com.danikula.videocache.support.PhlegmaticByteArraySource;
|
||||||
import com.danikula.videocache.test.BuildConfig;
|
import com.danikula.videocache.test.BuildConfig;
|
||||||
|
|
||||||
@@ -178,4 +180,18 @@ public class ProxyCacheTest {
|
|||||||
proxyCache.read(new byte[5], 19999, 5);
|
proxyCache.read(new byte[5], 19999, 5);
|
||||||
assertThat(cache.isCompleted()).isTrue();
|
assertThat(cache.isCompleted()).isTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNoTouchSource() throws Exception {
|
||||||
|
int dataSize = 2000;
|
||||||
|
byte[] data = generate(dataSize);
|
||||||
|
File file = newCacheFile();
|
||||||
|
IoUtils.saveToFile(data, file);
|
||||||
|
ProxyCache proxyCache = new ProxyCache(new AngryHttpUrlSource(), new FileCache(file));
|
||||||
|
|
||||||
|
byte[] readData = new byte[dataSize];
|
||||||
|
proxyCache.read(readData, 0, dataSize);
|
||||||
|
|
||||||
|
assertThat(readData).isEqualTo(data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,14 @@
|
|||||||
package com.danikula.videocache.support;
|
package com.danikula.videocache.support;
|
||||||
|
|
||||||
import android.text.TextUtils;
|
|
||||||
|
|
||||||
import com.danikula.videocache.HttpUrlSource;
|
|
||||||
import com.danikula.videocache.ProxyCacheException;
|
import com.danikula.videocache.ProxyCacheException;
|
||||||
|
import com.danikula.videocache.Source;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link HttpUrlSource} that throws exception in all methods.
|
* {@link Source} that throws exception in all methods.
|
||||||
*
|
*
|
||||||
* @author Alexey Danilov (danikula@gmail.com).
|
* @author Alexey Danilov (danikula@gmail.com).
|
||||||
*/
|
*/
|
||||||
public class AngryHttpUrlSource extends HttpUrlSource {
|
public class AngryHttpUrlSource implements Source {
|
||||||
|
|
||||||
public AngryHttpUrlSource(String url, String mime) {
|
|
||||||
super(url, mime);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int available() throws ProxyCacheException {
|
public int available() throws ProxyCacheException {
|
||||||
@@ -35,12 +29,4 @@ public class AngryHttpUrlSource extends HttpUrlSource {
|
|||||||
public int read(byte[] buffer) throws ProxyCacheException {
|
public int read(byte[] buffer) throws ProxyCacheException {
|
||||||
throw new IllegalStateException();
|
throw new IllegalStateException();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getMime() throws ProxyCacheException {
|
|
||||||
String mime = super.getMime();
|
|
||||||
if (!TextUtils.isEmpty(mime)) {
|
|
||||||
return mime;
|
|
||||||
}
|
|
||||||
throw new IllegalStateException();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.danikula.videocache.support;
|
package com.danikula.videocache.support;
|
||||||
|
|
||||||
import com.danikula.android.garden.io.IoUtils;
|
import com.danikula.android.garden.io.IoUtils;
|
||||||
import com.danikula.videocache.HttpProxyCache;
|
import com.danikula.videocache.HttpProxyCacheServer;
|
||||||
import com.google.common.io.Files;
|
import com.google.common.io.Files;
|
||||||
|
|
||||||
import org.robolectric.RuntimeEnvironment;
|
import org.robolectric.RuntimeEnvironment;
|
||||||
@@ -31,13 +31,13 @@ public class ProxyCacheTestUtils {
|
|||||||
return Files.asByteSource(file).read();
|
return Files.asByteSource(file).read();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Response readProxyResponse(HttpProxyCache proxy) throws IOException {
|
public static Response readProxyResponse(HttpProxyCacheServer proxy, String url) throws IOException {
|
||||||
return readProxyResponse(proxy, -1);
|
return readProxyResponse(proxy, url, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Response readProxyResponse(HttpProxyCache proxy, int offset) throws IOException {
|
public static Response readProxyResponse(HttpProxyCacheServer proxy, String url, int offset) throws IOException {
|
||||||
URL url = new URL(proxy.getUrl());
|
URL proxiedUrl = new URL(proxy.getProxyUrl(url));
|
||||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
HttpURLConnection connection = (HttpURLConnection) proxiedUrl.openConnection();
|
||||||
try {
|
try {
|
||||||
if (offset >= 0) {
|
if (offset >= 0) {
|
||||||
connection.setRequestProperty("Range", "bytes=" + offset + "-");
|
connection.setRequestProperty("Range", "bytes=" + offset + "-");
|
||||||
|
|||||||
Reference in New Issue
Block a user