mirror of
https://github.com/zhigang1992/AndroidVideoCache.git
synced 2026-06-15 18:07:51 +08:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0264944a20 | ||
|
|
cf69365920 | ||
|
|
14ac4a98b1 | ||
|
|
c5cf254b74 | ||
|
|
e4ab124d57 | ||
|
|
e7f471983d | ||
|
|
d32e88f641 | ||
|
|
55988e278d | ||
|
|
e06e45c75c | ||
|
|
1d3230ba57 | ||
|
|
fe9af27f96 | ||
|
|
9ffa983f58 | ||
|
|
6125478d27 | ||
|
|
e135bf0b42 | ||
|
|
edb12b574f | ||
|
|
63ee20f93c | ||
|
|
88da0aa5c0 | ||
|
|
3d50eb64c3 | ||
|
|
f4b9e5c8f5 | ||
|
|
15c5388f6c | ||
|
|
8263814aea | ||
|
|
1c7cb32a97 | ||
|
|
f8f19c5a5c | ||
|
|
c0ef7dd1b1 | ||
|
|
5115b96a9e | ||
|
|
7f22a66941 | ||
|
|
983ae8d7b7 | ||
|
|
492706ce69 | ||
|
|
a57acc1ad5 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,6 +4,7 @@
|
||||
|
||||
/.idea
|
||||
/build
|
||||
/publish.sh
|
||||
/local.properties
|
||||
/gradle.properties
|
||||
/library/build
|
||||
|
||||
100
README.md
100
README.md
@@ -2,52 +2,108 @@
|
||||
[](http://android-arsenal.com/details/1/1751)
|
||||
|
||||
## Why AndroidVideoCache?
|
||||
Because android MediaPlayer doesn't cache video while streaming.
|
||||
Because there is no sense to download video a lot of times while streaming!
|
||||
`AndroidVideoCache` allows to add caching support to your `VideoView/MediaPlayer`, [ExoPlayer](https://github.com/danikula/ExoPlayer/commit/6110be8559f003f98020ada8c5e09691b67aaff4) or any another player with help of single line!
|
||||
|
||||
## Features
|
||||
- caching to disk during streaming;
|
||||
- offline work with cached resources;
|
||||
- partial loading;
|
||||
- cache limits (max cache size, max files count);
|
||||
- multiple clients for same url.
|
||||
|
||||
Note `AndroidVideoCache` works only with **direct urls** to media file, it [**doesn't support**](https://github.com/danikula/AndroidVideoCache/issues/19) any streaming technology like DASH, SmoothStreaming, HLS.
|
||||
|
||||
## How to use?
|
||||
Just add link to repository and dependency:
|
||||
Just add dependency (`AndroidVideoCache` is available in jcenter):
|
||||
```
|
||||
repositories {
|
||||
maven { url 'https://dl.bintray.com/alexeydanilov/maven' }
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
compile 'com.danikula:videocache:1.0.1'
|
||||
compile 'com.danikula:videocache:2.3.2'
|
||||
}
|
||||
```
|
||||
|
||||
and use proxy for caching video:
|
||||
and use url from proxy instead of original url for adding caching:
|
||||
|
||||
```java
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
...
|
||||
try {
|
||||
Cache cache = new FileCache(new File(getExternalCacheDir(), VIDEO_CACHE_NAME));
|
||||
HttpUrlSource source = new HttpUrlSource(VIDEO_URL);
|
||||
proxyCache = new HttpProxyCache(source, cache);
|
||||
videoView.setVideoPath(proxyCache.getUrl());
|
||||
videoView.start();
|
||||
} catch (ProxyCacheException e) {
|
||||
Log.e(LOG_TAG, "Error playing video", e);
|
||||
}
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
HttpProxyCacheServer proxy = getProxy();
|
||||
String proxyUrl = proxy.getProxyUrl(VIDEO_URL);
|
||||
videoView.setVideoPath(proxyUrl);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
private HttpProxyCacheServer getProxy() {
|
||||
// should return single instance of HttpProxyCacheServer shared for whole app.
|
||||
}
|
||||
```
|
||||
|
||||
if (proxyCache != null) {
|
||||
proxyCache.shutdown();
|
||||
To guarantee normal work you should use **single** instance of `HttpProxyCacheServer` for whole app.
|
||||
For example you can store shared proxy in your `Application`:
|
||||
|
||||
```java
|
||||
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() {
|
||||
return new HttpProxyCacheServer(this);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
or use [simple factory](http://pastebin.com/s2fafSYS).
|
||||
More preferable way is use some dependency injector like [Dagger](http://square.github.io/dagger/).
|
||||
|
||||
By default `HttpProxyCacheServer` uses 512Mb for caching files. You can change this value:
|
||||
|
||||
```java
|
||||
private HttpProxyCacheServer newProxy() {
|
||||
return new HttpProxyCacheServer.Builder(this)
|
||||
.maxCacheSize(1024 * 1024 * 1024) // 1 Gb for cache
|
||||
.build();
|
||||
}
|
||||
```
|
||||
|
||||
or can limit total count of files in cache:
|
||||
|
||||
```java
|
||||
private HttpProxyCacheServer newProxy() {
|
||||
return new HttpProxyCacheServer.Builder(this)
|
||||
.maxCacheFilesCount(20)
|
||||
.build();
|
||||
}
|
||||
```
|
||||
|
||||
See `sample` app for details.
|
||||
|
||||
## Whats new
|
||||
See Release Notes [here](https://github.com/danikula/AndroidVideoCache/releases)
|
||||
|
||||
## Code contributions
|
||||
If it's a feature that you think would need to be discussed please open an issue first, otherwise, you can follow this process:
|
||||
|
||||
1. [Fork the project](http://help.github.com/fork-a-repo/)
|
||||
2. Create a feature branch (git checkout -b my_branch)
|
||||
3. Push your changes to your new branch (git push origin my_branch)
|
||||
4. Initiate a [pull request](http://help.github.com/send-pull-requests/) on github
|
||||
5. Your pull request will be reviewed and hopefully merged :)
|
||||
|
||||
## Where published?
|
||||
[Here](https://bintray.com/alexeydanilov/maven/videocache/view)
|
||||
|
||||
## Questions?
|
||||
[danikula@gmail.com](mailto:danikula@gmail.com)
|
||||
|
||||
## License
|
||||
|
||||
Copyright 2014-2015 Alexey Danilov
|
||||
@@ -62,4 +118,4 @@ See `sample` app for details.
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
limitations under the License.
|
||||
|
||||
@@ -3,12 +3,12 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:1.2.3'
|
||||
classpath 'com.android.tools.build:gradle:1.3.0'
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
}
|
||||
}
|
||||
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-2.4-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
|
||||
|
||||
@@ -7,9 +7,17 @@ buildscript {
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: 'idea'
|
||||
apply plugin: 'java'
|
||||
apply plugin: 'bintray-release'
|
||||
|
||||
idea {
|
||||
module {
|
||||
downloadJavadoc = true
|
||||
downloadSources = true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile 'com.google.android:android:1.6_r2'
|
||||
}
|
||||
@@ -18,7 +26,7 @@ publish {
|
||||
userOrg = 'alexeydanilov'
|
||||
groupId = 'com.danikula'
|
||||
artifactId = 'videocache'
|
||||
publishVersion = '1.0.1'
|
||||
publishVersion = '2.3.2'
|
||||
description = 'Cache support for android VideoView'
|
||||
website = 'https://github.com/danikula/AndroidVideoCache'
|
||||
}
|
||||
|
||||
@@ -3,9 +3,6 @@ package com.danikula.videocache;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.util.Arrays;
|
||||
|
||||
import static com.danikula.videocache.Preconditions.checkArgument;
|
||||
import static com.danikula.videocache.Preconditions.checkNotNull;
|
||||
|
||||
/**
|
||||
* Simple memory based {@link Cache} implementation.
|
||||
*
|
||||
@@ -24,7 +21,6 @@ public class ByteArrayCache implements Cache {
|
||||
this.data = Preconditions.checkNotNull(data);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int read(byte[] buffer, long offset, int length) throws ProxyCacheException {
|
||||
if (offset >= data.length) {
|
||||
|
||||
@@ -22,7 +22,7 @@ public class ByteArraySource implements Source {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int available() throws ProxyCacheException {
|
||||
public int length() throws ProxyCacheException {
|
||||
return data.length;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
package com.danikula.videocache;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* @author Egor Makovsky (yahor.makouski@gmail.com).
|
||||
* Listener for cache availability.
|
||||
*
|
||||
* @author Egor Makovsky (yahor.makouski@gmail.com)
|
||||
* @author Alexey Danilov (danikula@gmail.com).
|
||||
*/
|
||||
public interface CacheListener {
|
||||
void onError(ProxyCacheException e);
|
||||
|
||||
void onCacheDataAvailable(int cachePercentage);
|
||||
void onCacheAvailable(File cacheFile, String url, int percentsAvailable);
|
||||
}
|
||||
|
||||
30
library/src/main/java/com/danikula/videocache/Config.java
Normal file
30
library/src/main/java/com/danikula/videocache/Config.java
Normal file
@@ -0,0 +1,30 @@
|
||||
package com.danikula.videocache;
|
||||
|
||||
import com.danikula.videocache.file.DiskUsage;
|
||||
import com.danikula.videocache.file.FileNameGenerator;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* Configuration for proxy cache.
|
||||
*
|
||||
* @author Alexey Danilov (danikula@gmail.com).
|
||||
*/
|
||||
class Config {
|
||||
|
||||
public final File cacheRoot;
|
||||
public final FileNameGenerator fileNameGenerator;
|
||||
public final DiskUsage diskUsage;
|
||||
|
||||
Config(File cacheRoot, FileNameGenerator fileNameGenerator, DiskUsage diskUsage) {
|
||||
this.cacheRoot = cacheRoot;
|
||||
this.fileNameGenerator = fileNameGenerator;
|
||||
this.diskUsage = diskUsage;
|
||||
}
|
||||
|
||||
File generateCacheFile(String url) {
|
||||
String name = fileNameGenerator.generate(url);
|
||||
return new File(cacheRoot, name);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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{" +
|
||||
"rangeOffset=" + rangeOffset +
|
||||
", partial=" + partial +
|
||||
", uri='" + uri + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -1,240 +1,107 @@
|
||||
package com.danikula.videocache;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import com.danikula.videocache.file.FileCache;
|
||||
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static com.danikula.videocache.ProxyCacheUtils.DEFAULT_BUFFER_SIZE;
|
||||
|
||||
/**
|
||||
* {@link ProxyCache} that uses local server to handle requests and cache data.
|
||||
* Typical usage:
|
||||
* <pre><code>
|
||||
* HttpProxyCache proxy;
|
||||
* public onCreate(Bundle state) {
|
||||
* super.onCreate(state);
|
||||
* ...
|
||||
* try{
|
||||
* HttpUrlSource source = new HttpUrlSource(YOUR_VIDEO_URI);
|
||||
* Cache cache = new FileCache(new File(context.getCacheDir(), "video.mp4"));
|
||||
* proxy = new HttpProxyCache(source, cache);
|
||||
* videoView.setVideoPath(proxy.getUrl());
|
||||
* } catch(ProxyCacheException e) {
|
||||
* Log.e(LOG_TAG, "Error playing video", e);
|
||||
* }
|
||||
* }
|
||||
* public onDestroy(){
|
||||
* super.onDestroy();
|
||||
*
|
||||
* if (proxy != null) {
|
||||
* proxy.shutdown();
|
||||
* }
|
||||
* }
|
||||
* <code/></pre>
|
||||
* {@link ProxyCache} that read http url and writes data to {@link Socket}
|
||||
*
|
||||
* @author Alexey Danilov (danikula@gmail.com).
|
||||
*/
|
||||
public class HttpProxyCache extends ProxyCache {
|
||||
class HttpProxyCache extends ProxyCache {
|
||||
|
||||
private static final int CLIENT_COUNT = 3;
|
||||
private static final Pattern RANGE_HEADER_PATTERN = Pattern.compile("[R,r]ange:[ ]?bytes=(\\d*)-");
|
||||
private static final String PROXY_HOST = "127.0.0.1";
|
||||
private static final float NO_CACHE_BARRIER = .2f;
|
||||
|
||||
private final HttpUrlSource httpUrlSource;
|
||||
private final Cache cache;
|
||||
private final ServerSocket serverSocket;
|
||||
private final int port;
|
||||
private final Thread waitConnectionThread;
|
||||
private final ExecutorService executorService;
|
||||
private final HttpUrlSource source;
|
||||
private final FileCache cache;
|
||||
private CacheListener listener;
|
||||
|
||||
public HttpProxyCache(HttpUrlSource source, Cache cache, boolean logEnabled) throws ProxyCacheException {
|
||||
super(source, cache, logEnabled);
|
||||
|
||||
this.httpUrlSource = source;
|
||||
public HttpProxyCache(HttpUrlSource source, FileCache cache) {
|
||||
super(source, cache);
|
||||
this.cache = cache;
|
||||
this.executorService = Executors.newFixedThreadPool(CLIENT_COUNT);
|
||||
try {
|
||||
InetAddress inetAddress = InetAddress.getByName(PROXY_HOST);
|
||||
this.serverSocket = new ServerSocket(0, CLIENT_COUNT, inetAddress);
|
||||
this.port = serverSocket.getLocalPort();
|
||||
CountDownLatch startSignal = new CountDownLatch(1);
|
||||
this.waitConnectionThread = new Thread(new WaitRequestsRunnable(startSignal));
|
||||
this.waitConnectionThread.start();
|
||||
startSignal.await(); // freeze thread, wait for server starts
|
||||
} catch (IOException | InterruptedException e) {
|
||||
executorService.shutdown();
|
||||
throw new ProxyCacheException("Error starting local server", e);
|
||||
}
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
public HttpProxyCache(HttpUrlSource source, Cache cache) throws ProxyCacheException {
|
||||
this(source, cache, false);
|
||||
public void registerCacheListener(CacheListener cacheListener) {
|
||||
this.listener = cacheListener;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return "http://" + PROXY_HOST + ":" + port + Uri.parse(httpUrlSource.url).getPath();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
super.shutdown();
|
||||
|
||||
Log.i(ProxyCacheUtils.LOG_TAG, "Shutdown proxy");
|
||||
waitConnectionThread.interrupt();
|
||||
try {
|
||||
if (!serverSocket.isClosed()) {
|
||||
serverSocket.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
onError(new ProxyCacheException("Error shutting down local server", e));
|
||||
}
|
||||
}
|
||||
|
||||
private void waitForRequest() {
|
||||
try {
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
Socket socket = serverSocket.accept();
|
||||
Log.d(ProxyCacheUtils.LOG_TAG, "Accept new socket " + socket);
|
||||
processSocketInBackground(socket);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
onError(new ProxyCacheException("Error during waiting connection", e));
|
||||
}
|
||||
}
|
||||
|
||||
private void processSocketInBackground(final Socket socket) throws IOException {
|
||||
executorService.submit(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
processSocket(socket);
|
||||
} catch (Throwable e) {
|
||||
onError(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void processSocket(Socket socket) {
|
||||
try {
|
||||
InputStream inputStream = socket.getInputStream();
|
||||
String request = readRequest(inputStream);
|
||||
Log.i(ProxyCacheUtils.LOG_TAG, "Request to cache proxy:\n" + request);
|
||||
long rangeOffset = getRangeOffset(request);
|
||||
writeResponse(socket, rangeOffset);
|
||||
} catch (ProxyCacheException | IOException e) {
|
||||
onError(new ProxyCacheException("Error processing request", e));
|
||||
} finally {
|
||||
releaseSocket(socket);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeResponse(Socket socket, long rangeOffset) throws ProxyCacheException, IOException {
|
||||
public void processRequest(GetRequest request, Socket socket) throws IOException, ProxyCacheException {
|
||||
OutputStream out = new BufferedOutputStream(socket.getOutputStream());
|
||||
byte[] buffer = new byte[ProxyCacheUtils.DEFAULT_BUFFER_SIZE];
|
||||
int readBytes;
|
||||
long offset = Math.max(rangeOffset, 0);
|
||||
boolean headersWrote = false;
|
||||
while ((readBytes = read(buffer, offset, buffer.length)) != -1) {
|
||||
// tiny optimization: to prevent HEAD request in source for content-length. content-length 'll available after reading source
|
||||
if (!headersWrote) {
|
||||
writeResponseHeaders(out, rangeOffset);
|
||||
headersWrote = true;
|
||||
}
|
||||
out.write(buffer, 0, readBytes);
|
||||
if (isLogEnabled()) {
|
||||
Log.d(ProxyCacheUtils.LOG_TAG, "Write data[" + readBytes + " bytes] to socket " + socket + " with offset " + offset + ": " + ProxyCacheUtils.preview(buffer, readBytes));
|
||||
}
|
||||
offset += readBytes;
|
||||
}
|
||||
out.flush();
|
||||
}
|
||||
|
||||
private void writeResponseHeaders(OutputStream out, long rangeOffset) throws IOException, ProxyCacheException {
|
||||
String responseHeaders = newResponseHeaders(rangeOffset);
|
||||
String responseHeaders = newResponseHeaders(request);
|
||||
out.write(responseHeaders.getBytes("UTF-8"));
|
||||
Log.i(ProxyCacheUtils.LOG_TAG, "Response headers:\n" + responseHeaders);
|
||||
|
||||
long offset = request.rangeOffset;
|
||||
if (isUseCache(request)) {
|
||||
responseWithCache(out, offset);
|
||||
} else {
|
||||
responseWithoutCache(out, offset);
|
||||
}
|
||||
}
|
||||
|
||||
private String newResponseHeaders(long offset) throws IOException, ProxyCacheException {
|
||||
boolean partial = offset >= 0;
|
||||
String mime = httpUrlSource.getMime();
|
||||
private boolean isUseCache(GetRequest request) throws ProxyCacheException {
|
||||
int sourceLength = source.length();
|
||||
boolean sourceLengthKnown = sourceLength > 0;
|
||||
int 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;
|
||||
}
|
||||
|
||||
private String newResponseHeaders(GetRequest request) throws IOException, ProxyCacheException {
|
||||
String mime = source.getMime();
|
||||
boolean mimeKnown = !TextUtils.isEmpty(mime);
|
||||
int length = cache.isCompleted() ? cache.available() : httpUrlSource.available();
|
||||
int length = cache.isCompleted() ? cache.available() : source.length();
|
||||
boolean lengthKnown = length >= 0;
|
||||
long contentLength = partial ? length - offset : length;
|
||||
long contentLength = request.partial ? length - request.rangeOffset : length;
|
||||
boolean addRange = lengthKnown && request.partial;
|
||||
return new StringBuilder()
|
||||
.append(partial ? "HTTP/1.1 206 PARTIAL CONTENT\n" : "HTTP/1.1 200 OK\n")
|
||||
.append(request.partial ? "HTTP/1.1 206 PARTIAL CONTENT\n" : "HTTP/1.1 200 OK\n")
|
||||
.append("Accept-Ranges: bytes\n")
|
||||
.append(lengthKnown ? String.format("Content-Length: %d\n", contentLength) : "")
|
||||
.append(lengthKnown && partial ? String.format("Content-Range: bytes %d-%d/%d\n", offset, length, length) : "")
|
||||
.append(addRange ? String.format("Content-Range: bytes %d-%d/%d\n", request.rangeOffset, length - 1, length) : "")
|
||||
.append(mimeKnown ? String.format("Content-Type: %s\n", mime) : "")
|
||||
.append("\n") // headers end
|
||||
.toString();
|
||||
}
|
||||
|
||||
private String readRequest(InputStream inputStream) throws IOException {
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
|
||||
StringBuilder str = new StringBuilder();
|
||||
String line;
|
||||
while (!TextUtils.isEmpty(line = reader.readLine())) { // until new line (headers ending)
|
||||
str.append(line).append('\n');
|
||||
private void responseWithCache(OutputStream out, long offset) throws ProxyCacheException, IOException {
|
||||
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
|
||||
int readBytes;
|
||||
while ((readBytes = read(buffer, offset, buffer.length)) != -1) {
|
||||
out.write(buffer, 0, readBytes);
|
||||
offset += readBytes;
|
||||
}
|
||||
return str.toString();
|
||||
out.flush();
|
||||
}
|
||||
|
||||
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) {
|
||||
private void responseWithoutCache(OutputStream out, long offset) throws ProxyCacheException, IOException {
|
||||
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));
|
||||
HttpUrlSource source = new HttpUrlSource(this.source);
|
||||
source.open((int) offset);
|
||||
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
|
||||
int readBytes;
|
||||
while ((readBytes = source.read(buffer)) != -1) {
|
||||
out.write(buffer, 0, readBytes);
|
||||
offset += readBytes;
|
||||
}
|
||||
out.flush();
|
||||
} finally {
|
||||
source.close();
|
||||
}
|
||||
}
|
||||
|
||||
private final class WaitRequestsRunnable implements Runnable {
|
||||
|
||||
private final CountDownLatch startSignal;
|
||||
|
||||
public WaitRequestsRunnable(CountDownLatch startSignal) {
|
||||
this.startSignal = startSignal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
startSignal.countDown();
|
||||
waitForRequest();
|
||||
@Override
|
||||
protected void onCachePercentsAvailableChanged(int percents) {
|
||||
if (listener != null) {
|
||||
listener.onCacheAvailable(cache.file, source.url, percents);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,432 @@
|
||||
package com.danikula.videocache;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
|
||||
import com.danikula.videocache.file.DiskUsage;
|
||||
import com.danikula.videocache.file.FileNameGenerator;
|
||||
import com.danikula.videocache.file.Md5FileNameGenerator;
|
||||
import com.danikula.videocache.file.TotalCountLruDiskUsage;
|
||||
import com.danikula.videocache.file.TotalSizeLruDiskUsage;
|
||||
|
||||
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.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.
|
||||
* 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 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);
|
||||
private final Map<String, HttpProxyCacheServerClients> clientsMap = new ConcurrentHashMap<>();
|
||||
private final ServerSocket serverSocket;
|
||||
private final int port;
|
||||
private final Thread waitConnectionThread;
|
||||
private final Config config;
|
||||
private boolean pinged;
|
||||
|
||||
public HttpProxyCacheServer(Context context) {
|
||||
this(new Builder(context).buildConfig());
|
||||
}
|
||||
|
||||
private HttpProxyCacheServer(Config config) {
|
||||
this.config = checkNotNull(config);
|
||||
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
|
||||
Log.i(LOG_TAG, "Proxy cache server started. Ping it...");
|
||||
makeSureServerWorks();
|
||||
} catch (IOException | InterruptedException e) {
|
||||
socketProcessor.shutdown();
|
||||
throw new IllegalStateException("Error starting local proxy server", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void makeSureServerWorks() {
|
||||
int maxPingAttempts = 3;
|
||||
int delay = 200;
|
||||
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 [attempt: " + pingAttempts + ", timeout: " + delay + "]. " +
|
||||
"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();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private String appendToProxyUrl(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);
|
||||
if (PING_REQUEST.equals(url)) {
|
||||
responseToPing(socket);
|
||||
} else {
|
||||
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, "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());
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
if (clients == null) {
|
||||
clients = new HttpProxyCacheServerClients(url, config);
|
||||
clientsMap.put(url, clients);
|
||||
}
|
||||
return clients;
|
||||
}
|
||||
}
|
||||
|
||||
private int getClientsCount() {
|
||||
synchronized (clientsLock) {
|
||||
int count = 0;
|
||||
for (HttpProxyCacheServerClients clients : clientsMap.values()) {
|
||||
count += clients.getClientsCount();
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
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, "Releasing input stream… Socket is closed by client.");
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
private class PingCallable implements Callable<Boolean> {
|
||||
|
||||
@Override
|
||||
public Boolean call() throws Exception {
|
||||
return pingServer();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder for {@link HttpProxyCacheServer}.
|
||||
*/
|
||||
public static final class Builder {
|
||||
|
||||
private static final long DEFAULT_MAX_SIZE = 512 * 104 * 1024;
|
||||
|
||||
private File cacheRoot;
|
||||
private FileNameGenerator fileNameGenerator;
|
||||
private DiskUsage diskUsage;
|
||||
|
||||
public Builder(Context context) {
|
||||
this.cacheRoot = StorageUtils.getIndividualCacheDirectory(context);
|
||||
this.diskUsage = new TotalSizeLruDiskUsage(DEFAULT_MAX_SIZE);
|
||||
this.fileNameGenerator = new Md5FileNameGenerator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides default cache folder to be used for caching files.
|
||||
* <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/>
|
||||
* <b>Note</b> directory must be used <b>only</b> for AndroidVideoCache files.
|
||||
*
|
||||
* @param file a cache directory, can't be null.
|
||||
* @return a builder.
|
||||
*/
|
||||
public Builder cacheDirectory(File file) {
|
||||
this.cacheRoot = checkNotNull(file);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides default cache file name generator {@link Md5FileNameGenerator} .
|
||||
*
|
||||
* @param fileNameGenerator a new file name generator.
|
||||
* @return a builder.
|
||||
*/
|
||||
public Builder fileNameGenerator(FileNameGenerator fileNameGenerator) {
|
||||
this.fileNameGenerator = checkNotNull(fileNameGenerator);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets max cache size in bytes.
|
||||
* All files that exceeds limit will be deleted using LRU strategy.
|
||||
* Default value is 512 Mb.
|
||||
* <p/>
|
||||
* Note this method overrides result of calling {@link #maxCacheFilesCount(int)}
|
||||
*
|
||||
* @param maxSize max cache size in bytes.
|
||||
* @return a builder.
|
||||
*/
|
||||
public Builder maxCacheSize(long maxSize) {
|
||||
this.diskUsage = new TotalSizeLruDiskUsage(maxSize);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @return a builder.
|
||||
*/
|
||||
public Builder maxCacheFilesCount(int count) {
|
||||
this.diskUsage = new TotalCountLruDiskUsage(count);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds new instance of {@link HttpProxyCacheServer}.
|
||||
*
|
||||
* @return proxy cache. Only single instance should be used across whole app.
|
||||
*/
|
||||
public HttpProxyCacheServer build() {
|
||||
Config config = buildConfig();
|
||||
return new HttpProxyCacheServer(config);
|
||||
}
|
||||
|
||||
private Config buildConfig() {
|
||||
return new Config(cacheRoot, fileNameGenerator, diskUsage);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package com.danikula.videocache;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
|
||||
import com.danikula.videocache.file.FileCache;
|
||||
|
||||
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 CacheListener uiCacheListener;
|
||||
private final Config config;
|
||||
|
||||
public HttpProxyCacheServerClients(String url, Config config) {
|
||||
this.url = checkNotNull(url);
|
||||
this.config = checkNotNull(config);
|
||||
this.uiCacheListener = new UiListenerHandler(url, listeners);
|
||||
}
|
||||
|
||||
public void processRequest(GetRequest request, Socket socket) throws ProxyCacheException, IOException {
|
||||
startProcessRequest();
|
||||
try {
|
||||
clientsCount.incrementAndGet();
|
||||
proxyCache.processRequest(request, socket);
|
||||
} finally {
|
||||
finishProcessRequest();
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void startProcessRequest() throws ProxyCacheException {
|
||||
proxyCache = proxyCache == null ? newHttpProxyCache() : proxyCache;
|
||||
}
|
||||
|
||||
private synchronized void finishProcessRequest() {
|
||||
if (clientsCount.decrementAndGet() <= 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);
|
||||
}
|
||||
|
||||
public int getClientsCount() {
|
||||
return clientsCount.get();
|
||||
}
|
||||
|
||||
private HttpProxyCache newHttpProxyCache() throws ProxyCacheException {
|
||||
HttpUrlSource source = new HttpUrlSource(url);
|
||||
FileCache cache = new FileCache(config.generateCacheFile(url), config.diskUsage);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,14 +3,20 @@ package com.danikula.videocache;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
|
||||
import static com.danikula.videocache.Preconditions.checkNotNull;
|
||||
import static com.danikula.videocache.ProxyCacheUtils.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;
|
||||
import static java.net.HttpURLConnection.HTTP_PARTIAL;
|
||||
import static java.net.HttpURLConnection.HTTP_SEE_OTHER;
|
||||
|
||||
/**
|
||||
* {@link Source} that uses http resource as source for {@link ProxyCache}.
|
||||
@@ -19,10 +25,11 @@ import static java.net.HttpURLConnection.HTTP_PARTIAL;
|
||||
*/
|
||||
public class HttpUrlSource implements Source {
|
||||
|
||||
final String url;
|
||||
private static final int MAX_REDIRECTS = 5;
|
||||
public final String url;
|
||||
private HttpURLConnection connection;
|
||||
private InputStream inputStream;
|
||||
private volatile int available = Integer.MIN_VALUE;
|
||||
private volatile int length = Integer.MIN_VALUE;
|
||||
private volatile String mime;
|
||||
|
||||
public HttpUrlSource(String url) {
|
||||
@@ -34,36 +41,36 @@ public class HttpUrlSource implements Source {
|
||||
this.mime = mime;
|
||||
}
|
||||
|
||||
public HttpUrlSource(HttpUrlSource source) {
|
||||
this.url = source.url;
|
||||
this.mime = source.mime;
|
||||
this.length = source.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int available() throws ProxyCacheException {
|
||||
if (available == Integer.MIN_VALUE) {
|
||||
public synchronized int length() throws ProxyCacheException {
|
||||
if (length == Integer.MIN_VALUE) {
|
||||
fetchContentInfo();
|
||||
}
|
||||
return available;
|
||||
return length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void open(int offset) throws ProxyCacheException {
|
||||
try {
|
||||
Log.d(ProxyCacheUtils.LOG_TAG, "Open connection " + (offset > 0 ? " with offset " + offset : "") + " to " + url);
|
||||
connection = (HttpURLConnection) new URL(url).openConnection();
|
||||
if (offset > 0) {
|
||||
connection.setRequestProperty("Range", "bytes=" + offset + "-");
|
||||
}
|
||||
connection = openConnection(offset, "GET", -1);
|
||||
mime = connection.getContentType();
|
||||
inputStream = connection.getInputStream();
|
||||
readSourceAvailableBytes(connection, offset);
|
||||
inputStream = new BufferedInputStream(connection.getInputStream(), DEFAULT_BUFFER_SIZE);
|
||||
length = readSourceAvailableBytes(connection, offset, connection.getResponseCode());
|
||||
} catch (IOException e) {
|
||||
throw new ProxyCacheException("Error opening connection for " + url + " with offset " + offset, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void readSourceAvailableBytes(HttpURLConnection connection, int offset) throws IOException {
|
||||
private int readSourceAvailableBytes(HttpURLConnection connection, int offset, int responseCode) throws IOException {
|
||||
int contentLength = connection.getContentLength();
|
||||
int responseCode = connection.getResponseCode();
|
||||
available = responseCode == HTTP_OK ? contentLength :
|
||||
responseCode == HTTP_PARTIAL ? contentLength + offset :
|
||||
available;
|
||||
return responseCode == HTTP_OK ? contentLength
|
||||
: responseCode == HTTP_PARTIAL ? contentLength + offset : length;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -80,33 +87,76 @@ public class HttpUrlSource implements Source {
|
||||
}
|
||||
try {
|
||||
return inputStream.read(buffer, 0, buffer.length);
|
||||
} catch (InterruptedIOException e) {
|
||||
throw new InterruptedProxyCacheException("Reading source " + url + " is interrupted", e);
|
||||
} catch (IOException e) {
|
||||
throw new ProxyCacheException("Error reading data from " + url, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void fetchContentInfo() throws ProxyCacheException {
|
||||
Log.d(ProxyCacheUtils.LOG_TAG, "Read content info from " + url);
|
||||
Log.d(LOG_TAG, "Read content info from " + url);
|
||||
HttpURLConnection urlConnection = null;
|
||||
InputStream inputStream = null;
|
||||
try {
|
||||
urlConnection = (HttpURLConnection) new URL(url).openConnection();
|
||||
urlConnection.setRequestMethod("HEAD");
|
||||
available = urlConnection.getContentLength();
|
||||
urlConnection = openConnection(0, "HEAD", 10000);
|
||||
length = urlConnection.getContentLength();
|
||||
mime = urlConnection.getContentType();
|
||||
Log.d(ProxyCacheUtils.LOG_TAG, "Content-Length of " + url + " is " + available + " bytes, mime is " + mime);
|
||||
inputStream = urlConnection.getInputStream();
|
||||
Log.i(LOG_TAG, "Content info for `" + url + "`: mime: " + mime + ", content-length: " + length);
|
||||
} catch (IOException e) {
|
||||
throw new ProxyCacheException("Error fetching Content-Length from " + url);
|
||||
Log.e(LOG_TAG, "Error fetching info from " + url, e);
|
||||
} finally {
|
||||
ProxyCacheUtils.close(inputStream);
|
||||
if (urlConnection != null) {
|
||||
urlConnection.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String getMime() throws ProxyCacheException {
|
||||
private HttpURLConnection openConnection(int offset, String method, int timeout) throws IOException, ProxyCacheException {
|
||||
HttpURLConnection connection;
|
||||
boolean redirected;
|
||||
int redirectCount = 0;
|
||||
String url = this.url;
|
||||
do {
|
||||
Log.d(LOG_TAG, "Open connection " + (offset > 0 ? " with offset " + offset : "") + " to " + url);
|
||||
connection = (HttpURLConnection) new URL(url).openConnection();
|
||||
connection.setRequestMethod(method);
|
||||
if (offset > 0) {
|
||||
connection.setRequestProperty("Range", "bytes=" + offset + "-");
|
||||
}
|
||||
if (timeout > 0) {
|
||||
connection.setConnectTimeout(timeout);
|
||||
connection.setReadTimeout(timeout);
|
||||
}
|
||||
int code = connection.getResponseCode();
|
||||
redirected = code == HTTP_MOVED_PERM || code == HTTP_MOVED_TEMP || code == HTTP_SEE_OTHER;
|
||||
if (redirected) {
|
||||
url = connection.getHeaderField("Location");
|
||||
redirectCount++;
|
||||
connection.disconnect();
|
||||
}
|
||||
if (redirectCount > MAX_REDIRECTS) {
|
||||
throw new ProxyCacheException("Too many redirects: " + redirectCount);
|
||||
}
|
||||
} while (redirected);
|
||||
return connection;
|
||||
}
|
||||
|
||||
public synchronized String getMime() throws ProxyCacheException {
|
||||
if (TextUtils.isEmpty(mime)) {
|
||||
fetchContentInfo();
|
||||
}
|
||||
return mime;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "HttpUrlSource{url='" + url + "}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,14 @@ final class Preconditions {
|
||||
return reference;
|
||||
}
|
||||
|
||||
static void checkAllNotNull(Object... references) {
|
||||
for (Object reference : references) {
|
||||
if (reference == null) {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static <T> T checkNotNull(T reference, String errorMessage) {
|
||||
if (reference == null) {
|
||||
throw new NullPointerException(errorMessage);
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package com.danikula.videocache;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
@@ -18,35 +16,23 @@ import static com.danikula.videocache.ProxyCacheUtils.LOG_TAG;
|
||||
*
|
||||
* @author Alexey Danilov (danikula@gmail.com).
|
||||
*/
|
||||
public class ProxyCache {
|
||||
class ProxyCache {
|
||||
|
||||
private static final int MAX_READ_SOURCE_ATTEMPTS = 1;
|
||||
|
||||
private final Source source;
|
||||
private final Cache cache;
|
||||
private final Object wc;
|
||||
private final Handler handler;
|
||||
private final Object wc = new Object();
|
||||
private final Object stopLock = new Object();
|
||||
private final AtomicInteger readSourceErrorsCount;
|
||||
private volatile Thread sourceReaderThread;
|
||||
private volatile boolean stopped;
|
||||
private final AtomicInteger readSourceErrorsCount;
|
||||
private CacheListener cacheListener;
|
||||
private final boolean logEnabled;
|
||||
|
||||
public ProxyCache(Source source, Cache cache, boolean logEnabled) {
|
||||
this.source = checkNotNull(source);
|
||||
this.cache = checkNotNull(cache);
|
||||
this.logEnabled = logEnabled;
|
||||
this.wc = new Object();
|
||||
this.handler = new Handler(Looper.getMainLooper());
|
||||
this.readSourceErrorsCount = new AtomicInteger();
|
||||
}
|
||||
private volatile int percentsAvailable = -1;
|
||||
|
||||
public ProxyCache(Source source, Cache cache) {
|
||||
this(source, cache, false);
|
||||
}
|
||||
|
||||
public void setCacheListener(CacheListener cacheListener) {
|
||||
this.cacheListener = cacheListener;
|
||||
this.source = checkNotNull(source);
|
||||
this.cache = checkNotNull(cache);
|
||||
this.readSourceErrorsCount = new AtomicInteger();
|
||||
}
|
||||
|
||||
public int read(byte[] buffer, long offset, int length) throws ProxyCacheException {
|
||||
@@ -55,23 +41,16 @@ public class ProxyCache {
|
||||
while (!cache.isCompleted() && cache.available() < (offset + length) && !stopped) {
|
||||
readSourceAsync();
|
||||
waitForSourceData();
|
||||
checkIsCacheValid();
|
||||
checkReadSourceErrorsCount();
|
||||
}
|
||||
int read = cache.read(buffer, offset, length);
|
||||
if (isLogEnabled()) {
|
||||
Log.d(LOG_TAG, "Read data[" + read + " bytes] from cache with offset " + offset + ": " + ProxyCacheUtils.preview(buffer, read));
|
||||
if (cache.isCompleted() && percentsAvailable != 100) {
|
||||
percentsAvailable = 100;
|
||||
onCachePercentsAvailableChanged(100);
|
||||
}
|
||||
return read;
|
||||
}
|
||||
|
||||
private void checkIsCacheValid() throws ProxyCacheException {
|
||||
int sourceAvailable = source.available();
|
||||
if (sourceAvailable > 0 && cache.available() > sourceAvailable) {
|
||||
throw new ProxyCacheException("Unexpected cache: cache [" + cache.available() + " bytes] > source[" + sourceAvailable + " bytes]");
|
||||
}
|
||||
}
|
||||
|
||||
private void checkReadSourceErrorsCount() throws ProxyCacheException {
|
||||
int errorsCount = readSourceErrorsCount.get();
|
||||
if (errorsCount >= MAX_READ_SOURCE_ATTEMPTS) {
|
||||
@@ -81,22 +60,24 @@ public class ProxyCache {
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
try {
|
||||
stopped = true;
|
||||
if (sourceReaderThread != null) {
|
||||
sourceReaderThread.interrupt();
|
||||
synchronized (stopLock) {
|
||||
Log.d(LOG_TAG, "Shutdown proxy for " + source);
|
||||
try {
|
||||
stopped = true;
|
||||
if (sourceReaderThread != null) {
|
||||
sourceReaderThread.interrupt();
|
||||
}
|
||||
cache.close();
|
||||
} catch (ProxyCacheException e) {
|
||||
onError(e);
|
||||
}
|
||||
cache.close();
|
||||
} catch (ProxyCacheException e) {
|
||||
onError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void readSourceAsync() throws ProxyCacheException {
|
||||
|
||||
private synchronized void readSourceAsync() throws ProxyCacheException {
|
||||
boolean readingInProgress = sourceReaderThread != null && sourceReaderThread.getState() != Thread.State.TERMINATED;
|
||||
if (!stopped && !cache.isCompleted() && !readingInProgress) {
|
||||
sourceReaderThread = new Thread(new SourceReaderRunnable(), "Source reader for ProxyCache");
|
||||
sourceReaderThread = new Thread(new SourceReaderRunnable(), "Source reader for " + source);
|
||||
sourceReaderThread.start();
|
||||
}
|
||||
}
|
||||
@@ -111,50 +92,68 @@ public class ProxyCache {
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyNewCacheDataAvailable(final int cachePercentage) {
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (cacheListener != null) {
|
||||
cacheListener.onCacheDataAvailable(cachePercentage);
|
||||
}
|
||||
}
|
||||
});
|
||||
private void notifyNewCacheDataAvailable(long cacheAvailable, long sourceAvailable) {
|
||||
onCacheAvailable(cacheAvailable, sourceAvailable);
|
||||
|
||||
synchronized (wc) {
|
||||
wc.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
protected void onCacheAvailable(long cacheAvailable, long sourceAvailable) {
|
||||
int percents = (int) (cacheAvailable * 100 / sourceAvailable);
|
||||
boolean percentsChanged = percents != percentsAvailable;
|
||||
boolean sourceLengthKnown = sourceAvailable >= 0;
|
||||
if (sourceLengthKnown && percentsChanged) {
|
||||
onCachePercentsAvailableChanged(percents);
|
||||
}
|
||||
percentsAvailable = percents;
|
||||
}
|
||||
|
||||
protected void onCachePercentsAvailableChanged(int percentsAvailable) {
|
||||
}
|
||||
|
||||
private void readSource() {
|
||||
int cachePercentage = 0;
|
||||
int sourceAvailable = -1;
|
||||
int offset = 0;
|
||||
try {
|
||||
int offset = cache.available();
|
||||
offset = cache.available();
|
||||
source.open(offset);
|
||||
sourceAvailable = source.length();
|
||||
byte[] buffer = new byte[ProxyCacheUtils.DEFAULT_BUFFER_SIZE];
|
||||
int readBytes;
|
||||
while ((readBytes = source.read(buffer)) != -1 && !Thread.currentThread().isInterrupted() && !stopped) {
|
||||
if (isLogEnabled()) {
|
||||
Log.d(LOG_TAG, "Write data[" + readBytes + " bytes] to cache from source with offset " + offset + ": " + ProxyCacheUtils.preview(buffer, readBytes));
|
||||
while ((readBytes = source.read(buffer)) != -1) {
|
||||
synchronized (stopLock) {
|
||||
if (isStopped()) {
|
||||
return;
|
||||
}
|
||||
cache.append(buffer, readBytes);
|
||||
}
|
||||
cache.append(buffer, readBytes);
|
||||
offset += readBytes;
|
||||
cachePercentage = offset * 100 / source.available();
|
||||
|
||||
notifyNewCacheDataAvailable(cachePercentage);
|
||||
}
|
||||
if (cache.available() == source.available()) {
|
||||
cache.complete();
|
||||
notifyNewCacheDataAvailable(offset, sourceAvailable);
|
||||
}
|
||||
tryComplete();
|
||||
} catch (Throwable e) {
|
||||
readSourceErrorsCount.incrementAndGet();
|
||||
onError(e);
|
||||
} finally {
|
||||
closeSource();
|
||||
notifyNewCacheDataAvailable(cachePercentage);
|
||||
notifyNewCacheDataAvailable(offset, sourceAvailable);
|
||||
}
|
||||
}
|
||||
|
||||
private void tryComplete() throws ProxyCacheException {
|
||||
synchronized (stopLock) {
|
||||
if (!isStopped() && cache.available() == source.length()) {
|
||||
cache.complete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isStopped() {
|
||||
return Thread.currentThread().isInterrupted() || stopped;
|
||||
}
|
||||
|
||||
private void closeSource() {
|
||||
try {
|
||||
source.close();
|
||||
@@ -164,12 +163,12 @@ public class ProxyCache {
|
||||
}
|
||||
|
||||
protected final void onError(final Throwable e) {
|
||||
Log.e(LOG_TAG, "ProxyCache error", e);
|
||||
handler.post(new ErrorDeliverer(e));
|
||||
}
|
||||
|
||||
protected boolean isLogEnabled() {
|
||||
return logEnabled;
|
||||
boolean interruption = e instanceof InterruptedProxyCacheException;
|
||||
if (interruption) {
|
||||
Log.d(LOG_TAG, "ProxyCache is interrupted");
|
||||
} else {
|
||||
Log.e(LOG_TAG, "ProxyCache error", e);
|
||||
}
|
||||
}
|
||||
|
||||
private class SourceReaderRunnable implements Runnable {
|
||||
@@ -179,24 +178,4 @@ public class ProxyCache {
|
||||
readSource();
|
||||
}
|
||||
}
|
||||
|
||||
private class ErrorDeliverer implements Runnable {
|
||||
|
||||
private final Throwable error;
|
||||
|
||||
public ErrorDeliverer(Throwable error) {
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (error instanceof ProxyCacheException) {
|
||||
if (cacheListener != null) {
|
||||
cacheListener.onError((ProxyCacheException) error);
|
||||
}
|
||||
} else {
|
||||
throw new RuntimeException("Unexpected error!", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
package com.danikula.videocache;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.webkit.MimeTypeMap;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLDecoder;
|
||||
import java.net.URLEncoder;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import static com.danikula.videocache.Preconditions.checkArgument;
|
||||
@@ -15,7 +21,7 @@ import static com.danikula.videocache.Preconditions.checkNotNull;
|
||||
*
|
||||
* @author Alexey Danilov (danikula@gmail.com).
|
||||
*/
|
||||
class ProxyCacheUtils {
|
||||
public class ProxyCacheUtils {
|
||||
|
||||
static final String LOG_TAG = "ProxyCache";
|
||||
static final int DEFAULT_BUFFER_SIZE = 8 * 1024;
|
||||
@@ -43,18 +49,47 @@ class ProxyCacheUtils {
|
||||
return preview;
|
||||
}
|
||||
|
||||
static void createDirectory(File directory) throws IOException {
|
||||
checkNotNull(directory, "File must be not null!");
|
||||
if (directory.exists()) {
|
||||
checkArgument(directory.isDirectory(), "File is not directory!");
|
||||
} else {
|
||||
boolean isCreated = directory.mkdirs();
|
||||
if (!isCreated) {
|
||||
String error = String.format("Directory %s can't be created", directory.getAbsolutePath());
|
||||
throw new IOException(error);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
static void close(Closeable closeable) {
|
||||
if (closeable != null) {
|
||||
try {
|
||||
closeable.close();
|
||||
} catch (IOException e) {
|
||||
Log.e(LOG_TAG, "Error closing resource", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static 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 static String bytesToHexString(byte[] bytes) {
|
||||
StringBuffer sb = new StringBuffer();
|
||||
for (byte b : bytes) {
|
||||
sb.append(String.format("%02x", b));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,34 @@ package com.danikula.videocache;
|
||||
*/
|
||||
public interface Source {
|
||||
|
||||
int available() throws ProxyCacheException;
|
||||
|
||||
/**
|
||||
* Opens source. Source should be open before using {@link #read(byte[])}
|
||||
*
|
||||
* @param offset offset in bytes for source.
|
||||
* @throws ProxyCacheException if error occur while opening source.
|
||||
*/
|
||||
void open(int offset) throws ProxyCacheException;
|
||||
|
||||
void close() throws ProxyCacheException;
|
||||
/**
|
||||
* Returns length bytes or <b>negative value</b> if length is unknown.
|
||||
*
|
||||
* @return bytes length
|
||||
* @throws ProxyCacheException if error occur while fetching source data.
|
||||
*/
|
||||
int length() throws ProxyCacheException;
|
||||
|
||||
/**
|
||||
* Read data to byte buffer from source with current offset.
|
||||
*
|
||||
* @param buffer a buffer to be used for reading data.
|
||||
* @throws ProxyCacheException if error occur while reading source.
|
||||
*/
|
||||
int read(byte[] buffer) throws ProxyCacheException;
|
||||
|
||||
/**
|
||||
* Closes source and release resources. Every opened source should be closed.
|
||||
*
|
||||
* @throws ProxyCacheException if error occur while closing source.
|
||||
*/
|
||||
void close() throws ProxyCacheException;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.danikula.videocache;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Environment;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import static android.os.Environment.MEDIA_MOUNTED;
|
||||
import static com.danikula.videocache.ProxyCacheUtils.LOG_TAG;
|
||||
|
||||
/**
|
||||
* Provides application storage paths
|
||||
* <p/>
|
||||
* See https://github.com/nostra13/Android-Universal-Image-Loader
|
||||
*
|
||||
* @author Sergey Tarasevich (nostra13[at]gmail[dot]com)
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class StorageUtils {
|
||||
|
||||
private static final String INDIVIDUAL_DIR_NAME = "video-cache";
|
||||
|
||||
/**
|
||||
* Returns individual application cache directory (for only video caching from Proxy). Cache directory will be
|
||||
* created on SD card <i>("/Android/data/[app_package_name]/cache/video-cache")</i> if card is mounted .
|
||||
* Else - Android defines cache directory on device's file system.
|
||||
*
|
||||
* @param context Application context
|
||||
* @return Cache {@link File directory}
|
||||
*/
|
||||
public static File getIndividualCacheDirectory(Context context) {
|
||||
File cacheDir = getCacheDirectory(context, true);
|
||||
return new File(cacheDir, INDIVIDUAL_DIR_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns application cache directory. Cache directory will be created on SD card
|
||||
* <i>("/Android/data/[app_package_name]/cache")</i> (if card is mounted and app has appropriate permission) or
|
||||
* on device's file system depending incoming parameters.
|
||||
*
|
||||
* @param context Application context
|
||||
* @param preferExternal Whether prefer external location for cache
|
||||
* @return Cache {@link File directory}.<br />
|
||||
* <b>NOTE:</b> Can be null in some unpredictable cases (if SD card is unmounted and
|
||||
* {@link android.content.Context#getCacheDir() Context.getCacheDir()} returns null).
|
||||
*/
|
||||
private static File getCacheDirectory(Context context, boolean preferExternal) {
|
||||
File appCacheDir = null;
|
||||
String externalStorageState;
|
||||
try {
|
||||
externalStorageState = Environment.getExternalStorageState();
|
||||
} catch (NullPointerException e) { // (sh)it happens
|
||||
externalStorageState = "";
|
||||
}
|
||||
if (preferExternal && MEDIA_MOUNTED.equals(externalStorageState)) {
|
||||
appCacheDir = getExternalCacheDir(context);
|
||||
}
|
||||
if (appCacheDir == null) {
|
||||
appCacheDir = context.getCacheDir();
|
||||
}
|
||||
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.");
|
||||
appCacheDir = new File(cacheDirPath);
|
||||
}
|
||||
return appCacheDir;
|
||||
}
|
||||
|
||||
private static File getExternalCacheDir(Context context) {
|
||||
File dataDir = new File(new File(Environment.getExternalStorageDirectory(), "Android"), "data");
|
||||
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");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return appCacheDir;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.danikula.videocache.file;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Declares how {@link FileCache} will use disc space.
|
||||
*
|
||||
* @author Alexey Danilov (danikula@gmail.com).
|
||||
*/
|
||||
public interface DiskUsage {
|
||||
|
||||
void touch(File file) throws IOException;
|
||||
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
package com.danikula.videocache;
|
||||
package com.danikula.videocache.file;
|
||||
|
||||
import com.danikula.videocache.Cache;
|
||||
import com.danikula.videocache.ProxyCacheException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
|
||||
import static com.danikula.videocache.Preconditions.checkNotNull;
|
||||
|
||||
/**
|
||||
* {@link Cache} that uses file for storing data.
|
||||
*
|
||||
@@ -15,22 +16,25 @@ public class FileCache implements Cache {
|
||||
|
||||
private static final String TEMP_POSTFIX = ".download";
|
||||
|
||||
private final DiskUsage diskUsage;
|
||||
public File file;
|
||||
private RandomAccessFile dataFile;
|
||||
private File file;
|
||||
|
||||
public FileCache(File file) throws ProxyCacheException {
|
||||
this(file, new UnlimitedDiskUsage());
|
||||
}
|
||||
|
||||
public FileCache(File file, DiskUsage diskUsage) throws ProxyCacheException {
|
||||
try {
|
||||
checkNotNull(file);
|
||||
boolean partialFile = isTempFile(file);
|
||||
boolean completed = file.exists() && !partialFile;
|
||||
if (completed) {
|
||||
this.dataFile = new RandomAccessFile(file, "r");
|
||||
this.file = file;
|
||||
} else {
|
||||
ProxyCacheUtils.createDirectory(file.getParentFile());
|
||||
this.file = partialFile ? file : new File(file.getParentFile(), file.getName() + TEMP_POSTFIX);
|
||||
this.dataFile = new RandomAccessFile(this.file, "rw");
|
||||
if (diskUsage == null) {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
this.diskUsage = diskUsage;
|
||||
File directory = file.getParentFile();
|
||||
Files.makeDir(directory);
|
||||
boolean completed = file.exists();
|
||||
this.file = completed ? file : new File(file.getParentFile(), file.getName() + TEMP_POSTFIX);
|
||||
this.dataFile = new RandomAccessFile(this.file, completed ? "r" : "rw");
|
||||
} catch (IOException e) {
|
||||
throw new ProxyCacheException("Error using file " + file + " as disc cache", e);
|
||||
}
|
||||
@@ -41,7 +45,7 @@ public class FileCache implements Cache {
|
||||
try {
|
||||
return (int) dataFile.length();
|
||||
} catch (IOException e) {
|
||||
throw new ProxyCacheException("Error reading length of file " + dataFile, e);
|
||||
throw new ProxyCacheException("Error reading length of file " + file, e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +78,7 @@ public class FileCache implements Cache {
|
||||
public synchronized void close() throws ProxyCacheException {
|
||||
try {
|
||||
dataFile.close();
|
||||
diskUsage.touch(file);
|
||||
} catch (IOException e) {
|
||||
throw new ProxyCacheException("Error closing file " + file, e);
|
||||
}
|
||||
@@ -117,4 +122,5 @@ public class FileCache implements Cache {
|
||||
private boolean isTempFile(File file) {
|
||||
return file.getName().endsWith(TEMP_POSTFIX);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.danikula.videocache.file;
|
||||
|
||||
/**
|
||||
* Generator for files to be used for caching.
|
||||
*
|
||||
* @author Alexey Danilov (danikula@gmail.com).
|
||||
*/
|
||||
public interface FileNameGenerator {
|
||||
|
||||
String generate(String url);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.danikula.videocache.file;
|
||||
|
||||
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.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Utils for work with files.
|
||||
*
|
||||
* @author Alexey Danilov (danikula@gmail.com).
|
||||
*/
|
||||
class Files {
|
||||
|
||||
static void makeDir(File directory) throws IOException {
|
||||
if (directory.exists()) {
|
||||
if (!directory.isDirectory()) {
|
||||
throw new IOException("File " + directory + " is not directory!");
|
||||
}
|
||||
} else {
|
||||
boolean isCreated = directory.mkdirs();
|
||||
if (!isCreated) {
|
||||
throw new IOException(String.format("Directory %s can't be created", directory.getAbsolutePath()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static List<File> getLruListFiles(File directory) {
|
||||
List<File> result = new LinkedList<>();
|
||||
File[] files = directory.listFiles();
|
||||
if (files != null) {
|
||||
result = Arrays.asList(files);
|
||||
Collections.sort(result, new LastModifiedComparator());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static void setLastModifiedNow(File file) throws IOException {
|
||||
if (file.exists()) {
|
||||
long now = System.currentTimeMillis();
|
||||
boolean modified = file.setLastModified(now); // on some devices (e.g. Nexus 5) doesn't work
|
||||
if (!modified) {
|
||||
modify(file);
|
||||
if (file.lastModified() < now) {
|
||||
throw new IOException("Error set last modified date to " + file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void modify(File file) throws IOException {
|
||||
long size = file.length();
|
||||
if (size == 0) {
|
||||
recreateZeroSizeFile(file);
|
||||
return;
|
||||
}
|
||||
|
||||
RandomAccessFile accessFile = new RandomAccessFile(file, "rwd");
|
||||
accessFile.seek(size - 1);
|
||||
byte lastByte = accessFile.readByte();
|
||||
accessFile.seek(size - 1);
|
||||
accessFile.write(lastByte);
|
||||
accessFile.close();
|
||||
}
|
||||
|
||||
private static void recreateZeroSizeFile(File file) throws IOException {
|
||||
if (!file.delete() || !file.createNewFile()) {
|
||||
throw new IOException("Error recreate zero-size file " + file);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class LastModifiedComparator implements Comparator<File> {
|
||||
|
||||
@Override
|
||||
public int compare(File lhs, File rhs) {
|
||||
return compareLong(lhs.lastModified(), rhs.lastModified());
|
||||
}
|
||||
|
||||
private int compareLong(long first, long second) {
|
||||
return (first < second) ? -1 : ((first == second) ? 0 : 1);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.danikula.videocache.file;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
/**
|
||||
* {@link DiskUsage} that uses LRU (Least Recently Used) strategy to trim cache.
|
||||
*
|
||||
* @author Alexey Danilov (danikula@gmail.com).
|
||||
*/
|
||||
abstract class LruDiskUsage implements DiskUsage {
|
||||
|
||||
private static final String LOG_TAG = "ProxyCache";
|
||||
private final ExecutorService workerThread = Executors.newSingleThreadExecutor();
|
||||
|
||||
@Override
|
||||
public void touch(File file) throws IOException {
|
||||
workerThread.submit(new TouchCallable(file));
|
||||
}
|
||||
|
||||
private void touchInBackground(File file) throws IOException {
|
||||
Files.setLastModifiedNow(file);
|
||||
List<File> files = Files.getLruListFiles(file.getParentFile());
|
||||
trim(files);
|
||||
}
|
||||
|
||||
protected abstract boolean accept(File file, long totalSize, int totalCount);
|
||||
|
||||
private void trim(List<File> files) {
|
||||
long totalSize = countTotalSize(files);
|
||||
int totalCount = files.size();
|
||||
for (File file : files) {
|
||||
boolean accepted = accept(file, totalSize, totalCount);
|
||||
if (!accepted) {
|
||||
long fileSize = file.length();
|
||||
boolean deleted = file.delete();
|
||||
if (deleted) {
|
||||
totalCount--;
|
||||
totalSize -= fileSize;
|
||||
Log.i(LOG_TAG, "Cache file " + file + " is deleted because it exceeds cache limit");
|
||||
} else {
|
||||
Log.e(LOG_TAG, "Error deleting file " + file + " for trimming cache");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private long countTotalSize(List<File> files) {
|
||||
long totalSize = 0;
|
||||
for (File file : files) {
|
||||
totalSize += file.length();
|
||||
}
|
||||
return totalSize;
|
||||
}
|
||||
|
||||
private class TouchCallable implements Callable<Void> {
|
||||
|
||||
private final File file;
|
||||
|
||||
public TouchCallable(File file) {
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void call() throws Exception {
|
||||
touchInBackground(file);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.danikula.videocache.file;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.danikula.videocache.ProxyCacheUtils;
|
||||
|
||||
/**
|
||||
* Implementation of {@link FileNameGenerator} that uses MD5 of url as file name
|
||||
*
|
||||
* @author Alexey Danilov (danikula@gmail.com).
|
||||
*/
|
||||
public class Md5FileNameGenerator implements FileNameGenerator {
|
||||
|
||||
private static final int MAX_EXTENSION_LENGTH = 4;
|
||||
|
||||
@Override
|
||||
public String generate(String url) {
|
||||
String extension = getExtension(url);
|
||||
String name = ProxyCacheUtils.computeMD5(url);
|
||||
return TextUtils.isEmpty(extension) ? name : name + "." + extension;
|
||||
}
|
||||
|
||||
private String getExtension(String url) {
|
||||
int dotIndex = url.lastIndexOf('.');
|
||||
int slashIndex = url.lastIndexOf('/');
|
||||
return dotIndex != -1 && dotIndex > slashIndex && dotIndex + 2 + MAX_EXTENSION_LENGTH > url.length() ?
|
||||
url.substring(dotIndex + 1, url.length()) : "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.danikula.videocache.file;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* {@link DiskUsage} that uses LRU (Least Recently Used) strategy and trims cache size to max files count if needed.
|
||||
*
|
||||
* @author Alexey Danilov (danikula@gmail.com).
|
||||
*/
|
||||
public class TotalCountLruDiskUsage extends LruDiskUsage {
|
||||
|
||||
private final int maxCount;
|
||||
|
||||
public TotalCountLruDiskUsage(int maxCount) {
|
||||
if (maxCount <= 0) {
|
||||
throw new IllegalArgumentException("Max count must be positive number!");
|
||||
}
|
||||
this.maxCount = maxCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean accept(File file, long totalSize, int totalCount) {
|
||||
return totalCount <= maxCount;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.danikula.videocache.file;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* {@link DiskUsage} that uses LRU (Least Recently Used) strategy and trims cache size to max size if needed.
|
||||
*
|
||||
* @author Alexey Danilov (danikula@gmail.com).
|
||||
*/
|
||||
public class TotalSizeLruDiskUsage extends LruDiskUsage {
|
||||
|
||||
private final long maxSize;
|
||||
|
||||
public TotalSizeLruDiskUsage(long maxSize) {
|
||||
if (maxSize <= 0) {
|
||||
throw new IllegalArgumentException("Max size must be positive number!");
|
||||
}
|
||||
this.maxSize = maxSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean accept(File file, long totalSize, int totalCount) {
|
||||
return totalSize <= maxSize;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.danikula.videocache.file;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Unlimited version of {@link DiskUsage}.
|
||||
*
|
||||
* @author Alexey Danilov (danikula@gmail.com).
|
||||
*/
|
||||
public class UnlimitedDiskUsage implements DiskUsage {
|
||||
|
||||
@Override
|
||||
public void touch(File file) throws IOException {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,44 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.4'
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
maven { url 'https://github.com/danikula/AndroidVideoCache/raw/mvn-repo' }
|
||||
maven { url 'https://github.com/dahlgren/vpi-aar/raw/master' }
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'com.neenbedankt.android-apt'
|
||||
|
||||
android {
|
||||
compileSdkVersion 22
|
||||
buildToolsVersion '22.0.1'
|
||||
compileSdkVersion 23
|
||||
buildToolsVersion '23.0.1'
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.danikula.videocache.sample"
|
||||
minSdkVersion 15
|
||||
targetSdkVersion 22
|
||||
targetSdkVersion 23
|
||||
versionCode 1
|
||||
versionName '1.0'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile('com.danikula:videocache:1.0')
|
||||
apt {
|
||||
arguments {
|
||||
androidManifestFile variant.outputs[0].processResources.manifestFile
|
||||
resourcePackageName android.defaultConfig.applicationId
|
||||
}
|
||||
}
|
||||
|
||||
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.3.2'
|
||||
compile 'com.viewpagerindicator:library:2.4.2-SNAPSHOT@aar'
|
||||
apt 'org.androidannotations:androidannotations:3.3.2'
|
||||
}
|
||||
|
||||
@@ -1,24 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.danikula.videocache.sample">
|
||||
package="com.danikula.videocache.sample"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
<application
|
||||
android:name=".App"
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@android:style/Theme.Holo.Light.NoActionBar">
|
||||
<activity
|
||||
android:name=".VideoActivity"
|
||||
android:name=".MenuActivity_"
|
||||
android:label="@string/app_name">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name=".SingleVideoActivity_" />
|
||||
<activity android:name=".MultipleVideosActivity_" />
|
||||
<activity android:name=".VideoGalleryActivity_" />
|
||||
<activity android:name=".SharedCacheActivity_" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
23
sample/src/main/java/com/danikula/videocache/sample/App.java
Normal file
23
sample/src/main/java/com/danikula/videocache/sample/App.java
Normal file
@@ -0,0 +1,23 @@
|
||||
package com.danikula.videocache.sample;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
|
||||
import com.danikula.videocache.HttpProxyCacheServer;
|
||||
|
||||
/**
|
||||
* @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() {
|
||||
return new HttpProxyCacheServer(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package com.danikula.videocache.sample;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.VideoView;
|
||||
|
||||
import com.danikula.videocache.CacheListener;
|
||||
import com.danikula.videocache.HttpProxyCacheServer;
|
||||
|
||||
import org.androidannotations.annotations.AfterViews;
|
||||
import org.androidannotations.annotations.EFragment;
|
||||
import org.androidannotations.annotations.FragmentArg;
|
||||
import org.androidannotations.annotations.InstanceState;
|
||||
import org.androidannotations.annotations.SeekBarTouchStop;
|
||||
import org.androidannotations.annotations.ViewById;
|
||||
|
||||
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;
|
||||
|
||||
@ViewById VideoView videoView;
|
||||
@ViewById ProgressBar progressBar;
|
||||
|
||||
private boolean visibleForUser;
|
||||
|
||||
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) {
|
||||
return GalleryVideoFragment_.builder()
|
||||
.url(url)
|
||||
.cachePath(cachePath)
|
||||
.build();
|
||||
}
|
||||
|
||||
@AfterViews
|
||||
void afterViewInjected() {
|
||||
startProxy();
|
||||
|
||||
if (visibleForUser) {
|
||||
startPlayer();
|
||||
}
|
||||
}
|
||||
|
||||
private void startPlayer() {
|
||||
videoView.seekTo(position);
|
||||
videoView.start();
|
||||
playerStarted = true;
|
||||
}
|
||||
|
||||
private void startProxy() {
|
||||
HttpProxyCacheServer proxy = App.getProxy(getActivity());
|
||||
proxy.registerCacheListener(this, url);
|
||||
videoView.setVideoPath(proxy.getProxyUrl(url));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUserVisibleHint(boolean isVisibleToUser) {
|
||||
super.setUserVisibleHint(isVisibleToUser);
|
||||
|
||||
visibleForUser = isVisibleToUser;
|
||||
if (videoView != null) {
|
||||
if (visibleForUser) {
|
||||
startPlayer();
|
||||
} else if (playerStarted) {
|
||||
position = videoView.getCurrentPosition();
|
||||
videoView.pause();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
updater.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
updater.stop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
videoView.stopPlayback();
|
||||
App.getProxy(getActivity()).unregisterCacheListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCacheAvailable(File file, String url, int percentsAvailable) {
|
||||
progressBar.setSecondaryProgress(percentsAvailable);
|
||||
}
|
||||
|
||||
private void updateVideoProgress() {
|
||||
int videoProgress = videoView.getCurrentPosition() * 100 / videoView.getDuration();
|
||||
progressBar.setProgress(videoProgress);
|
||||
}
|
||||
|
||||
@SeekBarTouchStop(R.id.progressBar)
|
||||
void seekVideo() {
|
||||
int videoPosition = videoView.getDuration() * progressBar.getProgress() / 100;
|
||||
videoView.seekTo(videoPosition);
|
||||
}
|
||||
|
||||
private final class VideoProgressUpdater extends Handler {
|
||||
|
||||
public void start() {
|
||||
sendEmptyMessage(0);
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
removeMessages(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
updateVideoProgress();
|
||||
sendEmptyMessageDelayed(0, 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.danikula.videocache.sample;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
import android.util.Log;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ListAdapter;
|
||||
import android.widget.ListView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.androidannotations.annotations.AfterViews;
|
||||
import org.androidannotations.annotations.Click;
|
||||
import org.androidannotations.annotations.EActivity;
|
||||
import org.androidannotations.annotations.ItemClick;
|
||||
import org.androidannotations.annotations.ViewById;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@EActivity(R.layout.activity_menu)
|
||||
public class MenuActivity extends FragmentActivity {
|
||||
|
||||
@ViewById ListView listView;
|
||||
|
||||
@AfterViews
|
||||
void onViewInjected() {
|
||||
ListAdapter adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, android.R.id.text1, buildListData());
|
||||
listView.setAdapter(adapter);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private List<ListEntry> buildListData() {
|
||||
return Arrays.asList(
|
||||
new ListEntry("Single Video", SingleVideoActivity_.class),
|
||||
new ListEntry("Multiple Videos", MultipleVideosActivity_.class),
|
||||
new ListEntry("Video Gallery with pre-caching", VideoGalleryActivity_.class),
|
||||
new ListEntry("Shared Cache", SharedCacheActivity_.class)
|
||||
);
|
||||
}
|
||||
|
||||
@ItemClick(R.id.listView)
|
||||
void onListItemClicked(int position) {
|
||||
ListEntry item = (ListEntry) listView.getAdapter().getItem(position);
|
||||
startActivity(new Intent(this, item.activityClass));
|
||||
}
|
||||
|
||||
@Click(R.id.cleanCacheButton)
|
||||
void onClearCacheButtonClick() {
|
||||
try {
|
||||
Utils.cleanDirectory(getExternalCacheDir());
|
||||
} catch (IOException e) {
|
||||
Log.e(null, "Error cleaning cache", e);
|
||||
Toast.makeText(this, "Error cleaning cache", Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ListEntry {
|
||||
|
||||
private final String title;
|
||||
private final Class activityClass;
|
||||
|
||||
public ListEntry(String title, Class activityClass) {
|
||||
this.title = title;
|
||||
this.activityClass = activityClass;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return title;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.danikula.videocache.sample;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
|
||||
import org.androidannotations.annotations.EActivity;
|
||||
|
||||
@EActivity(R.layout.activity_multiple_videos)
|
||||
public class MultipleVideosActivity extends FragmentActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle state) {
|
||||
super.onCreate(state);
|
||||
|
||||
if (state == null) {
|
||||
addVideoFragment(Video.ORANGE_1, R.id.videoContainer0);
|
||||
addVideoFragment(Video.ORANGE_2, R.id.videoContainer1);
|
||||
addVideoFragment(Video.ORANGE_3, R.id.videoContainer2);
|
||||
addVideoFragment(Video.ORANGE_4, R.id.videoContainer3);
|
||||
}
|
||||
}
|
||||
|
||||
private void addVideoFragment(Video video, int containerViewId) {
|
||||
getSupportFragmentManager()
|
||||
.beginTransaction()
|
||||
.add(containerViewId, VideoFragment.build(this, video))
|
||||
.commit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.danikula.videocache.sample;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
|
||||
import org.androidannotations.annotations.EActivity;
|
||||
|
||||
@EActivity(R.layout.activity_multiple_videos)
|
||||
public class SharedCacheActivity extends FragmentActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle state) {
|
||||
super.onCreate(state);
|
||||
|
||||
if (state == null) {
|
||||
addVideoFragment(Video.ORANGE_1, R.id.videoContainer0);
|
||||
addVideoFragment(Video.ORANGE_1, R.id.videoContainer1);
|
||||
addVideoFragment(Video.ORANGE_1, R.id.videoContainer2);
|
||||
addVideoFragment(Video.ORANGE_1, R.id.videoContainer3);
|
||||
}
|
||||
}
|
||||
|
||||
private void addVideoFragment(Video video, int containerViewId) {
|
||||
getSupportFragmentManager()
|
||||
.beginTransaction()
|
||||
.add(containerViewId, VideoFragment.build(this, video))
|
||||
.commit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.danikula.videocache.sample;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
|
||||
import org.androidannotations.annotations.EActivity;
|
||||
|
||||
@EActivity(R.layout.activity_single_video)
|
||||
public class SingleVideoActivity extends FragmentActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle state) {
|
||||
super.onCreate(state);
|
||||
|
||||
if (state == null) {
|
||||
getSupportFragmentManager()
|
||||
.beginTransaction()
|
||||
.add(R.id.containerView, VideoFragment.build(this, Video.ORANGE_1))
|
||||
.commit();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.danikula.videocache.sample;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Some utils methods.
|
||||
*
|
||||
* @author Alexey Danilov (danikula@gmail.com).
|
||||
*/
|
||||
public class Utils {
|
||||
|
||||
public static void cleanDirectory(File file) throws IOException {
|
||||
if (!file.exists()) {
|
||||
return;
|
||||
}
|
||||
File[] contentFiles = file.listFiles();
|
||||
if (contentFiles != null) {
|
||||
for (File contentFile : contentFiles) {
|
||||
delete(contentFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void delete(File file) throws IOException {
|
||||
if (file.isFile() && file.exists()) {
|
||||
deleteOrThrow(file);
|
||||
} else {
|
||||
cleanDirectory(file);
|
||||
deleteOrThrow(file);
|
||||
}
|
||||
}
|
||||
|
||||
private static void deleteOrThrow(File file) throws IOException {
|
||||
if (file.exists()) {
|
||||
boolean isDeleted = file.delete();
|
||||
if (!isDeleted) {
|
||||
throw new IOException(String.format("File %s can't be deleted", file.getAbsolutePath()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.danikula.videocache.sample;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public enum Video {
|
||||
|
||||
ORANGE_1("https://dl.dropboxusercontent.com/u/15506779/persistent/proxycache/orange1.mp4"),
|
||||
ORANGE_2("https://dl.dropboxusercontent.com/u/15506779/persistent/proxycache/orange2.mp4"),
|
||||
ORANGE_3("https://dl.dropboxusercontent.com/u/15506779/persistent/proxycache/orange3.mp4"),
|
||||
ORANGE_4("https://dl.dropboxusercontent.com/u/15506779/persistent/proxycache/orange4.mp4"),
|
||||
ORANGE_5("https://dl.dropboxusercontent.com/u/15506779/persistent/proxycache/orange5.mp4");
|
||||
|
||||
public final String url;
|
||||
|
||||
Video(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public File getCacheFile(Context context) {
|
||||
return new File(context.getExternalCacheDir(), name());
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
package com.danikula.videocache.sample;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.VideoView;
|
||||
|
||||
import com.danikula.videocache.Cache;
|
||||
import com.danikula.videocache.CacheListener;
|
||||
import com.danikula.videocache.FileCache;
|
||||
import com.danikula.videocache.HttpProxyCache;
|
||||
import com.danikula.videocache.HttpUrlSource;
|
||||
import com.danikula.videocache.ProxyCacheException;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class VideoActivity extends Activity implements CacheListener {
|
||||
|
||||
private static final String LOG_TAG = "VideoActivity";
|
||||
private static final String VIDEO_CACHE_NAME = "devbytes.mp4";
|
||||
private static final String VIDEO_URL = "https://dl.dropboxusercontent.com/u/15506779/persistent/proxycache/devbytes.mp4";
|
||||
|
||||
private VideoView videoView;
|
||||
private ProgressBar progressBar;
|
||||
private HttpProxyCache proxyCache;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setUpUi();
|
||||
playWithCache();
|
||||
}
|
||||
|
||||
private void setUpUi() {
|
||||
setContentView(R.layout.activity_video);
|
||||
videoView = (VideoView) findViewById(R.id.videoView);
|
||||
progressBar = (ProgressBar) findViewById(R.id.progressBar);
|
||||
progressBar.setMax(100);
|
||||
}
|
||||
|
||||
private void playWithCache() {
|
||||
try {
|
||||
Cache cache = new FileCache(new File(getExternalCacheDir(), VIDEO_CACHE_NAME));
|
||||
HttpUrlSource source = new HttpUrlSource(VIDEO_URL);
|
||||
proxyCache = new HttpProxyCache(source, cache);
|
||||
proxyCache.setCacheListener(this);
|
||||
videoView.setVideoPath(proxyCache.getUrl());
|
||||
videoView.start();
|
||||
} catch (ProxyCacheException e) {
|
||||
// do nothing. onError() handles all errors
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
if (proxyCache != null) {
|
||||
proxyCache.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ProxyCacheException e) {
|
||||
Log.e(LOG_TAG, "Error playing video", e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCacheDataAvailable(int cachePercentage) {
|
||||
progressBar.setProgress(cachePercentage);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package com.danikula.videocache.sample;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.util.Log;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.VideoView;
|
||||
|
||||
import com.danikula.videocache.CacheListener;
|
||||
import com.danikula.videocache.HttpProxyCacheServer;
|
||||
|
||||
import org.androidannotations.annotations.AfterViews;
|
||||
import org.androidannotations.annotations.EFragment;
|
||||
import org.androidannotations.annotations.FragmentArg;
|
||||
import org.androidannotations.annotations.SeekBarTouchStop;
|
||||
import org.androidannotations.annotations.ViewById;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
@EFragment(R.layout.fragment_video)
|
||||
public class VideoFragment extends Fragment implements CacheListener {
|
||||
|
||||
private static final String LOG_TAG = "VideoFragment";
|
||||
|
||||
@FragmentArg String url;
|
||||
@FragmentArg String cachePath;
|
||||
|
||||
@ViewById VideoView videoView;
|
||||
@ViewById ProgressBar progressBar;
|
||||
|
||||
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) {
|
||||
return VideoFragment_.builder()
|
||||
.url(url)
|
||||
.cachePath(cachePath)
|
||||
.build();
|
||||
}
|
||||
|
||||
@AfterViews
|
||||
void afterViewInjected() {
|
||||
startVideo();
|
||||
}
|
||||
|
||||
private void startVideo() {
|
||||
HttpProxyCacheServer proxy = App.getProxy(getActivity());
|
||||
proxy.registerCacheListener(this, url);
|
||||
videoView.setVideoPath(proxy.getProxyUrl(url));
|
||||
videoView.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
updater.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
updater.stop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
videoView.stopPlayback();
|
||||
App.getProxy(getActivity()).unregisterCacheListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCacheAvailable(File file, String url, int percentsAvailable) {
|
||||
progressBar.setSecondaryProgress(percentsAvailable);
|
||||
Log.d(LOG_TAG, String.format("onCacheAvailable. percents: %d, file: %s, url: %s", percentsAvailable, file, url));
|
||||
}
|
||||
|
||||
private void updateVideoProgress() {
|
||||
int videoProgress = videoView.getCurrentPosition() * 100 / videoView.getDuration();
|
||||
progressBar.setProgress(videoProgress);
|
||||
}
|
||||
|
||||
@SeekBarTouchStop(R.id.progressBar)
|
||||
void seekVideo() {
|
||||
int videoPosition = videoView.getDuration() * progressBar.getProgress() / 100;
|
||||
videoView.seekTo(videoPosition);
|
||||
}
|
||||
|
||||
private final class VideoProgressUpdater extends Handler {
|
||||
|
||||
public void start() {
|
||||
sendEmptyMessage(0);
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
removeMessages(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
updateVideoProgress();
|
||||
sendEmptyMessageDelayed(0, 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
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;
|
||||
import android.support.v4.view.ViewPager;
|
||||
|
||||
import com.viewpagerindicator.CirclePageIndicator;
|
||||
|
||||
import org.androidannotations.annotations.AfterViews;
|
||||
import org.androidannotations.annotations.EActivity;
|
||||
import org.androidannotations.annotations.ViewById;
|
||||
|
||||
@EActivity(R.layout.activity_video_gallery)
|
||||
public class VideoGalleryActivity extends FragmentActivity {
|
||||
|
||||
@ViewById ViewPager viewPager;
|
||||
@ViewById CirclePageIndicator viewPagerIndicator;
|
||||
|
||||
@AfterViews
|
||||
void afterViewInjected() {
|
||||
ViewsPagerAdapter viewsPagerAdapter = new ViewsPagerAdapter(this);
|
||||
viewPager.setAdapter(viewsPagerAdapter);
|
||||
viewPagerIndicator.setViewPager(viewPager);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return Video.values().length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getPageTitle(int position) {
|
||||
return Video.values()[position].name();
|
||||
}
|
||||
}
|
||||
}
|
||||
23
sample/src/main/res/layout/activity_menu.xml
Normal file
23
sample/src/main/res/layout/activity_menu.xml
Normal file
@@ -0,0 +1,23 @@
|
||||
<LinearLayout
|
||||
android:id="@+id/containerView"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:divider="@android:drawable/divider_horizontal_dark"
|
||||
android:orientation="vertical"
|
||||
android:showDividers="middle"
|
||||
tools:context=".MenuActivity">
|
||||
|
||||
<Button
|
||||
android:id="@+id/cleanCacheButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Clean cache" />
|
||||
|
||||
<ListView
|
||||
android:id="@+id/listView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</LinearLayout>
|
||||
58
sample/src/main/res/layout/activity_multiple_videos.xml
Normal file
58
sample/src/main/res/layout/activity_multiple_videos.xml
Normal file
@@ -0,0 +1,58 @@
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#CCC"
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/videoContainer0"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="8dp"
|
||||
android:layout_weight="1"
|
||||
android:background="#FFF" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/videoContainer1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="8dp"
|
||||
android:layout_weight="1"
|
||||
android:background="#FFF" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/videoContainer2"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="8dp"
|
||||
android:layout_weight="1"
|
||||
android:background="#FFF" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/videoContainer3"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="8dp"
|
||||
android:layout_weight="1"
|
||||
android:background="#FFF" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
7
sample/src/main/res/layout/activity_single_video.xml
Normal file
7
sample/src/main/res/layout/activity_single_video.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<FrameLayout
|
||||
android:id="@+id/containerView"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".VideoActivity" />
|
||||
23
sample/src/main/res/layout/activity_video_gallery.xml
Normal file
23
sample/src/main/res/layout/activity_video_gallery.xml
Normal file
@@ -0,0 +1,23 @@
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<android.support.v4.view.ViewPager
|
||||
android:id="@+id/viewPager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<com.viewpagerindicator.CirclePageIndicator
|
||||
android:id="@+id/viewPagerIndicator"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="8dp"
|
||||
app:fillColor="#800F"
|
||||
app:radius="10dp"
|
||||
app:strokeColor="#800F" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -10,13 +10,14 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<ProgressBar
|
||||
<SeekBar
|
||||
android:id="@+id/progressBar"
|
||||
style="@android:style/Widget.Holo.ProgressBar.Horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
style="@android:style/Widget.Holo.ProgressBar.Horizontal"
|
||||
android:layout_margin="16dp"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_centerHorizontal="true" />
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_margin="16dp"
|
||||
android:max="100" />
|
||||
|
||||
</RelativeLayout>
|
||||
@@ -39,7 +39,8 @@ dependencies {
|
||||
}
|
||||
testCompile 'com.squareup:fest-android:1.0.0'
|
||||
testCompile 'com.google.guava:guava-jdk5:17.0'
|
||||
testCompile('com.danikula:android-garden:2.0.13') {
|
||||
testCompile('com.danikula:android-garden:2.1.4') {
|
||||
exclude group: 'com.google.android'
|
||||
}
|
||||
testCompile 'org.mockito:mockito-all:1.9.5'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
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;
|
||||
|
||||
import static org.fest.assertions.api.Assertions.assertThat;
|
||||
import static org.fest.assertions.api.Assertions.fail;
|
||||
|
||||
/**
|
||||
* Tests for {@link FileNameGenerator} and implementations.
|
||||
*
|
||||
* @author Alexey Danilov (danikula@gmail.com).
|
||||
*/
|
||||
@RunWith(RobolectricGradleTestRunner.class)
|
||||
@Config(constants = BuildConfig.class, emulateSdk = BuildConfig.MIN_SDK_VERSION)
|
||||
public class FileNameGeneratorTest {
|
||||
|
||||
@Test
|
||||
public void testMd5SimpleName() throws Exception {
|
||||
String url = "http://host.com/videos/video.mpeg";
|
||||
String path = generateMd5Name("/home", url);
|
||||
String expected = "/home/" + ProxyCacheUtils.computeMD5(url) + ".mpeg";
|
||||
assertThat(path).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMd5NoExtension() throws Exception {
|
||||
String url = "http://host.com/video";
|
||||
String path = generateMd5Name("/home", url);
|
||||
String expected = "/home/" + ProxyCacheUtils.computeMD5(url);
|
||||
assertThat(path).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMd5TooLongExtension() throws Exception {
|
||||
String url = "http://host.com/videos/video-with-dot-.12345";
|
||||
String path = generateMd5Name("/home", url);
|
||||
String expected = "/home/" + ProxyCacheUtils.computeMD5(url);
|
||||
assertThat(path).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMd5InvalidExtension() throws Exception {
|
||||
String url = "http://host.com/videos/video.mp4?token=-648729473536183645";
|
||||
String path = generateMd5Name("/home", url);
|
||||
String expected = "/home/" + ProxyCacheUtils.computeMD5(url);
|
||||
assertThat(path).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMd5ExtraLongExtension() throws Exception {
|
||||
// https://github.com/danikula/AndroidVideoCache/issues/14
|
||||
String url = "https://d1wst0behutosd.cloudfront.net/videos/4367900/10807247.480p.mp4?Expires=1442849176&Signature=JXV~3AoI0rWcGuZBywg3-ukf6Ycw2X8v7Htog3lyvuFwp8o6VUEDFUsTC9-XtIGu-ULxCd7dP3fvB306lRyGFxdvf-sXLX~ar~HCQ7lullNyeLtp8BJOT5Y~W5rJE7X-AZaueNcycGtLFRhRtr5ySTguwtmJNaO3T1apX~-oVrFh1dWStEKbuPoXY04RgkmhMHoFgtwgXMC1ctIDeQHxZeXLi6LLyZnQsgzlUDffCx4P16iiW0uh2-Z~HUOi9BLBwHMQ5k5lYwZqdQ6DhhYoWlniRfQz6mp1IEiMgr4L3Z1ijgGITV4cYeF31CmFzCxaJTE7IIAC5tMDQSTt7M9Q4A__&Key-Pair-Id=APKAJJ6WELAPEP47UKWQ";
|
||||
String path = generateMd5Name("/home", url);
|
||||
String expected = "/home/" + ProxyCacheUtils.computeMD5(url);
|
||||
assertThat(path).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test(expected = NullPointerException.class)
|
||||
public void testAssertNullUrl() throws Exception {
|
||||
FileNameGenerator nameGenerator = new Md5FileNameGenerator();
|
||||
nameGenerator.generate(null);
|
||||
fail("Url should be not null");
|
||||
}
|
||||
|
||||
private String generateMd5Name(String rootFolder, String url) {
|
||||
FileNameGenerator nameGenerator = new Md5FileNameGenerator();
|
||||
String name = nameGenerator.generate(url);
|
||||
return new File(rootFolder, name).getAbsolutePath();
|
||||
}
|
||||
}
|
||||
@@ -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,239 @@
|
||||
package com.danikula.videocache;
|
||||
|
||||
import android.util.Pair;
|
||||
|
||||
import com.danikula.android.garden.io.IoUtils;
|
||||
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;
|
||||
import java.util.Arrays;
|
||||
|
||||
import static com.danikula.android.garden.io.Files.cleanDirectory;
|
||||
import static com.danikula.android.garden.io.Files.createDirectory;
|
||||
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_BIG_URL_ONE_REDIRECT;
|
||||
import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_SIZE;
|
||||
import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_URL;
|
||||
import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_URL_3_REDIRECTS;
|
||||
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.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 {
|
||||
|
||||
private File cacheFolder;
|
||||
|
||||
@Before
|
||||
public void setup() throws Exception {
|
||||
cacheFolder = ProxyCacheTestUtils.newCacheFile();
|
||||
createDirectory(cacheFolder);
|
||||
cleanDirectory(cacheFolder);
|
||||
}
|
||||
|
||||
@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 {
|
||||
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(cacheDir);
|
||||
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 testProxyFullResponseWithRedirect() throws Exception {
|
||||
Pair<File, Response> response = readProxyData(HTTP_DATA_BIG_URL_ONE_REDIRECT);
|
||||
|
||||
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 = 18000;
|
||||
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 - 1, 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));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProxyPartialResponseWithRedirect() throws Exception {
|
||||
int offset = 18000;
|
||||
Pair<File, Response> response = readProxyData(HTTP_DATA_BIG_URL_ONE_REDIRECT, 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 - 1, 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));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMaxSizeCacheLimit() throws Exception {
|
||||
HttpProxyCacheServer proxy = new HttpProxyCacheServer.Builder(RuntimeEnvironment.application)
|
||||
.cacheDirectory(cacheFolder)
|
||||
.maxCacheSize(HTTP_DATA_SIZE * 3 - 1) // for 2 files
|
||||
.build();
|
||||
|
||||
// use different url (doesn't matter than same content)
|
||||
readProxyResponse(proxy, HTTP_DATA_URL, 0);
|
||||
Thread.sleep(1050); // wait for new last modified date (file rounds time to second)
|
||||
|
||||
readProxyResponse(proxy, HTTP_DATA_URL_ONE_REDIRECT, 0);
|
||||
Thread.sleep(1050);
|
||||
|
||||
readProxyResponse(proxy, HTTP_DATA_URL_3_REDIRECTS, 0);
|
||||
Thread.sleep(1050);
|
||||
|
||||
assertThat(file(cacheFolder, HTTP_DATA_URL)).doesNotExist();
|
||||
assertThat(file(cacheFolder, HTTP_DATA_URL_ONE_REDIRECT)).exists();
|
||||
assertThat(file(cacheFolder, HTTP_DATA_URL_3_REDIRECTS)).exists();
|
||||
|
||||
readProxyResponse(proxy, HTTP_DATA_URL_ONE_REDIRECT, 0); // touch file
|
||||
readProxyResponse(proxy, HTTP_DATA_URL_6_REDIRECTS, 0);
|
||||
proxy.shutdown();
|
||||
|
||||
assertThat(file(cacheFolder, HTTP_DATA_URL_3_REDIRECTS)).doesNotExist();
|
||||
assertThat(file(cacheFolder, HTTP_DATA_URL_ONE_REDIRECT)).exists();
|
||||
assertThat(file(cacheFolder, HTTP_DATA_URL_6_REDIRECTS)).exists();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMaxFileCacheLimit() throws Exception {
|
||||
HttpProxyCacheServer proxy = new HttpProxyCacheServer.Builder(RuntimeEnvironment.application)
|
||||
.cacheDirectory(cacheFolder)
|
||||
.maxCacheFilesCount(2)
|
||||
.build();
|
||||
|
||||
// use different url (doesn't matter than same content)
|
||||
readProxyResponse(proxy, HTTP_DATA_URL, 0);
|
||||
Thread.sleep(1050); // wait for new last modified date (file rounds time to second)
|
||||
|
||||
readProxyResponse(proxy, HTTP_DATA_URL_ONE_REDIRECT, 0);
|
||||
Thread.sleep(1050);
|
||||
|
||||
readProxyResponse(proxy, HTTP_DATA_URL_3_REDIRECTS, 0);
|
||||
Thread.sleep(1050);
|
||||
|
||||
assertThat(file(cacheFolder, HTTP_DATA_URL)).doesNotExist();
|
||||
assertThat(file(cacheFolder, HTTP_DATA_URL_ONE_REDIRECT)).exists();
|
||||
assertThat(file(cacheFolder, HTTP_DATA_URL_3_REDIRECTS)).exists();
|
||||
|
||||
readProxyResponse(proxy, HTTP_DATA_URL_ONE_REDIRECT, 0); // touch file
|
||||
readProxyResponse(proxy, HTTP_DATA_URL_6_REDIRECTS, 0);
|
||||
proxy.shutdown();
|
||||
|
||||
assertThat(file(cacheFolder, HTTP_DATA_URL_3_REDIRECTS)).doesNotExist();
|
||||
assertThat(file(cacheFolder, HTTP_DATA_URL_ONE_REDIRECT)).exists();
|
||||
assertThat(file(cacheFolder, HTTP_DATA_URL_6_REDIRECTS)).exists();
|
||||
}
|
||||
|
||||
private Pair<File, Response> readProxyData(String url, int offset) throws IOException {
|
||||
File externalCacheDir = RuntimeEnvironment.application.getExternalCacheDir();
|
||||
File file = file(externalCacheDir, url);
|
||||
HttpProxyCacheServer proxy = newProxy(externalCacheDir);
|
||||
|
||||
Response response = readProxyResponse(proxy, url, offset);
|
||||
proxy.shutdown();
|
||||
|
||||
return new Pair<>(file, response);
|
||||
}
|
||||
|
||||
private File file(File parent, String url) {
|
||||
FileNameGenerator fileNameGenerator = new Md5FileNameGenerator();
|
||||
String name = fileNameGenerator.generate(url);
|
||||
return new File(parent, name);
|
||||
}
|
||||
|
||||
private Pair<File, Response> readProxyData(String url) throws IOException {
|
||||
return readProxyData(url, -1);
|
||||
}
|
||||
|
||||
private HttpProxyCacheServer newProxy(File cacheDir) {
|
||||
return new HttpProxyCacheServer.Builder(RuntimeEnvironment.application)
|
||||
.cacheDirectory(cacheDir)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,32 @@
|
||||
package com.danikula.videocache;
|
||||
|
||||
import com.danikula.android.garden.io.IoUtils;
|
||||
import com.danikula.videocache.support.AngryHttpUrlSource;
|
||||
import com.danikula.videocache.file.FileCache;
|
||||
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.annotation.Config;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Arrays;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.net.Socket;
|
||||
|
||||
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 com.danikula.videocache.support.ProxyCacheTestUtils.loadTestData;
|
||||
import static org.fest.assertions.api.Assertions.assertThat;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.anyInt;
|
||||
import static org.mockito.Matchers.anyLong;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Test {@link HttpProxyCache}.
|
||||
*
|
||||
* @author Alexey Danilov (danikula@gmail.com).
|
||||
*/
|
||||
@RunWith(RobolectricGradleTestRunner.class)
|
||||
@@ -33,110 +34,43 @@ import static org.fest.assertions.api.Assertions.assertThat;
|
||||
public class HttpProxyCacheTest {
|
||||
|
||||
@Test
|
||||
public void testHttpProxyCache() throws Exception {
|
||||
public void testProcessRequestNoCache() throws Exception {
|
||||
HttpUrlSource source = new HttpUrlSource(HTTP_DATA_URL);
|
||||
File file = newCacheFile();
|
||||
HttpProxyCache proxy = new HttpProxyCache(source, new FileCache(file));
|
||||
Response response = readProxyResponse(proxy);
|
||||
FileCache cache = new FileCache(ProxyCacheTestUtils.newCacheFile());
|
||||
HttpProxyCache proxyCache = new HttpProxyCache(source, cache);
|
||||
GetRequest request = new GetRequest("GET /" + HTTP_DATA_URL + " HTTP/1.1");
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
Socket socket = mock(Socket.class);
|
||||
when(socket.getOutputStream()).thenReturn(out);
|
||||
|
||||
proxyCache.processRequest(request, socket);
|
||||
Response response = new Response(out.toByteArray());
|
||||
|
||||
assertThat(response.data).isEqualTo(loadTestData());
|
||||
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.contentLength).isEqualTo(ProxyCacheTestUtils.HTTP_DATA_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);
|
||||
public void testProcessPartialRequestWithoutCache() throws Exception {
|
||||
HttpUrlSource source = new HttpUrlSource(HTTP_DATA_URL);
|
||||
FileCache fileCache = new FileCache(ProxyCacheTestUtils.newCacheFile());
|
||||
FileCache spyFileCache = Mockito.spy(fileCache);
|
||||
doThrow(new RuntimeException()).when(spyFileCache).read(any(byte[].class), anyLong(), anyInt());
|
||||
HttpProxyCache proxyCache = new HttpProxyCache(source, spyFileCache);
|
||||
GetRequest request = new GetRequest("GET /" + HTTP_DATA_URL + " HTTP/1.1\nRange: bytes=2000-");
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
Socket socket = mock(Socket.class);
|
||||
when(socket.getOutputStream()).thenReturn(out);
|
||||
|
||||
proxyCache.processRequest(request, socket);
|
||||
Response response = new Response(out.toByteArray());
|
||||
|
||||
byte[] fullData = loadTestData();
|
||||
byte[] partialData = new byte[fullData.length - 2000];
|
||||
System.arraycopy(fullData, 2000, partialData, 0, partialData.length);
|
||||
assertThat(response.data).isEqualTo(partialData);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,9 +15,14 @@ import static com.danikula.videocache.support.ProxyCacheTestUtils.ASSETS_DATA_BI
|
||||
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_SIZE;
|
||||
import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_URL;
|
||||
import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_URL_3_REDIRECTS;
|
||||
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.loadAssetFile;
|
||||
import static org.fest.assertions.api.Assertions.assertThat;
|
||||
import static org.fest.assertions.api.Assertions.fail;
|
||||
|
||||
/**
|
||||
* @author Alexey Danilov (danikula@gmail.com).
|
||||
@@ -58,7 +63,65 @@ public class HttpUrlSourceTest {
|
||||
@Test
|
||||
public void testFetchContentLength() throws Exception {
|
||||
Source source = new HttpUrlSource(HTTP_DATA_URL);
|
||||
assertThat(source.available()).isEqualTo(loadAssetFile(ASSETS_DATA_NAME).length);
|
||||
assertThat(source.length()).isEqualTo(loadAssetFile(ASSETS_DATA_NAME).length);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFetchInfoWithRedirect() throws Exception {
|
||||
HttpUrlSource source = new HttpUrlSource(HTTP_DATA_URL_ONE_REDIRECT);
|
||||
source.open(0);
|
||||
int available = source.length();
|
||||
String mime = source.getMime();
|
||||
source.close();
|
||||
|
||||
assertThat(available).isEqualTo(HTTP_DATA_SIZE);
|
||||
assertThat(mime).isEqualTo("image/jpeg");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFetchDataWithRedirect() throws Exception {
|
||||
HttpUrlSource source = new HttpUrlSource(HTTP_DATA_URL_ONE_REDIRECT);
|
||||
source.open(0);
|
||||
byte[] readData = new byte[HTTP_DATA_SIZE];
|
||||
source.read(readData);
|
||||
source.close();
|
||||
|
||||
byte[] expectedData = Arrays.copyOfRange(loadAssetFile(ASSETS_DATA_NAME), 0, HTTP_DATA_SIZE);
|
||||
assertThat(readData).isEqualTo(expectedData);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFetchPartialDataWithRedirect() throws Exception {
|
||||
int offset = 42;
|
||||
HttpUrlSource source = new HttpUrlSource(HTTP_DATA_URL_ONE_REDIRECT);
|
||||
source.open(offset);
|
||||
byte[] readData = new byte[HTTP_DATA_SIZE - offset];
|
||||
source.read(readData);
|
||||
source.close();
|
||||
|
||||
byte[] expectedData = Arrays.copyOfRange(loadAssetFile(ASSETS_DATA_NAME), offset, HTTP_DATA_SIZE);
|
||||
assertThat(readData).isEqualTo(expectedData);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFetchPartialDataWithMultiRedirects() throws Exception {
|
||||
int offset = 42;
|
||||
HttpUrlSource source = new HttpUrlSource(HTTP_DATA_URL_3_REDIRECTS);
|
||||
source.open(offset);
|
||||
byte[] readData = new byte[HTTP_DATA_SIZE - offset];
|
||||
source.read(readData);
|
||||
source.close();
|
||||
|
||||
byte[] expectedData = Arrays.copyOfRange(loadAssetFile(ASSETS_DATA_NAME), offset, HTTP_DATA_SIZE);
|
||||
assertThat(readData).isEqualTo(expectedData);
|
||||
}
|
||||
|
||||
@Ignore("To test it fairly we should disable caching connection.setUseCaches(false), but it will decrease performance")
|
||||
@Test(expected = ProxyCacheException.class)
|
||||
public void testExceedingRedirects() throws Exception {
|
||||
HttpUrlSource source = new HttpUrlSource(HTTP_DATA_URL_6_REDIRECTS);
|
||||
source.open(0);
|
||||
fail("Too many redirects");
|
||||
}
|
||||
|
||||
@Ignore("Seems Robolectric bug: MimeTypeMap.getFileExtensionFromUrl always returns null")
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package com.danikula.videocache;
|
||||
|
||||
import com.danikula.android.garden.io.IoUtils;
|
||||
import com.danikula.videocache.file.FileCache;
|
||||
import com.danikula.videocache.support.AngryHttpUrlSource;
|
||||
import com.danikula.videocache.support.PhlegmaticByteArraySource;
|
||||
import com.danikula.videocache.test.BuildConfig;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RobolectricGradleTestRunner;
|
||||
@@ -89,17 +91,6 @@ public class ProxyCacheTest {
|
||||
assertThat(fetchedData).isEqualTo(sourceCopy);
|
||||
}
|
||||
|
||||
@Test(expected = ProxyCacheException.class)
|
||||
public void testNoMoreSource() throws Exception {
|
||||
int sourceSize = 942;
|
||||
int cacheSize = 6157;
|
||||
ByteArraySource source = new ByteArraySource(generate(sourceSize));
|
||||
ByteArrayCache cache = new ByteArrayCache(generate(cacheSize));
|
||||
ProxyCache proxyCache = new ProxyCache(source, cache);
|
||||
proxyCache.read(new byte[sourceSize + cacheSize], sourceSize + cacheSize + 1, 10);
|
||||
Assert.fail();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProxyWithPhlegmaticSource() throws Exception {
|
||||
int dataSize = 100000;
|
||||
@@ -178,4 +169,18 @@ public class ProxyCacheTest {
|
||||
proxyCache.read(new byte[5], 19999, 5);
|
||||
assertThat(cache.isCompleted()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoTouchSource() throws Exception {
|
||||
int dataSize = 2000;
|
||||
byte[] data = generate(dataSize);
|
||||
File file = newCacheFile();
|
||||
IoUtils.saveToFile(data, file);
|
||||
ProxyCache proxyCache = new ProxyCache(new AngryHttpUrlSource(), new FileCache(file));
|
||||
|
||||
byte[] readData = new byte[dataSize];
|
||||
proxyCache.read(readData, 0, dataSize);
|
||||
|
||||
assertThat(readData).isEqualTo(data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
package com.danikula.videocache.file;
|
||||
|
||||
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;
|
||||
|
||||
import static com.danikula.android.garden.io.Files.cleanDirectory;
|
||||
import static com.danikula.android.garden.io.Files.createDirectory;
|
||||
import static org.fest.assertions.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for implementations of {@link DiskUsage}.
|
||||
*
|
||||
* @author Alexey Danilov (danikula@gmail.com).
|
||||
*/
|
||||
@RunWith(RobolectricGradleTestRunner.class)
|
||||
@Config(constants = BuildConfig.class, emulateSdk = BuildConfig.MIN_SDK_VERSION)
|
||||
public class DiskUsageTest {
|
||||
|
||||
private File cacheFolder;
|
||||
|
||||
@Before
|
||||
public void setup() throws Exception {
|
||||
cacheFolder = ProxyCacheTestUtils.newCacheFile();
|
||||
createDirectory(cacheFolder);
|
||||
cleanDirectory(cacheFolder);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMaxSizeCacheLimit() throws Exception {
|
||||
DiskUsage diskUsage = new TotalSizeLruDiskUsage(300);
|
||||
long now = System.currentTimeMillis();
|
||||
createFile(file("b"), 101, now - 10000);
|
||||
createFile(file("c"), 102, now - 8000);
|
||||
createFile(file("a"), 104, now - 4000); // exceeds
|
||||
|
||||
diskUsage.touch(file("c"));
|
||||
waitForAsyncTrimming();
|
||||
|
||||
assertThat(file("b")).doesNotExist();
|
||||
assertThat(file("c")).exists();
|
||||
assertThat(file("a")).exists();
|
||||
|
||||
createFile(file("d"), 500, now); // exceeds all
|
||||
diskUsage.touch(file("d"));
|
||||
waitForAsyncTrimming();
|
||||
|
||||
assertThat(file("a")).doesNotExist();
|
||||
assertThat(file("c")).doesNotExist();
|
||||
assertThat(file("d")).doesNotExist();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMaxFilesCount() throws Exception {
|
||||
DiskUsage diskUsage = new TotalCountLruDiskUsage(2);
|
||||
long now = System.currentTimeMillis();
|
||||
createFile(file("b"), 101, now - 10000);
|
||||
createFile(file("c"), 102, now - 8000);
|
||||
createFile(file("a"), 104, now - 4000);
|
||||
|
||||
diskUsage.touch(file("c"));
|
||||
waitForAsyncTrimming();
|
||||
|
||||
assertThat(file("b")).doesNotExist();
|
||||
assertThat(file("a")).exists();
|
||||
assertThat(file("c")).exists();
|
||||
|
||||
createFile(file("d"), 500, now);
|
||||
diskUsage.touch(file("d"));
|
||||
waitForAsyncTrimming();
|
||||
|
||||
assertThat(file("a")).doesNotExist();
|
||||
assertThat(file("c")).exists();
|
||||
assertThat(file("d")).exists();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTouch() throws Exception {
|
||||
DiskUsage diskUsage = new TotalCountLruDiskUsage(2);
|
||||
long now = System.currentTimeMillis();
|
||||
createFile(file("b"), 101, now - 10000);
|
||||
createFile(file("c"), 102, now - 8000);
|
||||
createFile(file("a"), 104, now - 4000);
|
||||
|
||||
diskUsage.touch(file("b"));
|
||||
waitForAsyncTrimming();
|
||||
|
||||
assertThat(file("b")).exists();
|
||||
assertThat(file("a")).exists();
|
||||
assertThat(file("c")).doesNotExist();
|
||||
|
||||
Thread.sleep(1000); // last modified is rounded to seconds, so wait for sec
|
||||
new TotalCountLruDiskUsage(1).touch(file("a"));
|
||||
waitForAsyncTrimming();
|
||||
|
||||
assertThat(file("a")).exists();
|
||||
assertThat(file("b")).doesNotExist();
|
||||
}
|
||||
|
||||
private void waitForAsyncTrimming() throws InterruptedException {
|
||||
Thread.sleep(200);
|
||||
}
|
||||
|
||||
private File file(String name) {
|
||||
return new File(cacheFolder, name);
|
||||
}
|
||||
|
||||
private void createFile(File file, int capacity, long lastModified) throws IOException {
|
||||
byte[] data = ProxyCacheTestUtils.generate(capacity);
|
||||
com.google.common.io.Files.write(data, file);
|
||||
boolean modified = file.setLastModified(lastModified);
|
||||
assertThat(modified).isTrue();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
package com.danikula.videocache;
|
||||
package com.danikula.videocache.file;
|
||||
|
||||
import com.danikula.android.garden.io.Files;
|
||||
import com.danikula.android.garden.io.IoUtils;
|
||||
import com.danikula.videocache.Cache;
|
||||
import com.danikula.videocache.ProxyCacheException;
|
||||
import com.danikula.videocache.test.BuildConfig;
|
||||
|
||||
import org.junit.Assert;
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.danikula.videocache.file;
|
||||
|
||||
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 static org.fest.assertions.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link Files}.
|
||||
*
|
||||
* @author Alexey Danilov (danikula@gmail.com).
|
||||
*/
|
||||
@RunWith(RobolectricGradleTestRunner.class)
|
||||
@Config(constants = BuildConfig.class, emulateSdk = BuildConfig.MIN_SDK_VERSION)
|
||||
public class FilesTest {
|
||||
|
||||
@Test
|
||||
public void testModify() throws Exception {
|
||||
byte[] data = ProxyCacheTestUtils.generate(999);
|
||||
File file = ProxyCacheTestUtils.newCacheFile();
|
||||
com.google.common.io.Files.write(data, file);
|
||||
long lastModified = file.lastModified();
|
||||
|
||||
Thread.sleep(1100); // file can store modification date in seconds. so wait for ~ 1 sec
|
||||
Files.modify(file);
|
||||
|
||||
assertThat(file).hasBinaryContent(data);
|
||||
assertThat(file.lastModified()).isGreaterThan(lastModified);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetModifiedNow() throws Exception {
|
||||
File file = ProxyCacheTestUtils.newCacheFile();
|
||||
com.google.common.io.Files.write(ProxyCacheTestUtils.generate(22), file);
|
||||
|
||||
Thread.sleep(1100); // file can store modification date in seconds. so wait for ~ 1 sec
|
||||
long nowRoundedToSecond = System.currentTimeMillis() / 1000 * 1000;
|
||||
Files.setLastModifiedNow(file);
|
||||
|
||||
assertThat(file.lastModified()).isGreaterThanOrEqualTo(nowRoundedToSecond);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testModifyZeroSizeFile() throws Exception {
|
||||
File file = ProxyCacheTestUtils.newCacheFile();
|
||||
boolean created = file.createNewFile();
|
||||
assertThat(created).isTrue();
|
||||
|
||||
Thread.sleep(1100); // file can store modification date in seconds. so wait for ~ 2 sec
|
||||
long nowRoundedToSecond = System.currentTimeMillis() / 1000 * 1000;
|
||||
Files.modify(file);
|
||||
|
||||
assertThat(file.lastModified()).isGreaterThanOrEqualTo(nowRoundedToSecond);
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,18 @@
|
||||
package com.danikula.videocache.support;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.danikula.videocache.HttpUrlSource;
|
||||
import com.danikula.videocache.ProxyCacheException;
|
||||
import com.danikula.videocache.Source;
|
||||
|
||||
/**
|
||||
* {@link HttpUrlSource} that throws exception in all methods.
|
||||
* {@link Source} that throws exception in all methods.
|
||||
*
|
||||
* @author Alexey Danilov (danikula@gmail.com).
|
||||
*/
|
||||
public class AngryHttpUrlSource extends HttpUrlSource {
|
||||
|
||||
public AngryHttpUrlSource(String url, String mime) {
|
||||
super(url, mime);
|
||||
}
|
||||
@Deprecated // use Mockito to throw error
|
||||
public class AngryHttpUrlSource implements Source {
|
||||
|
||||
@Override
|
||||
public int available() throws ProxyCacheException {
|
||||
public int length() throws ProxyCacheException {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
@@ -35,12 +30,4 @@ public class AngryHttpUrlSource extends HttpUrlSource {
|
||||
public int read(byte[] buffer) throws ProxyCacheException {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
public String getMime() throws ProxyCacheException {
|
||||
String mime = super.getMime();
|
||||
if (!TextUtils.isEmpty(mime)) {
|
||||
return mime;
|
||||
}
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import java.util.Random;
|
||||
/**
|
||||
* @author Alexey Danilov (danikula@gmail.com).
|
||||
*/
|
||||
@Deprecated // TODO: use Mockito to mock delay
|
||||
public class PhlegmaticByteArraySource extends ByteArraySource {
|
||||
|
||||
private final Random delayGenerator;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.danikula.videocache.support;
|
||||
|
||||
import com.danikula.android.garden.io.IoUtils;
|
||||
import com.danikula.videocache.HttpProxyCache;
|
||||
import com.danikula.videocache.HttpProxyCacheServer;
|
||||
import com.google.common.io.Files;
|
||||
|
||||
import org.robolectric.RuntimeEnvironment;
|
||||
@@ -21,7 +21,11 @@ import java.util.UUID;
|
||||
public class ProxyCacheTestUtils {
|
||||
|
||||
public static final String HTTP_DATA_URL = "https://dl.dropboxusercontent.com/u/15506779/persistent/proxycache/android.jpg";
|
||||
public static final String HTTP_DATA_URL_ONE_REDIRECT = "http://bit.ly/1V5PeY5";
|
||||
public static final String HTTP_DATA_URL_3_REDIRECTS = "http://bit.ly/1KvVmgZ";
|
||||
public static final String HTTP_DATA_URL_6_REDIRECTS = "http://ow.ly/SugRH";
|
||||
public static final String HTTP_DATA_BIG_URL = "https://dl.dropboxusercontent.com/u/15506779/persistent/proxycache/phones.jpg";
|
||||
public static final String HTTP_DATA_BIG_URL_ONE_REDIRECT = "http://bit.ly/1iJ69yA";
|
||||
public static final String ASSETS_DATA_NAME = "android.jpg";
|
||||
public static final String ASSETS_DATA_BIG_NAME = "phones.jpg";
|
||||
public static final int HTTP_DATA_SIZE = 4768;
|
||||
@@ -31,13 +35,13 @@ public class ProxyCacheTestUtils {
|
||||
return Files.asByteSource(file).read();
|
||||
}
|
||||
|
||||
public static Response readProxyResponse(HttpProxyCache proxy) throws IOException {
|
||||
return readProxyResponse(proxy, -1);
|
||||
public static Response readProxyResponse(HttpProxyCacheServer proxy, String url) throws IOException {
|
||||
return readProxyResponse(proxy, url, -1);
|
||||
}
|
||||
|
||||
public static Response readProxyResponse(HttpProxyCache proxy, int offset) throws IOException {
|
||||
URL url = new URL(proxy.getUrl());
|
||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
public static Response readProxyResponse(HttpProxyCacheServer proxy, String url, int offset) throws IOException {
|
||||
URL proxiedUrl = new URL(proxy.getProxyUrl(url));
|
||||
HttpURLConnection connection = (HttpURLConnection) proxiedUrl.openConnection();
|
||||
try {
|
||||
if (offset >= 0) {
|
||||
connection.setRequestProperty("Range", "bytes=" + offset + "-");
|
||||
@@ -48,6 +52,10 @@ public class ProxyCacheTestUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] loadTestData() throws IOException {
|
||||
return loadAssetFile(ASSETS_DATA_NAME);
|
||||
}
|
||||
|
||||
public static byte[] loadAssetFile(String name) throws IOException {
|
||||
InputStream in = RuntimeEnvironment.application.getResources().getAssets().open(name);
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
|
||||
@@ -1,14 +1,27 @@
|
||||
package com.danikula.videocache.support;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.io.ByteStreams;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class Response {
|
||||
|
||||
private static final String CONTENT_TYPE_HEADER = "Content-Type";
|
||||
private static final String CONTENT_LENGTH_HEADER = "Content-Length";
|
||||
private static final Pattern STATUS_CODE_PATTERN = Pattern.compile("HTTP/1.1 (\\d{3}) ");
|
||||
|
||||
public final int code;
|
||||
public final byte[] data;
|
||||
public final int contentLength;
|
||||
@@ -22,4 +35,33 @@ public class Response {
|
||||
this.headers = connection.getHeaderFields();
|
||||
this.data = ByteStreams.toByteArray(connection.getInputStream());
|
||||
}
|
||||
|
||||
public Response(byte[] responseData) throws IOException {
|
||||
int read = 0;
|
||||
BufferedReader reader = new BufferedReader(new StringReader(new String(responseData, "ascii")));
|
||||
String statusLine = reader.readLine();
|
||||
read += statusLine.length() + 1;
|
||||
Matcher matcher = STATUS_CODE_PATTERN.matcher(statusLine);
|
||||
boolean hasCode = matcher.find();
|
||||
Preconditions.checkArgument(hasCode, "Status code not found in `" + statusLine + "`");
|
||||
this.code = Integer.parseInt(matcher.group(1));
|
||||
|
||||
String header;
|
||||
this.headers = new HashMap<>();
|
||||
while (!TextUtils.isEmpty(header = reader.readLine())) {
|
||||
read += header.length() + 1;
|
||||
String[] keyValue = header.split(":");
|
||||
String headerName = keyValue[0].trim();
|
||||
String headerValue = keyValue[1].trim();
|
||||
headers.put(headerName, Collections.singletonList(headerValue));
|
||||
}
|
||||
read++;
|
||||
|
||||
this.contentType = headers.containsKey(CONTENT_TYPE_HEADER) ? headers.get(CONTENT_TYPE_HEADER).get(0) : null;
|
||||
this.contentLength = headers.containsKey(CONTENT_LENGTH_HEADER) ? Integer.parseInt(headers.get(CONTENT_LENGTH_HEADER).get(0)) : -1;
|
||||
|
||||
int bodySize = responseData.length - read;
|
||||
this.data = new byte[bodySize];
|
||||
System.arraycopy(responseData, read, data, 0, bodySize);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user