24 Commits

Author SHA1 Message Date
Alexey Danilov
1b597ec1ad 📦 release 2.7.0 2017-04-23 18:47:31 +03:00
Alexey Danilov
7e48955383 fix tests 2017-04-23 18:44:45 +03:00
Alexey Danilov
355e83b4ba #88: ignore ArrayIndexOutOfBoundsException on Android L while disconnecting connection 2017-04-23 17:52:12 +03:00
Alexey Danilov
c300b2e479 🎉 fix #28: ignore system proxy for localhost requests 2017-04-23 17:38:03 +03:00
Alexey Danilov
8726693015 display test results during testing 2017-04-23 17:35:38 +03:00
Alexey Danilov
02bc21f882 enchance error logging 2017-04-22 12:44:08 +03:00
Alexey Danilov
ba6ba4ebe9 fix reporting cached percentage 2017-04-22 12:24:05 +03:00
Joe Page
f5dd92efff allow user to create a custom DiskUsage class (or extend LruDiskUsage class) to implement custom cache keep/delete rules 2017-04-21 20:59:31 +03:00
Alexey Danilov
351cf2b986 #118 simplify sample app 2017-04-21 19:16:28 +03:00
Alexey Danilov
2253b16797 #122: support for 2Gb+ files 2017-04-21 18:09:23 +03:00
Alexey Danilov
6cd18555d3 #132: format response headers in US locale to make sure library supports any language 2017-04-21 17:21:11 +03:00
Alexey Danilov
01bba67289 #112: touch file to check is cache trimming is required after cache completion 2017-04-21 17:20:04 +03:00
Alexey Danilov
e0ee4a7dde tests for cache trimming (#112, PR 114) 2017-04-21 16:48:54 +03:00
Alexey Danilov
699aa05a2d fix travis ci fail 2017-04-21 11:15:14 +03:00
Alexey Danilov
4cf2a6bb68 update build tools 2017-04-18 12:37:54 +03:00
Alexey Danilov
3cc6f591c5 increment year of licence 2017-02-01 09:08:18 +03:00
Alexey Danilov
94692447db add example for providing own FileNameGenerator 2016-12-06 09:50:57 +03:00
Alexey Danilov
3023e4a1ee 📦 release 2.6.4: hide closing output stream error stacktrace 2016-12-03 12:09:04 +03:00
Alexey Danilov
090f5ec7f9 Update LICENSE 2016-11-16 19:38:02 +03:00
Alexey Danilov
8603d5ef7f add travis and bintray badges 2016-11-15 15:45:14 +03:00
Alexey Danilov
b8a6110aa8 add travis support 2016-11-15 15:27:10 +03:00
Alexey Danilov
9353cde8f4 📦 2.6.3: return file:// proxy uri for fully cached files 2016-09-27 22:29:29 +03:00
Alexey Danilov
0d1131b0fc ping server each time before wrapping to proxy url 2016-09-27 20:10:43 +03:00
Alexey Danilov
44318d87d9 📦 2.6.2: use slf4j for logging 2016-09-27 16:03:01 +03:00
51 changed files with 875 additions and 298 deletions

9
.travis.yml Normal file
View File

@@ -0,0 +1,9 @@
language: android
jdk: oraclejdk8
android:
components:
- platform-tools
- tools
- build-tools-25.0.2
- android-23
- extra

View File

@@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Copyright 2014-2016 Alexey Danilov
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@@ -1,5 +1,5 @@
## Video cache support for Android
[![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-AndroidVideoCache-brightgreen.svg?style=flat)](http://android-arsenal.com/details/1/1751)
[![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-AndroidVideoCache-brightgreen.svg?style=flat)](http://android-arsenal.com/details/1/1751) [![Build Status](https://api.travis-ci.org/danikula/AndroidVideoCache.svg?branch=master)](https://travis-ci.org/danikula/AndroidVideoCache/) [ ![Download](https://api.bintray.com/packages/alexeydanilov/maven/videocache/images/download.svg) ](https://bintray.com/alexeydanilov/maven/videocache/_latestVersion)
## Table of Content
- [Why AndroidVideoCache?](#why-androidvideocache)
@@ -8,6 +8,7 @@
- [Recipes](#recipes)
- [Disk cache limit](#disk-cache-limit)
- [Listen caching progress](#listen-caching-progress)
- [Providing names for cached files](#providing-names-for-cached-files)
- [Sample](#sample)
- [Known problems](#known-problems)
- [Whats new](#whats-new)
@@ -32,11 +33,8 @@ Note `AndroidVideoCache` works only with **direct urls** to media file, it [**d
## Get started
Just add dependency (`AndroidVideoCache` is available in jcenter):
```
repositories {
jcenter()
}
dependencies {
compile 'com.danikula:videocache:2.6.1'
compile 'com.danikula:videocache:2.7.0'
}
```
@@ -101,6 +99,16 @@ private HttpProxyCacheServer newProxy() {
}
```
or even implement your own `DiskUsage` strategy:
```java
private HttpProxyCacheServer newProxy() {
return new HttpProxyCacheServer.Builder(this)
.diskUsage(new MyCoolDiskUsageStrategy())
.build();
}
```
### Listen caching progress
Use `HttpProxyCacheServer.registerCacheListener(CacheListener listener)` method to set listener with callback `onCacheAvailable(File cacheFile, String url, int percentsAvailable)` to be aware of caching progress. Do not forget to to unsubscribe listener with help of `HttpProxyCacheServer.unregisterCacheListener(CacheListener listener)` method to avoid memory leaks.
@@ -108,11 +116,31 @@ Use `HttpProxyCacheServer.isCached(String url)` method to check was url's conten
See `sample` app for more details.
### Providing names for cached files
By default `AndroidVideoCache` uses MD5 of video url as file name. But in some cases url is not stable and it can contain some generated parts (e.g. session token). In this case caching mechanism will be broken. To fix it you have to provide own `FileNameGenerator`:
``` java
public class MyFileNameGenerator implements FileNameGenerator {
// Urls contain mutable parts (parameter 'sessionToken') and stable video's id (parameter 'videoId').
// e. g. http://example.com?videoId=abcqaz&sessionToken=xyz987
public String generate(String url) {
Uri uri = Uri.parse(url);
String videoId = uri.getQueryParameter("videoId");
return videoId + ".mp4";
}
}
...
HttpProxyCacheServer proxy = HttpProxyCacheServer.Builder(context)
.fileNameGenerator(new MyFileNameGenerator())
.build()
```
### Sample
See `sample` app.
## Known problems
`AndroidVideoCache` [doesn't work](https://github.com/danikula/AndroidVideoCache/issues/28) if wifi or mobile internet connection uses proxy.
- In some cases clients [can't connect](https://github.com/danikula/AndroidVideoCache/issues/134) to local proxy server ('Error pinging server' error). May be it is result of previous error. Note in this case video will be played, but without caching.
## Whats new
See Release Notes [here](https://github.com/danikula/AndroidVideoCache/releases)
@@ -136,7 +164,7 @@ If it's a feature that you think would need to be discussed please open an issue
## License
Copyright 2014-2016 Alexey Danilov
Copyright 2014-2017 Alexey Danilov
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@@ -1,9 +1,9 @@
buildscript {
repositories {
mavenCentral()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.1.3'
classpath 'com.android.tools.build:gradle:2.3.1'
}
}

View File

@@ -1,6 +1,6 @@
#Mon Sep 05 18:06:17 MSK 2016
#Tue Apr 18 11:58:38 MSK 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip

View File

@@ -3,14 +3,31 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.novoda:bintray-release:0.3.4'
classpath 'com.novoda:bintray-release:0.4.0'
}
}
apply plugin: 'com.android.library'
apply plugin: 'idea'
apply plugin: 'java'
apply plugin: 'bintray-release'
android {
compileSdkVersion 23
buildToolsVersion '25.0.2'
defaultConfig {
minSdkVersion 9
targetSdkVersion 23
versionCode 21
versionName '2.7.0'
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
}
idea {
module {
downloadJavadoc = true
@@ -18,18 +35,15 @@ idea {
}
}
targetCompatibility = '1.7'
sourceCompatibility = '1.7'
dependencies {
compile 'com.google.android:android:1.6_r2'
compile 'org.slf4j:slf4j-android:1.7.21'
}
publish {
userOrg = 'alexeydanilov'
groupId = 'com.danikula'
artifactId = 'videocache'
publishVersion = '2.6.1'
publishVersion = android.defaultConfig.versionName
description = 'Cache support for android VideoView'
website = 'https://github.com/danikula/AndroidVideoCache'
}

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
package="com.danikula.videocache">
<application />
</manifest>

View File

@@ -33,7 +33,7 @@ public class ByteArrayCache implements Cache {
}
@Override
public int available() throws ProxyCacheException {
public long available() throws ProxyCacheException {
return data.length;
}

View File

@@ -22,12 +22,12 @@ public class ByteArraySource implements Source {
}
@Override
public int length() throws ProxyCacheException {
public long length() throws ProxyCacheException {
return data.length;
}
@Override
public void open(int offset) throws ProxyCacheException {
public void open(long offset) throws ProxyCacheException {
arrayInputStream = new ByteArrayInputStream(data);
arrayInputStream.skip(offset);
}

View File

@@ -7,7 +7,7 @@ package com.danikula.videocache;
*/
public interface Cache {
int available() throws ProxyCacheException;
long available() throws ProxyCacheException;
int read(byte[] buffer, long offset, int length) throws ProxyCacheException;

View File

@@ -8,6 +8,7 @@ import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Locale;
import static com.danikula.videocache.ProxyCacheUtils.DEFAULT_BUFFER_SIZE;
@@ -48,9 +49,9 @@ class HttpProxyCache extends ProxyCache {
}
private boolean isUseCache(GetRequest request) throws ProxyCacheException {
int sourceLength = source.length();
long sourceLength = source.length();
boolean sourceLengthKnown = sourceLength > 0;
int cacheAvailable = cache.available();
long cacheAvailable = cache.available();
// do not use cache for partial requests which too far from available cache. It seems user seek video.
return !sourceLengthKnown || !request.partial || request.rangeOffset <= cacheAvailable + sourceLength * NO_CACHE_BARRIER;
}
@@ -58,16 +59,16 @@ class HttpProxyCache extends ProxyCache {
private String newResponseHeaders(GetRequest request) throws IOException, ProxyCacheException {
String mime = source.getMime();
boolean mimeKnown = !TextUtils.isEmpty(mime);
int length = cache.isCompleted() ? cache.available() : source.length();
long length = cache.isCompleted() ? cache.available() : source.length();
boolean lengthKnown = length >= 0;
long contentLength = request.partial ? length - request.rangeOffset : length;
boolean addRange = lengthKnown && request.partial;
return new StringBuilder()
.append(request.partial ? "HTTP/1.1 206 PARTIAL CONTENT\n" : "HTTP/1.1 200 OK\n")
.append("Accept-Ranges: bytes\n")
.append(lengthKnown ? String.format("Content-Length: %d\n", contentLength) : "")
.append(addRange ? String.format("Content-Range: bytes %d-%d/%d\n", request.rangeOffset, length - 1, length) : "")
.append(mimeKnown ? String.format("Content-Type: %s\n", mime) : "")
.append(lengthKnown ? format("Content-Length: %d\n", contentLength) : "")
.append(addRange ? format("Content-Range: bytes %d-%d/%d\n", request.rangeOffset, length - 1, length) : "")
.append(mimeKnown ? format("Content-Type: %s\n", mime) : "")
.append("\n") // headers end
.toString();
}
@@ -98,6 +99,10 @@ class HttpProxyCache extends ProxyCache {
}
}
private String format(String pattern, Object... args) {
return String.format(Locale.US, pattern, args);
}
@Override
protected void onCachePercentsAvailableChanged(int percents) {
if (listener != null) {

View File

@@ -1,8 +1,7 @@
package com.danikula.videocache;
import android.content.Context;
import android.os.SystemClock;
import android.util.Log;
import android.net.Uri;
import com.danikula.videocache.file.DiskUsage;
import com.danikula.videocache.file.FileNameGenerator;
@@ -12,29 +11,24 @@ import com.danikula.videocache.file.TotalSizeLruDiskUsage;
import com.danikula.videocache.sourcestorage.SourceInfoStorage;
import com.danikula.videocache.sourcestorage.SourceInfoStorageFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.util.Arrays;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeoutException;
import static com.danikula.videocache.Preconditions.checkAllNotNull;
import static com.danikula.videocache.Preconditions.checkNotNull;
import static com.danikula.videocache.ProxyCacheUtils.LOG_TAG;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
/**
* Simple lightweight proxy server with file caching support that handles HTTP requests.
@@ -42,24 +36,23 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS;
* <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>
* </code></pre>
*
* @author Alexey Danilov (danikula@gmail.com).
*/
public class HttpProxyCacheServer {
private static final Logger LOG = LoggerFactory.getLogger("HttpProxyCacheServer");
private static final String PROXY_HOST = "127.0.0.1";
private static final String PING_REQUEST = "ping";
private static final String PING_RESPONSE = "ping ok";
private final Object clientsLock = new Object();
private final ExecutorService socketProcessor = Executors.newFixedThreadPool(8);
@@ -68,7 +61,7 @@ public class HttpProxyCacheServer {
private final int port;
private final Thread waitConnectionThread;
private final Config config;
private boolean pinged;
private final Pinger pinger;
public HttpProxyCacheServer(Context context) {
this(new Builder(context).buildConfig());
@@ -80,69 +73,51 @@ public class HttpProxyCacheServer {
InetAddress inetAddress = InetAddress.getByName(PROXY_HOST);
this.serverSocket = new ServerSocket(0, 8, inetAddress);
this.port = serverSocket.getLocalPort();
IgnoreHostProxySelector.install(PROXY_HOST, port);
CountDownLatch startSignal = new CountDownLatch(1);
this.waitConnectionThread = new Thread(new WaitRequestsRunnable(startSignal));
this.waitConnectionThread.start();
startSignal.await(); // freeze thread, wait for server starts
Log.i(LOG_TAG, "Proxy cache server started. Ping it...");
makeSureServerWorks();
this.pinger = new Pinger(PROXY_HOST, port);
LOG.info("Proxy cache server started. Is it alive? " + isAlive());
} catch (IOException | InterruptedException e) {
socketProcessor.shutdown();
throw new IllegalStateException("Error starting local proxy server", e);
}
}
private void makeSureServerWorks() {
int maxPingAttempts = 3;
int delay = 300;
int pingAttempts = 0;
while (pingAttempts < maxPingAttempts) {
try {
Future<Boolean> pingFuture = socketProcessor.submit(new PingCallable());
this.pinged = pingFuture.get(delay, MILLISECONDS);
if (this.pinged) {
return;
}
SystemClock.sleep(delay);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
Log.e(LOG_TAG, "Error pinging server [attempt: " + pingAttempts + ", timeout: " + delay + "]. ", e);
}
pingAttempts++;
delay *= 2;
}
Log.e(LOG_TAG, "Shutdown server… Error pinging server [attempts: " + pingAttempts + ", max timeout: " + delay / 2 + "]. " +
"If you see this message, please, email me danikula@gmail.com");
shutdown();
}
private boolean pingServer() throws ProxyCacheException {
String pingUrl = appendToProxyUrl(PING_REQUEST);
HttpUrlSource source = new HttpUrlSource(pingUrl);
try {
byte[] expectedResponse = PING_RESPONSE.getBytes();
source.open(0);
byte[] response = new byte[expectedResponse.length];
source.read(response);
boolean pingOk = Arrays.equals(expectedResponse, response);
Log.d(LOG_TAG, "Ping response: `" + new String(response) + "`, pinged? " + pingOk);
return pingOk;
} catch (ProxyCacheException e) {
Log.e(LOG_TAG, "Error reading ping response", e);
return false;
} finally {
source.close();
}
}
/**
* Returns url that wrap original url and should be used for client (MediaPlayer, ExoPlayer, etc).
* <p>
* If file for this url is fully cached (it means method {@link #isCached(String)} returns {@code true})
* then file:// uri to cached file will be returned.
* <p>
* Calling this method has same effect as calling {@link #getProxyUrl(String, boolean)} with 2nd parameter set to {@code true}.
*
* @param url a url to file that should be cached.
* @return a wrapped by proxy url if file is not fully cached or url pointed to cache file otherwise.
*/
public String getProxyUrl(String url) {
if (!pinged) {
Log.e(LOG_TAG, "Proxy server isn't pinged. Caching doesn't work. If you see this message, please, email me danikula@gmail.com");
}
return pinged ? appendToProxyUrl(url) : url;
return getProxyUrl(url, true);
}
private String appendToProxyUrl(String url) {
return String.format(Locale.US, "http://%s:%d/%s", PROXY_HOST, port, ProxyCacheUtils.encode(url));
/**
* Returns url that wrap original url and should be used for client (MediaPlayer, ExoPlayer, etc).
* <p>
* If parameter {@code allowCachedFileUri} is {@code true} and file for this url is fully cached
* (it means method {@link #isCached(String)} returns {@code true}) then file:// uri to cached file will be returned.
*
* @param url a url to file that should be cached.
* @param allowCachedFileUri {@code true} if allow to return file:// uri if url is fully cached
* @return a wrapped by proxy url if file is not fully cached or url pointed to cache file otherwise (if {@code allowCachedFileUri} is {@code true}).
*/
public String getProxyUrl(String url, boolean allowCachedFileUri) {
if (allowCachedFileUri && isCached(url)) {
File cacheFile = getCacheFile(url);
touchFileSafely(cacheFile);
return Uri.fromFile(cacheFile).toString();
}
return isAlive() ? appendToProxyUrl(url) : url;
}
public void registerCacheListener(CacheListener cacheListener, String url) {
@@ -151,7 +126,7 @@ public class HttpProxyCacheServer {
try {
getClients(url).registerCacheListener(cacheListener);
} catch (ProxyCacheException e) {
Log.d(LOG_TAG, "Error registering cache listener", e);
LOG.warn("Error registering cache listener", e);
}
}
}
@@ -162,7 +137,7 @@ public class HttpProxyCacheServer {
try {
getClients(url).unregisterCacheListener(cacheListener);
} catch (ProxyCacheException e) {
Log.d(LOG_TAG, "Error registering cache listener", e);
LOG.warn("Error registering cache listener", e);
}
}
}
@@ -184,14 +159,11 @@ public class HttpProxyCacheServer {
*/
public boolean isCached(String url) {
checkNotNull(url, "Url can't be null!");
File cacheDir = config.cacheRoot;
String fileName = config.fileNameGenerator.generate(url);
File cacheFile = new File(cacheDir, fileName);
return cacheFile.exists();
return getCacheFile(url).exists();
}
public void shutdown() {
Log.i(LOG_TAG, "Shutdown proxy server");
LOG.info("Shutdown proxy server");
shutdownClients();
@@ -207,6 +179,28 @@ public class HttpProxyCacheServer {
}
}
private boolean isAlive() {
return pinger.ping(3, 70); // 70+140+280=max~500ms
}
private String appendToProxyUrl(String url) {
return String.format(Locale.US, "http://%s:%d/%s", PROXY_HOST, port, ProxyCacheUtils.encode(url));
}
private File getCacheFile(String url) {
File cacheDir = config.cacheRoot;
String fileName = config.fileNameGenerator.generate(url);
return new File(cacheDir, fileName);
}
private void touchFileSafely(File cacheFile) {
try {
config.diskUsage.touch(cacheFile);
} catch (IOException e) {
LOG.error("Error touching file " + cacheFile, e);
}
}
private void shutdownClients() {
synchronized (clientsLock) {
for (HttpProxyCacheServerClients clients : clientsMap.values()) {
@@ -220,7 +214,7 @@ public class HttpProxyCacheServer {
try {
while (!Thread.currentThread().isInterrupted()) {
Socket socket = serverSocket.accept();
Log.d(LOG_TAG, "Accept new socket " + socket);
LOG.debug("Accept new socket " + socket);
socketProcessor.submit(new SocketProcessorRunnable(socket));
}
} catch (IOException e) {
@@ -231,10 +225,10 @@ public class HttpProxyCacheServer {
private void processSocket(Socket socket) {
try {
GetRequest request = GetRequest.read(socket.getInputStream());
Log.i(LOG_TAG, "Request to cache proxy:" + request);
LOG.debug("Request to cache proxy:" + request);
String url = ProxyCacheUtils.decode(request.uri);
if (PING_REQUEST.equals(url)) {
responseToPing(socket);
if (pinger.isPingRequest(url)) {
pinger.responseToPing(socket);
} else {
HttpProxyCacheServerClients clients = getClients(url);
clients.processRequest(request, socket);
@@ -242,21 +236,15 @@ public class HttpProxyCacheServer {
} 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, "Closing socket… Socket is closed by client.");
LOG.debug("Closing socket… Socket is closed by client.");
} catch (ProxyCacheException | IOException e) {
onError(new ProxyCacheException("Error processing request", e));
} finally {
releaseSocket(socket);
Log.d(LOG_TAG, "Opened connections: " + getClientsCount());
LOG.debug("Opened connections: " + getClientsCount());
}
}
private void responseToPing(Socket socket) throws IOException {
OutputStream out = socket.getOutputStream();
out.write("HTTP/1.1 200 OK\n\n".getBytes());
out.write(PING_RESPONSE.getBytes());
}
private HttpProxyCacheServerClients getClients(String url) throws ProxyCacheException {
synchronized (clientsLock) {
HttpProxyCacheServerClients clients = clientsMap.get(url);
@@ -292,7 +280,7 @@ public class HttpProxyCacheServer {
} 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, "Releasing input stream… Socket is closed by client.");
LOG.debug("Releasing input stream… Socket is closed by client.");
} catch (IOException e) {
onError(new ProxyCacheException("Error closing socket input stream", e));
}
@@ -304,7 +292,7 @@ public class HttpProxyCacheServer {
socket.shutdownOutput();
}
} catch (IOException e) {
onError(new ProxyCacheException("Error closing socket output stream", e));
LOG.warn("Failed to close socket on proxy side: {}. It seems client have already closed connection.", e.getMessage());
}
}
@@ -319,7 +307,7 @@ public class HttpProxyCacheServer {
}
private void onError(Throwable e) {
Log.e(LOG_TAG, "HttpProxyCacheServer error", e);
LOG.error("HttpProxyCacheServer error", e);
}
private final class WaitRequestsRunnable implements Runnable {
@@ -351,14 +339,6 @@ public class HttpProxyCacheServer {
}
}
private class PingCallable implements Callable<Boolean> {
@Override
public Boolean call() throws Exception {
return pingServer();
}
}
/**
* Builder for {@link HttpProxyCacheServer}.
*/
@@ -380,11 +360,11 @@ public class HttpProxyCacheServer {
/**
* Overrides default cache folder to be used for caching files.
* <p/>
* <p>
* By default AndroidVideoCache uses
* '/Android/data/[app_package_name]/cache/video-cache/' if card is mounted and app has appropriate permission
* or 'video-cache' subdirectory in default application's cache directory otherwise.
* <p/>
* </p>
* <b>Note</b> directory must be used <b>only</b> for AndroidVideoCache files.
*
* @param file a cache directory, can't be null.
@@ -408,9 +388,10 @@ public class HttpProxyCacheServer {
/**
* Sets max cache size in bytes.
* <p>
* All files that exceeds limit will be deleted using LRU strategy.
* Default value is 512 Mb.
* <p/>
* </p>
* Note this method overrides result of calling {@link #maxCacheFilesCount(int)}
*
* @param maxSize max cache size in bytes.
@@ -424,7 +405,6 @@ public class HttpProxyCacheServer {
/**
* Sets max cache files count.
* All files that exceeds limit will be deleted using LRU strategy.
* <p/>
* Note this method overrides result of calling {@link #maxCacheSize(long)}
*
* @param count max cache files count.
@@ -435,6 +415,17 @@ public class HttpProxyCacheServer {
return this;
}
/**
* Set custom DiskUsage logic for handling when to keep or clean cache.
*
* @param diskUsage a disk usage strategy, cant be {@code null}.
* @return a builder.
*/
public Builder diskUsage(DiskUsage diskUsage) {
this.diskUsage = checkNotNull(diskUsage);
return this;
}
/**
* Builds new instance of {@link HttpProxyCacheServer}.
*

View File

@@ -1,11 +1,13 @@
package com.danikula.videocache;
import android.text.TextUtils;
import android.util.Log;
import com.danikula.videocache.sourcestorage.SourceInfoStorage;
import com.danikula.videocache.sourcestorage.SourceInfoStorageFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
@@ -15,7 +17,6 @@ import java.net.URL;
import static com.danikula.videocache.Preconditions.checkNotNull;
import static com.danikula.videocache.ProxyCacheUtils.DEFAULT_BUFFER_SIZE;
import static com.danikula.videocache.ProxyCacheUtils.LOG_TAG;
import static java.net.HttpURLConnection.HTTP_MOVED_PERM;
import static java.net.HttpURLConnection.HTTP_MOVED_TEMP;
import static java.net.HttpURLConnection.HTTP_OK;
@@ -29,6 +30,8 @@ import static java.net.HttpURLConnection.HTTP_SEE_OTHER;
*/
public class HttpUrlSource implements Source {
private static final Logger LOG = LoggerFactory.getLogger("HttpUrlSource");
private static final int MAX_REDIRECTS = 5;
private final SourceInfoStorage sourceInfoStorage;
private SourceInfo sourceInfo;
@@ -52,7 +55,7 @@ public class HttpUrlSource implements Source {
}
@Override
public synchronized int length() throws ProxyCacheException {
public synchronized long length() throws ProxyCacheException {
if (sourceInfo.length == Integer.MIN_VALUE) {
fetchContentInfo();
}
@@ -60,12 +63,12 @@ public class HttpUrlSource implements Source {
}
@Override
public void open(int offset) throws ProxyCacheException {
public void open(long offset) throws ProxyCacheException {
try {
connection = openConnection(offset, -1);
String mime = connection.getContentType();
inputStream = new BufferedInputStream(connection.getInputStream(), DEFAULT_BUFFER_SIZE);
int length = readSourceAvailableBytes(connection, offset, connection.getResponseCode());
long length = readSourceAvailableBytes(connection, offset, connection.getResponseCode());
this.sourceInfo = new SourceInfo(sourceInfo.url, length, mime);
this.sourceInfoStorage.put(sourceInfo.url, sourceInfo);
} catch (IOException e) {
@@ -73,12 +76,17 @@ public class HttpUrlSource implements Source {
}
}
private int readSourceAvailableBytes(HttpURLConnection connection, int offset, int responseCode) throws IOException {
int contentLength = connection.getContentLength();
private long readSourceAvailableBytes(HttpURLConnection connection, long offset, int responseCode) throws IOException {
long contentLength = getContentLength(connection);
return responseCode == HTTP_OK ? contentLength
: responseCode == HTTP_PARTIAL ? contentLength + offset : sourceInfo.length;
}
private long getContentLength(HttpURLConnection connection) {
String contentLengthValue = connection.getHeaderField("Content-Length");
return contentLengthValue == null ? -1 : Long.parseLong(contentLengthValue);
}
@Override
public void close() throws ProxyCacheException {
if (connection != null) {
@@ -87,8 +95,13 @@ public class HttpUrlSource implements Source {
} catch (NullPointerException | IllegalArgumentException e) {
String message = "Wait... but why? WTF!? " +
"Really shouldn't happen any more after fixing https://github.com/danikula/AndroidVideoCache/issues/43. " +
"If you read it on your device log, please, notify me danikula@gmail.com or create issue here https://github.com/danikula/AndroidVideoCache/issues.";
"If you read it on your device log, please, notify me danikula@gmail.com or create issue here " +
"https://github.com/danikula/AndroidVideoCache/issues.";
throw new RuntimeException(message, e);
} catch (ArrayIndexOutOfBoundsException e) {
LOG.error("Error closing connection correctly. Should happen only on Android L. " +
"If anybody know how to fix it, please visit https://github.com/danikula/AndroidVideoCache/issues/88. " +
"Until good solution is not know, just ignore this issue :(", e);
}
}
}
@@ -108,19 +121,19 @@ public class HttpUrlSource implements Source {
}
private void fetchContentInfo() throws ProxyCacheException {
Log.d(LOG_TAG, "Read content info from " + sourceInfo.url);
LOG.debug("Read content info from " + sourceInfo.url);
HttpURLConnection urlConnection = null;
InputStream inputStream = null;
try {
urlConnection = openConnection(0, 10000);
int length = urlConnection.getContentLength();
long length = getContentLength(urlConnection);
String mime = urlConnection.getContentType();
inputStream = urlConnection.getInputStream();
this.sourceInfo = new SourceInfo(sourceInfo.url, length, mime);
this.sourceInfoStorage.put(sourceInfo.url, sourceInfo);
Log.i(LOG_TAG, "Source info fetched: " + sourceInfo);
LOG.debug("Source info fetched: " + sourceInfo);
} catch (IOException e) {
Log.e(LOG_TAG, "Error fetching info from " + sourceInfo.url, e);
LOG.error("Error fetching info from " + sourceInfo.url, e);
} finally {
ProxyCacheUtils.close(inputStream);
if (urlConnection != null) {
@@ -129,13 +142,13 @@ public class HttpUrlSource implements Source {
}
}
private HttpURLConnection openConnection(int offset, int timeout) throws IOException, ProxyCacheException {
private HttpURLConnection openConnection(long offset, int timeout) throws IOException, ProxyCacheException {
HttpURLConnection connection;
boolean redirected;
int redirectCount = 0;
String url = this.sourceInfo.url;
do {
Log.d(LOG_TAG, "Open connection " + (offset > 0 ? " with offset " + offset : "") + " to " + url);
LOG.debug("Open connection " + (offset > 0 ? " with offset " + offset : "") + " to " + url);
connection = (HttpURLConnection) new URL(url).openConnection();
if (offset > 0) {
connection.setRequestProperty("Range", "bytes=" + offset + "-");

View File

@@ -0,0 +1,50 @@
package com.danikula.videocache;
import java.io.IOException;
import java.net.Proxy;
import java.net.ProxySelector;
import java.net.SocketAddress;
import java.net.URI;
import java.util.Arrays;
import java.util.List;
import static com.danikula.videocache.Preconditions.checkNotNull;
/**
* {@link ProxySelector} that ignore system default proxies for concrete host.
* <p>
* It is important to <a href="https://github.com/danikula/AndroidVideoCache/issues/28">ignore system proxy</a> for localhost connection.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
class IgnoreHostProxySelector extends ProxySelector {
private static final List<Proxy> NO_PROXY_LIST = Arrays.asList(Proxy.NO_PROXY);
private final ProxySelector defaultProxySelector;
private final String hostToIgnore;
private final int portToIgnore;
IgnoreHostProxySelector(ProxySelector defaultProxySelector, String hostToIgnore, int portToIgnore) {
this.defaultProxySelector = checkNotNull(defaultProxySelector);
this.hostToIgnore = checkNotNull(hostToIgnore);
this.portToIgnore = portToIgnore;
}
static void install(String hostToIgnore, int portToIgnore) {
ProxySelector defaultProxySelector = ProxySelector.getDefault();
ProxySelector ignoreHostProxySelector = new IgnoreHostProxySelector(defaultProxySelector, hostToIgnore, portToIgnore);
ProxySelector.setDefault(ignoreHostProxySelector);
}
@Override
public List<Proxy> select(URI uri) {
boolean ignored = hostToIgnore.equals(uri.getHost()) && portToIgnore == uri.getPort();
return ignored ? NO_PROXY_LIST : defaultProxySelector.select(uri);
}
@Override
public void connectFailed(URI uri, SocketAddress address, IOException failure) {
defaultProxySelector.connectFailed(uri, address, failure);
}
}

View File

@@ -0,0 +1,127 @@
package com.danikula.videocache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Proxy;
import java.net.ProxySelector;
import java.net.Socket;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeoutException;
import static com.danikula.videocache.Preconditions.checkArgument;
import static com.danikula.videocache.Preconditions.checkNotNull;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
/**
* Pings {@link HttpProxyCacheServer} to make sure it works.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
class Pinger {
private static final Logger LOG = LoggerFactory.getLogger("Pinger");
private static final String PING_REQUEST = "ping";
private static final String PING_RESPONSE = "ping ok";
private final ExecutorService pingExecutor = Executors.newSingleThreadExecutor();
private final String host;
private final int port;
Pinger(String host, int port) {
this.host = checkNotNull(host);
this.port = port;
}
boolean ping(int maxAttempts, int startTimeout) {
checkArgument(maxAttempts >= 1);
checkArgument(startTimeout > 0);
int timeout = startTimeout;
int attempts = 0;
while (attempts < maxAttempts) {
try {
Future<Boolean> pingFuture = pingExecutor.submit(new PingCallable());
boolean pinged = pingFuture.get(timeout, MILLISECONDS);
if (pinged) {
return true;
}
} catch (TimeoutException e) {
LOG.warn("Error pinging server (attempt: " + attempts + ", timeout: " + timeout + "). ");
} catch (InterruptedException | ExecutionException e) {
LOG.error("Error pinging server due to unexpected error", e);
}
attempts++;
timeout *= 2;
}
String error = String.format(Locale.US, "Error pinging server (attempts: %d, max timeout: %d). " +
"If you see this message, please, report at https://github.com/danikula/AndroidVideoCache/issues/134. " +
"Default proxies are: %s"
, attempts, timeout / 2, getDefaultProxies());
LOG.error(error, new ProxyCacheException(error));
return false;
}
private List<Proxy> getDefaultProxies() {
try {
ProxySelector defaultProxySelector = ProxySelector.getDefault();
return defaultProxySelector.select(new URI(getPingUrl()));
} catch (URISyntaxException e) {
throw new IllegalStateException(e);
}
}
boolean isPingRequest(String request) {
return PING_REQUEST.equals(request);
}
void responseToPing(Socket socket) throws IOException {
OutputStream out = socket.getOutputStream();
out.write("HTTP/1.1 200 OK\n\n".getBytes());
out.write(PING_RESPONSE.getBytes());
}
private boolean pingServer() throws ProxyCacheException {
String pingUrl = getPingUrl();
HttpUrlSource source = new HttpUrlSource(pingUrl);
try {
byte[] expectedResponse = PING_RESPONSE.getBytes();
source.open(0);
byte[] response = new byte[expectedResponse.length];
source.read(response);
boolean pingOk = Arrays.equals(expectedResponse, response);
LOG.info("Ping response: `" + new String(response) + "`, pinged? " + pingOk);
return pingOk;
} catch (ProxyCacheException e) {
LOG.error("Error reading ping response", e);
return false;
} finally {
source.close();
}
}
private String getPingUrl() {
return String.format(Locale.US, "http://%s:%d/%s", host, port, PING_REQUEST);
}
private class PingCallable implements Callable<Boolean> {
@Override
public Boolean call() throws Exception {
return pingServer();
}
}
}

View File

@@ -1,11 +1,11 @@
package com.danikula.videocache;
import android.util.Log;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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}).
@@ -18,6 +18,7 @@ import static com.danikula.videocache.ProxyCacheUtils.LOG_TAG;
*/
class ProxyCache {
private static final Logger LOG = LoggerFactory.getLogger("ProxyCache");
private static final int MAX_READ_SOURCE_ATTEMPTS = 1;
private final Source source;
@@ -61,7 +62,7 @@ class ProxyCache {
public void shutdown() {
synchronized (stopLock) {
Log.d(LOG_TAG, "Shutdown proxy for " + source);
LOG.debug("Shutdown proxy for " + source);
try {
stopped = true;
if (sourceReaderThread != null) {
@@ -102,7 +103,7 @@ class ProxyCache {
protected void onCacheAvailable(long cacheAvailable, long sourceLength) {
boolean zeroLengthSource = sourceLength == 0;
int percents = zeroLengthSource ? 100 : (int) (cacheAvailable * 100 / sourceLength);
int percents = zeroLengthSource ? 100 : (int) ((float) cacheAvailable / sourceLength * 100);
boolean percentsChanged = percents != percentsAvailable;
boolean sourceLengthKnown = sourceLength >= 0;
if (sourceLengthKnown && percentsChanged) {
@@ -115,8 +116,8 @@ class ProxyCache {
}
private void readSource() {
int sourceAvailable = -1;
int offset = 0;
long sourceAvailable = -1;
long offset = 0;
try {
offset = cache.available();
source.open(offset);
@@ -173,9 +174,9 @@ class ProxyCache {
protected final void onError(final Throwable e) {
boolean interruption = e instanceof InterruptedProxyCacheException;
if (interruption) {
Log.d(LOG_TAG, "ProxyCache is interrupted");
LOG.debug("ProxyCache is interrupted");
} else {
Log.e(LOG_TAG, "ProxyCache error", e);
LOG.error("ProxyCache error", e);
}
}

View File

@@ -7,15 +7,17 @@ package com.danikula.videocache;
*/
public class ProxyCacheException extends Exception {
private static final String LIBRARY_VERSION = ". Version: " + BuildConfig.VERSION_NAME;
public ProxyCacheException(String message) {
super(message);
super(message + LIBRARY_VERSION);
}
public ProxyCacheException(String message, Throwable cause) {
super(message, cause);
super(message + LIBRARY_VERSION, cause);
}
public ProxyCacheException(Throwable cause) {
super(cause);
super("No explanation error" + LIBRARY_VERSION, cause);
}
}

View File

@@ -1,9 +1,11 @@
package com.danikula.videocache;
import android.text.TextUtils;
import android.util.Log;
import android.webkit.MimeTypeMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Closeable;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
@@ -23,7 +25,7 @@ import static com.danikula.videocache.Preconditions.checkNotNull;
*/
public class ProxyCacheUtils {
static final String LOG_TAG = "ProxyCache";
private static final Logger LOG = LoggerFactory.getLogger("ProxyCacheUtils");
static final int DEFAULT_BUFFER_SIZE = 8 * 1024;
static final int MAX_ARRAY_PREVIEW = 16;
@@ -70,7 +72,7 @@ public class ProxyCacheUtils {
try {
closeable.close();
} catch (IOException e) {
Log.e(LOG_TAG, "Error closing resource", e);
LOG.error("Error closing resource", e);
}
}
}

View File

@@ -13,7 +13,7 @@ public interface Source {
* @param offset offset in bytes for source.
* @throws ProxyCacheException if error occur while opening source.
*/
void open(int offset) throws ProxyCacheException;
void open(long offset) throws ProxyCacheException;
/**
* Returns length bytes or <b>negative value</b> if length is unknown.
@@ -21,12 +21,13 @@ public interface Source {
* @return bytes length
* @throws ProxyCacheException if error occur while fetching source data.
*/
int length() throws ProxyCacheException;
long length() throws ProxyCacheException;
/**
* Read data to byte buffer from source with current offset.
*
* @param buffer a buffer to be used for reading data.
* @return a count of read bytes
* @throws ProxyCacheException if error occur while reading source.
*/
int read(byte[] buffer) throws ProxyCacheException;

View File

@@ -8,10 +8,10 @@ package com.danikula.videocache;
public class SourceInfo {
public final String url;
public final int length;
public final long length;
public final String mime;
public SourceInfo(String url, int length, String mime) {
public SourceInfo(String url, long length, String mime) {
this.url = url;
this.length = length;
this.mime = mime;

View File

@@ -2,12 +2,13 @@ package com.danikula.videocache;
import android.content.Context;
import android.os.Environment;
import android.util.Log;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import static android.os.Environment.MEDIA_MOUNTED;
import static com.danikula.videocache.ProxyCacheUtils.LOG_TAG;
/**
* Provides application storage paths
@@ -19,6 +20,7 @@ import static com.danikula.videocache.ProxyCacheUtils.LOG_TAG;
*/
final class StorageUtils {
private static final Logger LOG = LoggerFactory.getLogger("StorageUtils");
private static final String INDIVIDUAL_DIR_NAME = "video-cache";
/**
@@ -61,7 +63,7 @@ final class StorageUtils {
}
if (appCacheDir == null) {
String cacheDirPath = "/data/data/" + context.getPackageName() + "/cache/";
Log.w(LOG_TAG, "Can't define system cache directory! '" + cacheDirPath + "%s' will be used.");
LOG.warn("Can't define system cache directory! '" + cacheDirPath + "%s' will be used.");
appCacheDir = new File(cacheDirPath);
}
return appCacheDir;
@@ -72,7 +74,7 @@ final class StorageUtils {
File appCacheDir = new File(new File(dataDir, context.getPackageName()), "cache");
if (!appCacheDir.exists()) {
if (!appCacheDir.mkdirs()) {
Log.w(LOG_TAG, "Unable to create external cache directory");
LOG.warn("Unable to create external cache directory");
return null;
}
}

View File

@@ -41,7 +41,7 @@ public class FileCache implements Cache {
}
@Override
public synchronized int available() throws ProxyCacheException {
public synchronized long available() throws ProxyCacheException {
try {
return (int) dataFile.length();
} catch (IOException e) {
@@ -100,6 +100,7 @@ public class FileCache implements Cache {
file = completedFile;
try {
dataFile = new RandomAccessFile(file, "r");
diskUsage.touch(file);
} catch (IOException e) {
throw new ProxyCacheException("Error opening " + file + " as disc cache", e);
}

View File

@@ -1,11 +1,15 @@
package com.danikula.videocache.file;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
@@ -16,6 +20,8 @@ import java.util.List;
*/
class Files {
private static final Logger LOG = LoggerFactory.getLogger("Files");
static void makeDir(File directory) throws IOException {
if (directory.exists()) {
if (!directory.isDirectory()) {
@@ -46,7 +52,8 @@ class Files {
if (!modified) {
modify(file);
if (file.lastModified() < now) {
throw new IOException("Error set last modified date to " + file);
// NOTE: apparently this is a known issue (see: http://stackoverflow.com/questions/6633748/file-lastmodified-is-never-what-was-set-with-file-setlastmodified)
LOG.warn("Last modified date {} is not set for file {}", new Date(file.lastModified()), file.getAbsolutePath());
}
}
}

View File

@@ -1,6 +1,7 @@
package com.danikula.videocache.file;
import android.util.Log;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
@@ -14,9 +15,9 @@ import java.util.concurrent.Executors;
*
* @author Alexey Danilov (danikula@gmail.com).
*/
abstract class LruDiskUsage implements DiskUsage {
public abstract class LruDiskUsage implements DiskUsage {
private static final String LOG_TAG = "ProxyCache";
private static final Logger LOG = LoggerFactory.getLogger("LruDiskUsage");
private final ExecutorService workerThread = Executors.newSingleThreadExecutor();
@Override
@@ -43,9 +44,9 @@ abstract class LruDiskUsage implements DiskUsage {
if (deleted) {
totalCount--;
totalSize -= fileSize;
Log.i(LOG_TAG, "Cache file " + file + " is deleted because it exceeds cache limit");
LOG.info("Cache file " + file + " is deleted because it exceeds cache limit");
} else {
Log.e(LOG_TAG, "Error deleting file " + file + " for trimming cache");
LOG.error("Error deleting file " + file + " for trimming cache");
}
}
}

View File

@@ -83,7 +83,7 @@ class DatabaseSourceInfoStorage extends SQLiteOpenHelper implements SourceInfoSt
private SourceInfo convert(Cursor cursor) {
return new SourceInfo(
cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_URL)),
cursor.getInt(cursor.getColumnIndexOrThrow(COLUMN_LENGTH)),
cursor.getLong(cursor.getColumnIndexOrThrow(COLUMN_LENGTH)),
cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_MIME))
);
}

View File

@@ -16,7 +16,7 @@ apply plugin: 'com.neenbedankt.android-apt'
android {
compileSdkVersion 23
buildToolsVersion '24'
buildToolsVersion '25.0.2'
defaultConfig {
applicationId 'com.danikula.videocache.sample'
@@ -38,7 +38,7 @@ dependencies {
// compile project(':library')
compile 'com.android.support:support-v4:23.1.0'
compile 'org.androidannotations:androidannotations-api:3.3.2'
compile 'com.danikula:videocache:2.6.1'
compile 'com.danikula:videocache:2.7.0'
compile 'com.viewpagerindicator:library:2.4.2-SNAPSHOT@aar'
apt 'org.androidannotations:androidannotations:3.3.2'
}

View File

@@ -18,6 +18,8 @@ public class App extends Application {
}
private HttpProxyCacheServer newProxy() {
return new HttpProxyCacheServer(this);
return new HttpProxyCacheServer.Builder(this)
.cacheDirectory(Utils.getVideoCacheDir(this))
.build();
}
}

View File

@@ -1,6 +1,5 @@
package com.danikula.videocache.sample;
import android.content.Context;
import android.os.Handler;
import android.os.Message;
import android.support.v4.app.Fragment;
@@ -22,10 +21,7 @@ import java.io.File;
@EFragment(R.layout.fragment_video)
public class GalleryVideoFragment extends Fragment implements CacheListener {
private static final String LOG_TAG = "VideoFragment";
@FragmentArg String url;
@FragmentArg String cachePath;
@InstanceState int position;
@InstanceState boolean playerStarted;
@@ -37,14 +33,9 @@ public class GalleryVideoFragment extends Fragment implements CacheListener {
private final VideoProgressUpdater updater = new VideoProgressUpdater();
public static Fragment build(Context context, Video video) {
return build(video.url, video.getCacheFile(context).getAbsolutePath());
}
public static Fragment build(String url, String cachePath) {
public static Fragment build(String url) {
return GalleryVideoFragment_.builder()
.url(url)
.cachePath(cachePath)
.build();
}

View File

@@ -49,7 +49,8 @@ public class MenuActivity extends FragmentActivity {
@Click(R.id.cleanCacheButton)
void onClearCacheButtonClick() {
try {
Utils.cleanDirectory(getExternalCacheDir());
Utils.cleanVideoCacheDir(this);
} catch (IOException e) {
Log.e(null, "Error cleaning cache", e);
Toast.makeText(this, "Error cleaning cache", Toast.LENGTH_LONG).show();

View File

@@ -23,7 +23,7 @@ public class MultipleVideosActivity extends FragmentActivity {
private void addVideoFragment(Video video, int containerViewId) {
getSupportFragmentManager()
.beginTransaction()
.add(containerViewId, VideoFragment.build(this, video))
.add(containerViewId, VideoFragment.build(video.url))
.commit();
}
}

View File

@@ -23,7 +23,7 @@ public class SharedCacheActivity extends FragmentActivity {
private void addVideoFragment(Video video, int containerViewId) {
getSupportFragmentManager()
.beginTransaction()
.add(containerViewId, VideoFragment.build(this, video))
.add(containerViewId, VideoFragment.build(video.url))
.commit();
}
}

View File

@@ -15,7 +15,7 @@ public class SingleVideoActivity extends FragmentActivity {
if (state == null) {
getSupportFragmentManager()
.beginTransaction()
.add(R.id.containerView, VideoFragment.build(this, Video.ORANGE_1))
.add(R.id.containerView, VideoFragment.build(Video.ORANGE_1.url))
.commit();
}
}

View File

@@ -1,5 +1,7 @@
package com.danikula.videocache.sample;
import android.content.Context;
import java.io.File;
import java.io.IOException;
@@ -10,7 +12,16 @@ import java.io.IOException;
*/
public class Utils {
public static void cleanDirectory(File file) throws IOException {
public static File getVideoCacheDir(Context context) {
return new File(context.getExternalCacheDir(), "video-cache");
}
public static void cleanVideoCacheDir(Context context) throws IOException {
File videoCacheDir = getVideoCacheDir(context);
cleanDirectory(videoCacheDir);
}
private static void cleanDirectory(File file) throws IOException {
if (!file.exists()) {
return;
}

View File

@@ -1,9 +1,5 @@
package com.danikula.videocache.sample;
import android.content.Context;
import java.io.File;
public enum Video {
ORANGE_1(Config.ROOT + "orange1.mp4"),
@@ -18,10 +14,6 @@ public enum Video {
this.url = url;
}
public File getCacheFile(Context context) {
return new File(context.getExternalCacheDir(), name());
}
private class Config {
private static final String ROOT = "https://raw.githubusercontent.com/danikula/AndroidVideoCache/master/files/";
}

View File

@@ -1,6 +1,5 @@
package com.danikula.videocache.sample;
import android.content.Context;
import android.os.Handler;
import android.os.Message;
import android.support.v4.app.Fragment;
@@ -26,7 +25,6 @@ public class VideoFragment extends Fragment implements CacheListener {
private static final String LOG_TAG = "VideoFragment";
@FragmentArg String url;
@FragmentArg String cachePath;
@ViewById ImageView cacheStatusImageView;
@ViewById VideoView videoView;
@@ -34,14 +32,9 @@ public class VideoFragment extends Fragment implements CacheListener {
private final VideoProgressUpdater updater = new VideoProgressUpdater();
public static Fragment build(Context context, Video video) {
return build(video.url, video.getCacheFile(context).getAbsolutePath());
}
public static Fragment build(String url, String cachePath) {
public static Fragment build(String url) {
return VideoFragment_.builder()
.url(url)
.cachePath(cachePath)
.build();
}
@@ -55,12 +48,17 @@ public class VideoFragment extends Fragment implements CacheListener {
HttpProxyCacheServer proxy = App.getProxy(getActivity());
boolean fullyCached = proxy.isCached(url);
setCachedState(fullyCached);
if (fullyCached) {
progressBar.setSecondaryProgress(100);
}
}
private void startVideo() {
HttpProxyCacheServer proxy = App.getProxy(getActivity());
proxy.registerCacheListener(this, url);
videoView.setVideoPath(proxy.getProxyUrl(url));
String proxyUrl = proxy.getProxyUrl(url);
Log.d(LOG_TAG, "Use proxy url " + proxyUrl + " instead of original url " + url);
videoView.setVideoPath(proxyUrl);
videoView.start();
}

View File

@@ -1,6 +1,5 @@
package com.danikula.videocache.sample;
import android.content.Context;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentStatePagerAdapter;
@@ -27,17 +26,14 @@ public class VideoGalleryActivity extends FragmentActivity {
private static final class ViewsPagerAdapter extends FragmentStatePagerAdapter {
private final Context context;
public ViewsPagerAdapter(FragmentActivity activity) {
super(activity.getSupportFragmentManager());
this.context = activity;
}
@Override
public Fragment getItem(int position) {
Video video = Video.values()[position];
return GalleryVideoFragment.build(context, video);
return GalleryVideoFragment.build(video.url);
}
@Override

View File

@@ -5,13 +5,13 @@ repositories {
apply plugin: 'com.android.application'
android {
compileSdkVersion 22
buildToolsVersion '22.0.1'
compileSdkVersion 23
buildToolsVersion '25.0.2'
defaultConfig {
applicationId 'com.danikula.proxycache.test'
minSdkVersion 16
targetSdkVersion 22
targetSdkVersion 23
versionCode 1
versionName '0.1'
}
@@ -21,11 +21,47 @@ android {
}
}
// temporary workaround for Robolectric issue https://github.com/robolectric/robolectric/issues/2647
android.applicationVariants.all { variant ->
def productFlavor = variant.productFlavors[0] != null ? "${variant.productFlavors[0].name.capitalize()}" : ""
def buildType = "${variant.buildType.name.capitalize()}"
tasks["compile${productFlavor}${buildType}UnitTestSources"].dependsOn(tasks["merge${productFlavor}${buildType}Assets"])
}
// display test progress http://stackoverflow.com/a/36130467/999458
tasks.withType(Test) {
testLogging {
// set options for log level LIFECYCLE
events "passed", "skipped", "failed", "standardOut"
showExceptions true
exceptionFormat "full"
showCauses true
showStackTraces true
// set options for log level DEBUG and INFO
debug {
events "started", "passed", "skipped", "failed", "standardOut", "standardError"
exceptionFormat "full"
}
info.events = debug.events
info.exceptionFormat = debug.exceptionFormat
afterSuite { desc, result ->
if (!desc.parent) { // will match the outermost suite
def output = "Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} successes, ${result.failedTestCount} failures, ${result.skippedTestCount} skipped)"
def startItem = '| ', endItem = ' |'
def repeatLength = startItem.length() + output.length() + endItem.length()
println('\n' + ('-' * repeatLength) + '\n' + startItem + output + endItem + '\n' + ('-' * repeatLength))
}
}
}
}
dependencies {
compile project(':library')
testCompile 'org.slf4j:slf4j-simple:1.7.21'
testCompile 'junit:junit:4.12'
testCompile 'org.robolectric:robolectric:3.1'
testCompile 'org.robolectric:robolectric:3.3.2'
testCompile 'com.squareup:fest-android:1.0.0'
testCompile 'com.google.guava:guava-jdk5:17.0'
testCompile('com.danikula:android-garden:2.1.4') {

View File

@@ -0,0 +1,17 @@
package com.danikula.videocache;
import com.danikula.videocache.test.BuildConfig;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class)
public abstract class BaseTest {
static {
System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "trace");
}
}

View File

@@ -2,12 +2,8 @@ package com.danikula.videocache;
import com.danikula.videocache.file.FileNameGenerator;
import com.danikula.videocache.file.Md5FileNameGenerator;
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;
@@ -19,9 +15,7 @@ import static org.fest.assertions.api.Assertions.fail;
*
* @author Alexey Danilov (danikula@gmail.com).
*/
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class)
public class FileNameGeneratorTest {
public class FileNameGeneratorTest extends BaseTest {
@Test
public void testMd5SimpleName() throws Exception {

View File

@@ -1,11 +1,6 @@
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;
@@ -16,9 +11,7 @@ import static org.fest.assertions.api.Assertions.fail;
/**
* @author Alexey Danilov (danikula@gmail.com).
*/
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class)
public class GetRequestTest {
public class GetRequestTest extends BaseTest {
@Test
public void testPartialHttpGet() throws Exception {

View File

@@ -1,5 +1,6 @@
package com.danikula.videocache;
import android.net.Uri;
import android.util.Pair;
import com.danikula.android.garden.io.IoUtils;
@@ -7,14 +8,10 @@ import com.danikula.videocache.file.FileNameGenerator;
import com.danikula.videocache.file.Md5FileNameGenerator;
import com.danikula.videocache.support.ProxyCacheTestUtils;
import com.danikula.videocache.support.Response;
import com.danikula.videocache.test.BuildConfig;
import org.junit.Before;
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;
@@ -33,16 +30,17 @@ import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_URL_
import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_URL_6_REDIRECTS;
import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_URL_ONE_REDIRECT;
import static com.danikula.videocache.support.ProxyCacheTestUtils.getFileContent;
import static com.danikula.videocache.support.ProxyCacheTestUtils.getPort;
import static com.danikula.videocache.support.ProxyCacheTestUtils.installExternalSystemProxy;
import static com.danikula.videocache.support.ProxyCacheTestUtils.loadAssetFile;
import static com.danikula.videocache.support.ProxyCacheTestUtils.readProxyResponse;
import static com.danikula.videocache.support.ProxyCacheTestUtils.resetSystemProxy;
import static org.fest.assertions.api.Assertions.assertThat;
/**
* @author Alexey Danilov (danikula@gmail.com).
*/
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class)
public class HttpProxyCacheServerTest {
public class HttpProxyCacheServerTest extends BaseTest {
private File cacheFolder;
@@ -51,6 +49,7 @@ public class HttpProxyCacheServerTest {
cacheFolder = ProxyCacheTestUtils.newCacheFile();
createDirectory(cacheFolder);
cleanDirectory(cacheFolder);
resetSystemProxy();
}
@Test
@@ -256,6 +255,111 @@ public class HttpProxyCacheServerTest {
assertThat(proxy.isCached(HTTP_DATA_URL)).isFalse();
}
@Test
public void testGetProxiedUrlForEmptyCache() throws Exception {
HttpProxyCacheServer proxy = newProxy(cacheFolder);
String expectedUrl = "http://127.0.0.1:" + getPort(proxy) + "/" + ProxyCacheUtils.encode(HTTP_DATA_URL);
assertThat(proxy.getProxyUrl(HTTP_DATA_URL)).isEqualTo(expectedUrl);
assertThat(proxy.getProxyUrl(HTTP_DATA_URL, true)).isEqualTo(expectedUrl);
assertThat(proxy.getProxyUrl(HTTP_DATA_URL, false)).isEqualTo(expectedUrl);
proxy.shutdown();
}
@Test
public void testGetProxiedUrlForPartialCache() throws Exception {
File cacheDir = RuntimeEnvironment.application.getExternalCacheDir();
File file = new File(cacheDir, new Md5FileNameGenerator().generate(HTTP_DATA_URL));
int partialCacheSize = 1000;
byte[] partialData = ProxyCacheTestUtils.generate(partialCacheSize);
File partialCacheFile = ProxyCacheTestUtils.getTempFile(file);
IoUtils.saveToFile(partialData, partialCacheFile);
HttpProxyCacheServer proxy = newProxy(cacheFolder);
String expectedUrl = "http://127.0.0.1:" + getPort(proxy) + "/" + ProxyCacheUtils.encode(HTTP_DATA_URL);
assertThat(proxy.getProxyUrl(HTTP_DATA_URL)).isEqualTo(expectedUrl);
assertThat(proxy.getProxyUrl(HTTP_DATA_URL, true)).isEqualTo(expectedUrl);
assertThat(proxy.getProxyUrl(HTTP_DATA_URL, false)).isEqualTo(expectedUrl);
proxy.shutdown();
}
@Test
public void testGetProxiedUrlForExistedCache() throws Exception {
HttpProxyCacheServer proxy = newProxy(cacheFolder);
readProxyResponse(proxy, HTTP_DATA_URL, 0);
String proxiedUrl = "http://127.0.0.1:" + getPort(proxy) + "/" + ProxyCacheUtils.encode(HTTP_DATA_URL);
File cachedFile = file(cacheFolder, HTTP_DATA_URL);
String cachedFileUri = Uri.fromFile(cachedFile).toString();
assertThat(proxy.getProxyUrl(HTTP_DATA_URL)).isEqualTo(cachedFileUri);
assertThat(proxy.getProxyUrl(HTTP_DATA_URL, true)).isEqualTo(cachedFileUri);
assertThat(proxy.getProxyUrl(HTTP_DATA_URL, false)).isEqualTo(proxiedUrl);
proxy.shutdown();
}
@Test
public void testTrimFileCacheForTotalCountLru() throws Exception {
FileNameGenerator fileNameGenerator = new Md5FileNameGenerator();
HttpProxyCacheServer proxy = new HttpProxyCacheServer.Builder(RuntimeEnvironment.application)
.cacheDirectory(cacheFolder)
.fileNameGenerator(fileNameGenerator)
.maxCacheFilesCount(2)
.build();
readProxyResponse(proxy, proxy.getProxyUrl(HTTP_DATA_URL), 0);
assertThat(new File(cacheFolder, fileNameGenerator.generate(HTTP_DATA_URL))).exists();
readProxyResponse(proxy, proxy.getProxyUrl(HTTP_DATA_URL_ONE_REDIRECT), 0);
assertThat(new File(cacheFolder, fileNameGenerator.generate(HTTP_DATA_URL_ONE_REDIRECT))).exists();
readProxyResponse(proxy, proxy.getProxyUrl(HTTP_DATA_URL_3_REDIRECTS), 0);
assertThat(new File(cacheFolder, fileNameGenerator.generate(HTTP_DATA_URL_3_REDIRECTS))).exists();
waitForAsyncTrimming();
assertThat(new File(cacheFolder, fileNameGenerator.generate(HTTP_DATA_URL))).doesNotExist();
}
@Test
public void testTrimFileCacheForTotalSizeLru() throws Exception {
FileNameGenerator fileNameGenerator = new Md5FileNameGenerator();
HttpProxyCacheServer proxy = new HttpProxyCacheServer.Builder(RuntimeEnvironment.application)
.cacheDirectory(cacheFolder)
.fileNameGenerator(fileNameGenerator)
.maxCacheSize(HTTP_DATA_SIZE * 3 - 1)
.build();
readProxyResponse(proxy, proxy.getProxyUrl(HTTP_DATA_URL), 0);
assertThat(new File(cacheFolder, fileNameGenerator.generate(HTTP_DATA_URL))).exists();
readProxyResponse(proxy, proxy.getProxyUrl(HTTP_DATA_URL_ONE_REDIRECT), 0);
assertThat(new File(cacheFolder, fileNameGenerator.generate(HTTP_DATA_URL_ONE_REDIRECT))).exists();
readProxyResponse(proxy, proxy.getProxyUrl(HTTP_DATA_URL_3_REDIRECTS), 0);
assertThat(new File(cacheFolder, fileNameGenerator.generate(HTTP_DATA_URL_3_REDIRECTS))).exists();
waitForAsyncTrimming();
assertThat(new File(cacheFolder, fileNameGenerator.generate(HTTP_DATA_URL))).doesNotExist();
}
@Test // https://github.com/danikula/AndroidVideoCache/issues/28
public void testWorkWithExternalProxy() throws Exception {
installExternalSystemProxy();
Pair<File, Response> response = readProxyData(HTTP_DATA_URL, 0);
assertThat(response.second.data).isEqualTo(loadAssetFile(ASSETS_DATA_NAME));
}
@Test // https://github.com/danikula/AndroidVideoCache/issues/28
public void testDoesNotWorkWithoutCustomProxySelector() throws Exception {
HttpProxyCacheServer httpProxyCacheServer = new HttpProxyCacheServer(RuntimeEnvironment.application);
// IgnoreHostProxySelector is set in HttpProxyCacheServer constructor. So let reset it by custom.
installExternalSystemProxy();
String proxiedUrl = httpProxyCacheServer.getProxyUrl(HTTP_DATA_URL);
// server can't proxy this url due to it is not alive (can't ping itself), so it returns original url
assertThat(proxiedUrl).isEqualTo(HTTP_DATA_URL);
}
private Pair<File, Response> readProxyData(String url, int offset) throws IOException {
File file = file(cacheFolder, url);
HttpProxyCacheServer proxy = newProxy(cacheFolder);
@@ -281,4 +385,8 @@ public class HttpProxyCacheServerTest {
.cacheDirectory(cacheDir)
.build();
}
private void waitForAsyncTrimming() throws InterruptedException {
Thread.sleep(500);
}
}

View File

@@ -6,14 +6,10 @@ import com.danikula.videocache.sourcestorage.SourceInfoStorage;
import com.danikula.videocache.sourcestorage.SourceInfoStorageFactory;
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.mockito.Mockito;
import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import java.io.ByteArrayOutputStream;
import java.io.File;
@@ -49,9 +45,7 @@ import static org.mockito.Mockito.when;
*
* @author Alexey Danilov (danikula@gmail.com).
*/
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class)
public class HttpProxyCacheTest {
public class HttpProxyCacheTest extends BaseTest {
@Test
public void testProcessRequestNoCache() throws Exception {

View File

@@ -3,14 +3,10 @@ package com.danikula.videocache;
import com.danikula.videocache.sourcestorage.SourceInfoStorage;
import com.danikula.videocache.sourcestorage.SourceInfoStorageFactory;
import com.danikula.videocache.support.ProxyCacheTestUtils;
import com.danikula.videocache.test.BuildConfig;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.annotation.Config;
import java.io.ByteArrayOutputStream;
import java.util.Arrays;
@@ -33,9 +29,7 @@ import static org.mockito.Matchers.any;
/**
* @author Alexey Danilov (danikula@gmail.com).
*/
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class)
public class HttpUrlSourceTest {
public class HttpUrlSourceTest extends BaseTest {
@Test
public void testHttpUrlSourceRange() throws Exception {
@@ -76,7 +70,7 @@ public class HttpUrlSourceTest {
public void testFetchInfoWithRedirect() throws Exception {
HttpUrlSource source = new HttpUrlSource(HTTP_DATA_URL_ONE_REDIRECT);
source.open(0);
int available = source.length();
long available = source.length();
String mime = source.getMime();
source.close();

View File

@@ -0,0 +1,82 @@
package com.danikula.videocache;
import org.junit.Before;
import org.junit.Test;
import org.robolectric.RuntimeEnvironment;
import java.io.ByteArrayOutputStream;
import java.net.Socket;
import static com.danikula.videocache.support.ProxyCacheTestUtils.getPort;
import static com.danikula.videocache.support.ProxyCacheTestUtils.getPortWithoutPing;
import static com.danikula.videocache.support.ProxyCacheTestUtils.installExternalSystemProxy;
import static com.danikula.videocache.support.ProxyCacheTestUtils.resetSystemProxy;
import static org.fest.assertions.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* Tests {@link Pinger}.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
public class PingerTest extends BaseTest {
@Before
public void setup() throws Exception {
resetSystemProxy();
}
@Test
public void testPingSuccess() throws Exception {
HttpProxyCacheServer server = new HttpProxyCacheServer(RuntimeEnvironment.application);
Pinger pinger = new Pinger("127.0.0.1", getPort(server));
boolean pinged = pinger.ping(1, 100);
assertThat(pinged).isTrue();
server.shutdown();
}
@Test
public void testPingFail() throws Exception {
Pinger pinger = new Pinger("127.0.0.1", 33);
boolean pinged = pinger.ping(3, 70);
assertThat(pinged).isFalse();
}
@Test
public void testIsPingRequest() throws Exception {
Pinger pinger = new Pinger("127.0.0.1", 1);
assertThat(pinger.isPingRequest("ping")).isTrue();
assertThat(pinger.isPingRequest("notPing")).isFalse();
}
@Test
public void testResponseToPing() throws Exception {
Pinger pinger = new Pinger("127.0.0.1", 1);
ByteArrayOutputStream out = new ByteArrayOutputStream();
Socket socket = mock(Socket.class);
when(socket.getOutputStream()).thenReturn(out);
pinger.responseToPing(socket);
assertThat(out.toString()).isEqualTo("HTTP/1.1 200 OK\n\nping ok");
}
@Test // https://github.com/danikula/AndroidVideoCache/issues/28
public void testPingedWithExternalProxy() throws Exception {
installExternalSystemProxy();
HttpProxyCacheServer server = new HttpProxyCacheServer(RuntimeEnvironment.application);
Pinger pinger = new Pinger("127.0.0.1", getPortWithoutPing(server));
assertThat(pinger.ping(1, 100)).isTrue();
}
@Test // https://github.com/danikula/AndroidVideoCache/issues/28
public void testIsNotPingedWithoutCustomProxySelector() throws Exception {
HttpProxyCacheServer server = new HttpProxyCacheServer(RuntimeEnvironment.application);
// IgnoreHostProxySelector is set in HttpProxyCacheServer constructor. So let reset it by custom.
installExternalSystemProxy();
Pinger pinger = new Pinger("127.0.0.1", getPortWithoutPing(server));
assertThat(pinger.ping(1, 100)).isFalse();
}
}

View File

@@ -3,12 +3,8 @@ package com.danikula.videocache;
import com.danikula.android.garden.io.IoUtils;
import com.danikula.videocache.file.FileCache;
import com.danikula.videocache.support.ProxyCacheTestUtils;
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;
@@ -28,9 +24,7 @@ import static org.fest.assertions.api.Assertions.assertThat;
/**
* @author Alexey Danilov (danikula@gmail.com).
*/
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class)
public class ProxyCacheTest {
public class ProxyCacheTest extends BaseTest {
@Test
public void testNoCache() throws Exception {
@@ -102,7 +96,6 @@ public class ProxyCacheTest {
assertThat(readData).isEqualTo(sourceData);
}
@Test
public void testReadEnd() throws Exception {
int capacity = 5323;

View File

@@ -0,0 +1,54 @@
package com.danikula.videocache;
import com.google.common.collect.Lists;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.ProxySelector;
import java.net.URI;
import java.util.List;
import static com.danikula.videocache.support.ProxyCacheTestUtils.resetSystemProxy;
import static org.fest.assertions.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
/**
* Tests {@link IgnoreHostProxySelector}.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
public class ProxySelectorTest extends BaseTest {
@Before
public void setup() throws Exception {
resetSystemProxy();
}
@Test // https://github.com/danikula/AndroidVideoCache/issues/28
public void testIgnoring() throws Exception {
InetSocketAddress proxyAddress = new InetSocketAddress("proxy.com", 80);
Proxy systemProxy = new Proxy(Proxy.Type.HTTP, proxyAddress);
ProxySelector mockedProxySelector = Mockito.mock(ProxySelector.class);
when(mockedProxySelector.select(Mockito.<URI>any())).thenReturn(Lists.newArrayList(systemProxy));
ProxySelector.setDefault(mockedProxySelector);
IgnoreHostProxySelector.install("localhost", 42);
ProxySelector proxySelector = ProxySelector.getDefault();
List<Proxy> githubProxies = proxySelector.select(new URI("http://github.com"));
assertThat(githubProxies).hasSize(1);
assertThat(githubProxies.get(0).address()).isEqualTo(proxyAddress);
List<Proxy> localhostProxies = proxySelector.select(new URI("http://localhost:42"));
assertThat(localhostProxies).hasSize(1);
assertThat(localhostProxies.get(0)).isEqualTo(Proxy.NO_PROXY);
List<Proxy> localhostPort69Proxies = proxySelector.select(new URI("http://localhost:69"));
assertThat(localhostPort69Proxies).hasSize(1);
assertThat(localhostPort69Proxies.get(0).address()).isEqualTo(proxyAddress);
}
}

View File

@@ -1,13 +1,10 @@
package com.danikula.videocache.file;
import com.danikula.videocache.BaseTest;
import com.danikula.videocache.support.ProxyCacheTestUtils;
import com.danikula.videocache.test.BuildConfig;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.annotation.Config;
import java.io.File;
import java.io.IOException;
@@ -21,9 +18,7 @@ import static org.fest.assertions.api.Assertions.assertThat;
*
* @author Alexey Danilov (danikula@gmail.com).
*/
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class)
public class DiskUsageTest {
public class DiskUsageTest extends BaseTest {
private File cacheFolder;

View File

@@ -1,19 +1,16 @@
package com.danikula.videocache.file;
import com.danikula.android.garden.io.Files;
import com.danikula.android.garden.io.IoUtils;
import com.danikula.videocache.BaseTest;
import com.danikula.videocache.Cache;
import com.danikula.videocache.ProxyCacheException;
import com.danikula.videocache.test.BuildConfig;
import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricGradleTestRunner;
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_NAME;
@@ -22,14 +19,13 @@ 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 com.google.common.io.Files.write;
import static org.fest.assertions.api.Assertions.assertThat;
/**
* @author Alexey Danilov (danikula@gmail.com).
*/
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class)
public class FileCacheTest {
public class FileCacheTest extends BaseTest {
@Test
public void testWriteReadDiscCache() throws Exception {
@@ -90,7 +86,7 @@ public class FileCacheTest {
fileCache.read(readData, firstPortionLength, secondPortionLength);
assertThat(readData).isEqualTo(wroteSecondPortion);
readData = new byte[fileCache.available()];
readData = new byte[(int)fileCache.available()];
fileCache.read(readData, 0, readData.length);
byte[] fileContent = getFileContent(getTempFile(file));
assertThat(readData).isEqualTo(fileContent);
@@ -100,7 +96,8 @@ public class FileCacheTest {
public void testIsFileCacheCompleted() throws Exception {
File file = newCacheFile();
File partialFile = new File(file.getParentFile(), file.getName() + ".download");
IoUtils.saveToFile(loadAssetFile(ASSETS_DATA_NAME), partialFile);
write(loadAssetFile(ASSETS_DATA_NAME), partialFile);
write(loadAssetFile(ASSETS_DATA_NAME), partialFile);
Cache fileCache = new FileCache(partialFile);
assertThat(file.exists()).isFalse();
@@ -119,7 +116,7 @@ public class FileCacheTest {
@Test(expected = ProxyCacheException.class)
public void testErrorWritingCompletedCache() throws Exception {
File file = newCacheFile();
IoUtils.saveToFile(loadAssetFile(ASSETS_DATA_NAME), file);
write(loadAssetFile(ASSETS_DATA_NAME), file);
FileCache fileCache = new FileCache(file);
fileCache.append(generate(100), 20);
Assert.fail();
@@ -129,7 +126,7 @@ public class FileCacheTest {
public void testErrorWritingAfterCompletion() throws Exception {
File file = newCacheFile();
File partialFile = new File(file.getParentFile(), file.getName() + ".download");
IoUtils.saveToFile(loadAssetFile(ASSETS_DATA_NAME), partialFile);
write(loadAssetFile(ASSETS_DATA_NAME), partialFile);
FileCache fileCache = new FileCache(partialFile);
fileCache.complete();
fileCache.append(generate(100), 20);
@@ -145,4 +142,46 @@ public class FileCacheTest {
fileCache.available();
Assert.fail();
}
@Test
public void testTrimAfterCompletionForTotalCountLru() throws Exception {
File cacheDir = newCacheFile();
DiskUsage diskUsage = new TotalCountLruDiskUsage(2);
byte[] data = loadAssetFile(ASSETS_DATA_NAME);
saveAndCompleteCache(diskUsage, data,
new File(cacheDir, "0.dat"),
new File(cacheDir, "1.dat"),
new File(cacheDir, "2.dat")
);
waitForAsyncTrimming();
assertThat(new File(cacheDir, "0.dat")).doesNotExist();
}
@Test
public void testTrimAfterCompletionForTotalSizeLru() throws Exception {
File cacheDir = newCacheFile();
byte[] data = loadAssetFile(ASSETS_DATA_NAME);
DiskUsage diskUsage = new TotalSizeLruDiskUsage(data.length * 3 - 1);
saveAndCompleteCache(diskUsage, data,
new File(cacheDir, "0.dat"),
new File(cacheDir, "1.dat"),
new File(cacheDir, "2.dat")
);
waitForAsyncTrimming();
File deletedFile = new File(cacheDir, "0.dat");
assertThat(deletedFile).doesNotExist();
}
private void saveAndCompleteCache(DiskUsage diskUsage, byte[] data, File... files) throws ProxyCacheException, IOException {
for (File file : files) {
FileCache fileCache = new FileCache(file, diskUsage);
fileCache.append(data, data.length);
fileCache.complete();
assertThat(file).exists();
}
}
private void waitForAsyncTrimming() throws InterruptedException {
Thread.sleep(100);
}
}

View File

@@ -1,12 +1,9 @@
package com.danikula.videocache.file;
import com.danikula.videocache.BaseTest;
import com.danikula.videocache.support.ProxyCacheTestUtils;
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;
@@ -17,9 +14,7 @@ import static org.fest.assertions.api.Assertions.assertThat;
*
* @author Alexey Danilov (danikula@gmail.com).
*/
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class)
public class FilesTest {
public class FilesTest extends BaseTest {
@Test
public void testModify() throws Exception {

View File

@@ -1,15 +1,12 @@
package com.danikula.videocache.sourcestorage;
import com.danikula.videocache.BaseTest;
import com.danikula.videocache.SourceInfo;
import com.danikula.videocache.test.BuildConfig;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import static org.fest.assertions.api.Assertions.assertThat;
import static org.fest.assertions.api.Assertions.fail;
@@ -19,9 +16,7 @@ import static org.fest.assertions.api.Assertions.fail;
*
* @author Alexey Danilov (danikula@gmail.com).
*/
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class)
public class SourceInfoStorageTest {
public class SourceInfoStorageTest extends BaseTest {
private SourceInfoStorage storage;

View File

@@ -7,8 +7,11 @@ import com.danikula.videocache.HttpUrlSource;
import com.danikula.videocache.ProxyCacheException;
import com.danikula.videocache.Source;
import com.danikula.videocache.sourcestorage.SourceInfoStorage;
import com.google.common.collect.Lists;
import com.google.common.io.Files;
import org.apache.tools.ant.util.ReflectUtil;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.robolectric.RuntimeEnvironment;
@@ -18,16 +21,24 @@ import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.ProxySelector;
import java.net.URI;
import java.net.URL;
import java.util.Random;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.fest.assertions.api.Assertions.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
/**
* @author Alexey Danilov (danikula@gmail.com).
@@ -54,9 +65,9 @@ public class ProxyCacheTestUtils {
}
public static Response readProxyResponse(HttpProxyCacheServer proxy, String url, int offset) throws IOException {
String proxyUrl = proxy.getProxyUrl(url);
String proxyUrl = proxy.getProxyUrl(url, false);
if (!proxyUrl.startsWith("http://127.0.0.1")) {
throw new IllegalStateException("Url " + url + " is not proxied!");
throw new IllegalStateException("Proxy url " + proxyUrl + " is not proxied! Original url is " + url);
}
URL proxiedUrl = new URL(proxyUrl);
HttpURLConnection connection = (HttpURLConnection) proxiedUrl.openConnection();
@@ -135,4 +146,31 @@ public class ProxyCacheTestUtils {
}).doCallRealMethod().when(spySource).read(any(byte[].class));
return spySource;
}
public static int getPort(HttpProxyCacheServer server) {
String proxyUrl = server.getProxyUrl("test");
Pattern pattern = Pattern.compile("http://127.0.0.1:(\\d*)/test");
Matcher matcher = pattern.matcher(proxyUrl);
assertThat(matcher.find()).isTrue();
String portAsString = matcher.group(1);
return Integer.parseInt(portAsString);
}
public static int getPortWithoutPing(HttpProxyCacheServer server) {
return (Integer) ReflectUtil.getField(server, "port");
}
public static void installExternalSystemProxy() {
// see proxies list at http://proxylist.hidemyass.com/
Proxy systemProxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("162.8.230.7", 11180));
ProxySelector mockedProxySelector = Mockito.mock(ProxySelector.class);
when(mockedProxySelector.select(Mockito.<URI>any())).thenReturn(Lists.newArrayList(systemProxy));
ProxySelector.setDefault(mockedProxySelector);
}
public static void resetSystemProxy() {
ProxySelector mockedProxySelector = Mockito.mock(ProxySelector.class);
when(mockedProxySelector.select(Mockito.<URI>any())).thenReturn(Lists.newArrayList(Proxy.NO_PROXY));
ProxySelector.setDefault(mockedProxySelector);
}
}