27 Commits

Author SHA1 Message Date
Alexey Danilov
9b5193c38f 📦 release 2.6.4: hide closing output stream error stacktrace 2016-12-03 12:08:25 +03:00
Alexey Danilov
8603d5ef7f add travis and bintray badges 2016-11-15 15:45:14 +03:00
Alexey Danilov
b8a6110aa8 add travis support 2016-11-15 15:27:10 +03:00
Alexey Danilov
9353cde8f4 📦 2.6.3: return file:// proxy uri for fully cached files 2016-09-27 22:29:29 +03:00
Alexey Danilov
0d1131b0fc ping server each time before wrapping to proxy url 2016-09-27 20:10:43 +03:00
Alexey Danilov
44318d87d9 📦 2.6.2: use slf4j for logging 2016-09-27 16:03:01 +03:00
Alexey Danilov
76c13ba827 📦 release 2.6.1. fix 🐛 #87 : work with Arabic locale 2016-09-05 18:39:21 +03:00
Alexey Danilov
b94e829448 Merge pull request #85 from Hsiny/Hsiny-patch-1
fix do not close socket output correctly
2016-08-31 11:30:03 +03:00
XinYang
293d2d4089 fix do not close socket output correctly 2016-08-29 21:08:50 +08:00
Alexey Danilov
99b8330679 📦 release 2.6.0 2016-08-26 13:14:47 +03:00
Alexey Danilov
0f522f200d notify CacheListener after cache is completed with original file 2016-08-26 13:10:19 +03:00
Alexey Danilov
71c6301eb4 🤘 cache source's info in db to increase performance (#41, #45) 2016-08-05 14:36:25 +03:00
Alexey Danilov
372542c2dc update robolectric 3.0.rc-2 -> 3.1 2016-08-04 22:07:27 +03:00
Alexey Danilov
22cfddb2c9 add table of content, known problems to readme 2016-08-04 18:33:58 +03:00
Alexey Danilov
8fc4a9ac7d 📦 release 2.5.0 2016-07-29 17:30:16 +03:00
Alexey Danilov
6c996ea66c 🐛 don't close source after processing partial request without cache #43
prevent invalid calling source.close() in different threads to avoid crashes on Lollipop (#37, #29, #63, #66)
2016-07-29 14:09:11 +03:00
wlq
582832f8b5 fix default cache size 2016-07-28 23:01:03 +03:00
Alexey Danilov
cd0b4111a4 ability to check was content fully cached to file or not 2016-07-27 23:14:06 +03:00
Alexey Danilov
eb67640212 update readme 2016-07-27 21:07:35 +03:00
Alexey Danilov
1d50807fe0 fix HttpUrlSourceTest test 2016-07-27 20:56:21 +03:00
Alexey Danilov
6da9650030 update build tools 2016-07-27 20:36:41 +03:00
Alexey Danilov
a1d00fec7b use static files for tests and sample from github instead of dropbox 2016-03-03 14:13:46 +03:00
Alexey Danilov
f5a779266d add test files 2016-03-03 13:50:01 +03:00
Alexey Danilov
ca6f36ada3 update the latest version in README 2015-12-08 19:06:07 +03:00
Arthur Alves
33b7e5e9d3 Catch NullPointerException to avoid crash due to unsupported async disconnects on Android L (which uses an outdated okhttp version) 2015-12-08 18:59:37 +03:00
Alexey Danilov
0aad13f118 fix dividing by zero while loading empty file (#26) 2015-11-06 13:43:08 +03:00
Alexey Danilov
a270460c8d fix fetching content info (#27) 2015-10-29 16:14:20 +03:00
64 changed files with 1218 additions and 380 deletions

8
.travis.yml Normal file
View File

@@ -0,0 +1,8 @@
language: android
android:
components:
- platform-tools
- tools
- build-tools-22.0.1
- android-23
- extra

View File

@@ -1,5 +1,20 @@
# Video cache support for Android
[![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-AndroidVideoCache-brightgreen.svg?style=flat)](http://android-arsenal.com/details/1/1751)
## Video cache support for Android
[![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-AndroidVideoCache-brightgreen.svg?style=flat)](http://android-arsenal.com/details/1/1751) [![Build Status](https://api.travis-ci.org/danikula/AndroidVideoCache.svg?branch=master)](https://travis-ci.org/danikula/AndroidVideoCache/) [ ![Download](https://api.bintray.com/packages/alexeydanilov/maven/videocache/images/download.svg) ](https://bintray.com/alexeydanilov/maven/videocache/_latestVersion)
## Table of Content
- [Why AndroidVideoCache?](#why-androidvideocache)
- [Features](#features)
- [Get started](#get-started)
- [Recipes](#recipes)
- [Disk cache limit](#disk-cache-limit)
- [Listen caching progress](#listen-caching-progress)
- [Sample](#sample)
- [Known problems](#known-problems)
- [Whats new](#whats-new)
- [Code contributions](#code-contributions)
- [Where published?](#where-published)
- [Questions?](#questions)
- [License](#license)
## Why AndroidVideoCache?
Because there is no sense to download video a lot of times while streaming!
@@ -14,14 +29,11 @@ Because there is no sense to download video a lot of times while streaming!
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?
## Get started
Just add dependency (`AndroidVideoCache` is available in jcenter):
```
repositories {
jcenter()
}
dependencies {
compile 'com.danikula:videocache:2.3.2'
compile 'com.danikula:videocache:2.6.4'
}
```
@@ -64,6 +76,8 @@ public class App extends Application {
or use [simple factory](http://pastebin.com/s2fafSYS).
More preferable way is use some dependency injector like [Dagger](http://square.github.io/dagger/).
## Recipes
### Disk cache limit
By default `HttpProxyCacheServer` uses 512Mb for caching files. You can change this value:
```java
@@ -82,9 +96,20 @@ private HttpProxyCacheServer newProxy() {
.maxCacheFilesCount(20)
.build();
}
```
```
See `sample` app for details.
### Listen caching progress
Use `HttpProxyCacheServer.registerCacheListener(CacheListener listener)` method to set listener with callback `onCacheAvailable(File cacheFile, String url, int percentsAvailable)` to be aware of caching progress. Do not forget to to unsubscribe listener with help of `HttpProxyCacheServer.unregisterCacheListener(CacheListener listener)` method to avoid memory leaks.
Use `HttpProxyCacheServer.isCached(String url)` method to check was url's content fully cached to file or not.
See `sample` app for more details.
### Sample
See `sample` app.
## Known problems
`AndroidVideoCache` [doesn't work](https://github.com/danikula/AndroidVideoCache/issues/28) if wifi or mobile internet connection uses proxy.
## Whats new
See Release Notes [here](https://github.com/danikula/AndroidVideoCache/releases)
@@ -94,9 +119,11 @@ If it's a feature that you think would need to be discussed please open an issue
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 :)
3. Fix a problem. Your code **must** contain test for reproducing problem. Your tests **must be passed** with help of your fix
4. Push your changes to your new branch (git push origin my_branch)
5. Initiate a [pull request](http://help.github.com/send-pull-requests/) on github
6. Rebase [master branch](https://github.com/danikula/AndroidVideoCache) if your local branch is not actual. Merging is not acceptable, only rebase
6. Your pull request will be reviewed and hopefully merged :)
## Where published?
[Here](https://bintray.com/alexeydanilov/maven/videocache/view)
@@ -106,7 +133,7 @@ If it's a feature that you think would need to be discussed please open an issue
## License
Copyright 2014-2015 Alexey Danilov
Copyright 2014-2016 Alexey Danilov
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@@ -3,7 +3,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.3.0'
classpath 'com.android.tools.build:gradle:2.1.3'
}
}

BIN
files/android Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
files/android.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
files/devbytes.mp4 Normal file

Binary file not shown.

0
files/empty.txt Normal file
View File

BIN
files/orange1.mp4 Normal file

Binary file not shown.

BIN
files/orange2.mp4 Normal file

Binary file not shown.

BIN
files/orange3.mp4 Normal file

Binary file not shown.

BIN
files/orange4.mp4 Normal file

Binary file not shown.

BIN
files/orange5.mp4 Normal file

Binary file not shown.

BIN
files/phones.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

BIN
files/space.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

@@ -1,6 +1,6 @@
#Wed Apr 10 15:27:10 PDT 2013
#Mon Sep 05 18:06:17 MSK 2016
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ package com.danikula.videocache;
import com.danikula.videocache.file.DiskUsage;
import com.danikula.videocache.file.FileNameGenerator;
import com.danikula.videocache.sourcestorage.SourceInfoStorage;
import java.io.File;
@@ -15,11 +16,13 @@ class Config {
public final File cacheRoot;
public final FileNameGenerator fileNameGenerator;
public final DiskUsage diskUsage;
public final SourceInfoStorage sourceInfoStorage;
Config(File cacheRoot, FileNameGenerator fileNameGenerator, DiskUsage diskUsage) {
Config(File cacheRoot, FileNameGenerator fileNameGenerator, DiskUsage diskUsage, SourceInfoStorage sourceInfoStorage) {
this.cacheRoot = cacheRoot;
this.fileNameGenerator = fileNameGenerator;
this.diskUsage = diskUsage;
this.sourceInfoStorage = sourceInfoStorage;
}
File generateCacheFile(String url) {

View File

@@ -83,25 +83,25 @@ class HttpProxyCache extends ProxyCache {
}
private void responseWithoutCache(OutputStream out, long offset) throws ProxyCacheException, IOException {
HttpUrlSource newSourceNoCache = new HttpUrlSource(this.source);
try {
HttpUrlSource source = new HttpUrlSource(this.source);
source.open((int) offset);
newSourceNoCache.open((int) offset);
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
int readBytes;
while ((readBytes = source.read(buffer)) != -1) {
while ((readBytes = newSourceNoCache.read(buffer)) != -1) {
out.write(buffer, 0, readBytes);
offset += readBytes;
}
out.flush();
} finally {
source.close();
newSourceNoCache.close();
}
}
@Override
protected void onCachePercentsAvailableChanged(int percents) {
if (listener != null) {
listener.onCacheAvailable(cache.file, source.url, percents);
listener.onCacheAvailable(cache.file, source.getUrl(), percents);
}
}
}

View File

@@ -1,37 +1,34 @@
package com.danikula.videocache;
import android.content.Context;
import android.os.SystemClock;
import android.util.Log;
import android.net.Uri;
import com.danikula.videocache.file.DiskUsage;
import com.danikula.videocache.file.FileNameGenerator;
import com.danikula.videocache.file.Md5FileNameGenerator;
import com.danikula.videocache.file.TotalCountLruDiskUsage;
import com.danikula.videocache.file.TotalSizeLruDiskUsage;
import com.danikula.videocache.sourcestorage.SourceInfoStorage;
import com.danikula.videocache.sourcestorage.SourceInfoStorageFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.util.Arrays;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeoutException;
import static com.danikula.videocache.Preconditions.checkAllNotNull;
import static com.danikula.videocache.Preconditions.checkNotNull;
import static com.danikula.videocache.ProxyCacheUtils.LOG_TAG;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
/**
* Simple lightweight proxy server with file caching support that handles HTTP requests.
@@ -39,24 +36,23 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS;
* <pre><code>
* public onCreate(Bundle state) {
* super.onCreate(state);
* <p/>
*
* HttpProxyCacheServer proxy = getProxy();
* String proxyUrl = proxy.getProxyUrl(VIDEO_URL);
* videoView.setVideoPath(proxyUrl);
* }
* <p/>
*
* private HttpProxyCacheServer getProxy() {
* // should return single instance of HttpProxyCacheServer shared for whole app.
* }
* <code/></pre>
* </code></pre>
*
* @author Alexey Danilov (danikula@gmail.com).
*/
public class HttpProxyCacheServer {
private static final Logger LOG = LoggerFactory.getLogger("HttpProxyCacheServer");
private static final String PROXY_HOST = "127.0.0.1";
private static final String PING_REQUEST = "ping";
private static final String PING_RESPONSE = "ping ok";
private final Object clientsLock = new Object();
private final ExecutorService socketProcessor = Executors.newFixedThreadPool(8);
@@ -65,7 +61,7 @@ public class HttpProxyCacheServer {
private final int port;
private final Thread waitConnectionThread;
private final Config config;
private boolean pinged;
private final Pinger pinger;
public HttpProxyCacheServer(Context context) {
this(new Builder(context).buildConfig());
@@ -81,65 +77,46 @@ public class HttpProxyCacheServer {
this.waitConnectionThread = new Thread(new WaitRequestsRunnable(startSignal));
this.waitConnectionThread.start();
startSignal.await(); // freeze thread, wait for server starts
Log.i(LOG_TAG, "Proxy cache server started. Ping it...");
makeSureServerWorks();
this.pinger = new Pinger(PROXY_HOST, port);
LOG.info("Proxy cache server started. Is it alive? " + isAlive());
} catch (IOException | InterruptedException e) {
socketProcessor.shutdown();
throw new IllegalStateException("Error starting local proxy server", e);
}
}
private void makeSureServerWorks() {
int maxPingAttempts = 3;
int delay = 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();
}
}
/**
* Returns url that wrap original url and should be used for client (MediaPlayer, ExoPlayer, etc).
* <p>
* If file for this url is fully cached (it means method {@link #isCached(String)} returns {@code true})
* then file:// uri to cached file will be returned.
* <p>
* Calling this method has same effect as calling {@link #getProxyUrl(String, boolean)} with 2nd parameter set to {@code true}.
*
* @param url a url to file that should be cached.
* @return a wrapped by proxy url if file is not fully cached or url pointed to cache file otherwise.
*/
public String getProxyUrl(String url) {
if (!pinged) {
Log.e(LOG_TAG, "Proxy server isn't pinged. Caching doesn't work. If you see this message, please, email me danikula@gmail.com");
}
return pinged ? appendToProxyUrl(url) : url;
return getProxyUrl(url, true);
}
private String appendToProxyUrl(String url) {
return String.format("http://%s:%d/%s", PROXY_HOST, port, ProxyCacheUtils.encode(url));
/**
* Returns url that wrap original url and should be used for client (MediaPlayer, ExoPlayer, etc).
* <p>
* If parameter {@code allowCachedFileUri} is {@code true} and file for this url is fully cached
* (it means method {@link #isCached(String)} returns {@code true}) then file:// uri to cached file will be returned.
*
* @param url a url to file that should be cached.
* @param allowCachedFileUri {@code true} if allow to return file:// uri if url is fully cached
* @return a wrapped by proxy url if file is not fully cached or url pointed to cache file otherwise (if {@code allowCachedFileUri} is {@code true}).
*/
public String getProxyUrl(String url, boolean allowCachedFileUri) {
if (allowCachedFileUri && isCached(url)) {
File cacheFile = getCacheFile(url);
touchFileSafely(cacheFile);
return Uri.fromFile(cacheFile).toString();
}
return isAlive() ? appendToProxyUrl(url) : url;
}
public void registerCacheListener(CacheListener cacheListener, String url) {
@@ -148,7 +125,7 @@ public class HttpProxyCacheServer {
try {
getClients(url).registerCacheListener(cacheListener);
} catch (ProxyCacheException e) {
Log.d(LOG_TAG, "Error registering cache listener", e);
LOG.warn("Error registering cache listener", e);
}
}
}
@@ -159,7 +136,7 @@ public class HttpProxyCacheServer {
try {
getClients(url).unregisterCacheListener(cacheListener);
} catch (ProxyCacheException e) {
Log.d(LOG_TAG, "Error registering cache listener", e);
LOG.warn("Error registering cache listener", e);
}
}
}
@@ -173,11 +150,24 @@ public class HttpProxyCacheServer {
}
}
/**
* Checks is cache contains fully cached file for particular url.
*
* @param url an url cache file will be checked for.
* @return {@code true} if cache contains fully cached file for passed in parameters url.
*/
public boolean isCached(String url) {
checkNotNull(url, "Url can't be null!");
return getCacheFile(url).exists();
}
public void shutdown() {
Log.i(LOG_TAG, "Shutdown proxy server");
LOG.info("Shutdown proxy server");
shutdownClients();
config.sourceInfoStorage.release();
waitConnectionThread.interrupt();
try {
if (!serverSocket.isClosed()) {
@@ -188,6 +178,28 @@ public class HttpProxyCacheServer {
}
}
private boolean isAlive() {
return pinger.ping(3, 70); // 70+140+280=max~500ms
}
private String appendToProxyUrl(String url) {
return String.format(Locale.US, "http://%s:%d/%s", PROXY_HOST, port, ProxyCacheUtils.encode(url));
}
private File getCacheFile(String url) {
File cacheDir = config.cacheRoot;
String fileName = config.fileNameGenerator.generate(url);
return new File(cacheDir, fileName);
}
private void touchFileSafely(File cacheFile) {
try {
config.diskUsage.touch(cacheFile);
} catch (IOException e) {
LOG.error("Error touching file " + cacheFile, e);
}
}
private void shutdownClients() {
synchronized (clientsLock) {
for (HttpProxyCacheServerClients clients : clientsMap.values()) {
@@ -201,7 +213,7 @@ public class HttpProxyCacheServer {
try {
while (!Thread.currentThread().isInterrupted()) {
Socket socket = serverSocket.accept();
Log.d(LOG_TAG, "Accept new socket " + socket);
LOG.debug("Accept new socket " + socket);
socketProcessor.submit(new SocketProcessorRunnable(socket));
}
} catch (IOException e) {
@@ -212,10 +224,10 @@ public class HttpProxyCacheServer {
private void processSocket(Socket socket) {
try {
GetRequest request = GetRequest.read(socket.getInputStream());
Log.i(LOG_TAG, "Request to cache proxy:" + request);
LOG.debug("Request to cache proxy:" + request);
String url = ProxyCacheUtils.decode(request.uri);
if (PING_REQUEST.equals(url)) {
responseToPing(socket);
if (pinger.isPingRequest(url)) {
pinger.responseToPing(socket);
} else {
HttpProxyCacheServerClients clients = getClients(url);
clients.processRequest(request, socket);
@@ -223,21 +235,15 @@ public class HttpProxyCacheServer {
} catch (SocketException e) {
// There is no way to determine that client closed connection http://stackoverflow.com/a/10241044/999458
// So just to prevent log flooding don't log stacktrace
Log.d(LOG_TAG, "Closing socket… Socket is closed by client.");
LOG.debug("Closing socket… Socket is closed by client.");
} catch (ProxyCacheException | IOException e) {
onError(new ProxyCacheException("Error processing request", e));
} finally {
releaseSocket(socket);
Log.d(LOG_TAG, "Opened connections: " + getClientsCount());
LOG.debug("Opened connections: " + getClientsCount());
}
}
private void responseToPing(Socket socket) throws IOException {
OutputStream out = socket.getOutputStream();
out.write("HTTP/1.1 200 OK\n\n".getBytes());
out.write(PING_RESPONSE.getBytes());
}
private HttpProxyCacheServerClients getClients(String url) throws ProxyCacheException {
synchronized (clientsLock) {
HttpProxyCacheServerClients clients = clientsMap.get(url);
@@ -273,7 +279,7 @@ public class HttpProxyCacheServer {
} catch (SocketException e) {
// There is no way to determine that client closed connection http://stackoverflow.com/a/10241044/999458
// So just to prevent log flooding don't log stacktrace
Log.d(LOG_TAG, "Releasing input stream… Socket is closed by client.");
LOG.debug("Releasing input stream… Socket is closed by client.");
} catch (IOException e) {
onError(new ProxyCacheException("Error closing socket input stream", e));
}
@@ -281,11 +287,11 @@ public class HttpProxyCacheServer {
private void closeSocketOutput(Socket socket) {
try {
if (socket.isOutputShutdown()) {
if (!socket.isOutputShutdown()) {
socket.shutdownOutput();
}
} catch (IOException e) {
onError(new ProxyCacheException("Error closing socket output stream", e));
LOG.warn("Failed to close socket on proxy side: {}. It seems client have already closed connection.", e.getMessage());
}
}
@@ -300,7 +306,7 @@ public class HttpProxyCacheServer {
}
private void onError(Throwable e) {
Log.e(LOG_TAG, "HttpProxyCacheServer error", e);
LOG.error("HttpProxyCacheServer error", e);
}
private final class WaitRequestsRunnable implements Runnable {
@@ -332,26 +338,20 @@ public class HttpProxyCacheServer {
}
}
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 static final long DEFAULT_MAX_SIZE = 512 * 1024 * 1024;
private File cacheRoot;
private FileNameGenerator fileNameGenerator;
private DiskUsage diskUsage;
private SourceInfoStorage sourceInfoStorage;
public Builder(Context context) {
this.sourceInfoStorage = SourceInfoStorageFactory.newSourceInfoStorage(context);
this.cacheRoot = StorageUtils.getIndividualCacheDirectory(context);
this.diskUsage = new TotalSizeLruDiskUsage(DEFAULT_MAX_SIZE);
this.fileNameGenerator = new Md5FileNameGenerator();
@@ -359,11 +359,11 @@ public class HttpProxyCacheServer {
/**
* Overrides default cache folder to be used for caching files.
* <p/>
* <p>
* By default AndroidVideoCache uses
* '/Android/data/[app_package_name]/cache/video-cache/' if card is mounted and app has appropriate permission
* or 'video-cache' subdirectory in default application's cache directory otherwise.
* <p/>
* </p>
* <b>Note</b> directory must be used <b>only</b> for AndroidVideoCache files.
*
* @param file a cache directory, can't be null.
@@ -387,9 +387,10 @@ public class HttpProxyCacheServer {
/**
* Sets max cache size in bytes.
* <p>
* All files that exceeds limit will be deleted using LRU strategy.
* Default value is 512 Mb.
* <p/>
* </p>
* Note this method overrides result of calling {@link #maxCacheFilesCount(int)}
*
* @param maxSize max cache size in bytes.
@@ -403,7 +404,6 @@ public class HttpProxyCacheServer {
/**
* Sets max cache files count.
* All files that exceeds limit will be deleted using LRU strategy.
* <p/>
* Note this method overrides result of calling {@link #maxCacheSize(long)}
*
* @param count max cache files count.
@@ -425,7 +425,7 @@ public class HttpProxyCacheServer {
}
private Config buildConfig() {
return new Config(cacheRoot, fileNameGenerator, diskUsage);
return new Config(cacheRoot, fileNameGenerator, diskUsage, sourceInfoStorage);
}
}

View File

@@ -79,7 +79,7 @@ final class HttpProxyCacheServerClients {
}
private HttpProxyCache newHttpProxyCache() throws ProxyCacheException {
HttpUrlSource source = new HttpUrlSource(url);
HttpUrlSource source = new HttpUrlSource(url, config.sourceInfoStorage);
FileCache cache = new FileCache(config.generateCacheFile(url), config.diskUsage);
HttpProxyCache httpProxyCache = new HttpProxyCache(source, cache);
httpProxyCache.registerCacheListener(uiCacheListener);

View File

@@ -1,7 +1,12 @@
package com.danikula.videocache;
import android.text.TextUtils;
import android.util.Log;
import com.danikula.videocache.sourcestorage.SourceInfoStorage;
import com.danikula.videocache.sourcestorage.SourceInfoStorageFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedInputStream;
import java.io.IOException;
@@ -10,8 +15,8 @@ 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;
@@ -25,87 +30,100 @@ import static java.net.HttpURLConnection.HTTP_SEE_OTHER;
*/
public class HttpUrlSource implements Source {
private static final Logger LOG = LoggerFactory.getLogger("HttpUrlSource");
private static final int MAX_REDIRECTS = 5;
public final String url;
private final SourceInfoStorage sourceInfoStorage;
private SourceInfo sourceInfo;
private HttpURLConnection connection;
private InputStream inputStream;
private volatile int length = Integer.MIN_VALUE;
private volatile String mime;
public HttpUrlSource(String url) {
this(url, ProxyCacheUtils.getSupposablyMime(url));
this(url, SourceInfoStorageFactory.newEmptySourceInfoStorage());
}
public HttpUrlSource(String url, String mime) {
this.url = Preconditions.checkNotNull(url);
this.mime = mime;
public HttpUrlSource(String url, SourceInfoStorage sourceInfoStorage) {
this.sourceInfoStorage = checkNotNull(sourceInfoStorage);
SourceInfo sourceInfo = sourceInfoStorage.get(url);
this.sourceInfo = sourceInfo != null ? sourceInfo :
new SourceInfo(url, Integer.MIN_VALUE, ProxyCacheUtils.getSupposablyMime(url));
}
public HttpUrlSource(HttpUrlSource source) {
this.url = source.url;
this.mime = source.mime;
this.length = source.length;
this.sourceInfo = source.sourceInfo;
this.sourceInfoStorage = source.sourceInfoStorage;
}
@Override
public synchronized int length() throws ProxyCacheException {
if (length == Integer.MIN_VALUE) {
if (sourceInfo.length == Integer.MIN_VALUE) {
fetchContentInfo();
}
return length;
return sourceInfo.length;
}
@Override
public void open(int offset) throws ProxyCacheException {
try {
connection = openConnection(offset, "GET", -1);
mime = connection.getContentType();
connection = openConnection(offset, -1);
String mime = connection.getContentType();
inputStream = new BufferedInputStream(connection.getInputStream(), DEFAULT_BUFFER_SIZE);
length = readSourceAvailableBytes(connection, offset, connection.getResponseCode());
int length = readSourceAvailableBytes(connection, offset, connection.getResponseCode());
this.sourceInfo = new SourceInfo(sourceInfo.url, length, mime);
this.sourceInfoStorage.put(sourceInfo.url, sourceInfo);
} catch (IOException e) {
throw new ProxyCacheException("Error opening connection for " + url + " with offset " + offset, e);
throw new ProxyCacheException("Error opening connection for " + sourceInfo.url + " with offset " + offset, e);
}
}
private int readSourceAvailableBytes(HttpURLConnection connection, int offset, int responseCode) throws IOException {
int contentLength = connection.getContentLength();
return responseCode == HTTP_OK ? contentLength
: responseCode == HTTP_PARTIAL ? contentLength + offset : length;
: responseCode == HTTP_PARTIAL ? contentLength + offset : sourceInfo.length;
}
@Override
public void close() throws ProxyCacheException {
if (connection != null) {
connection.disconnect();
try {
connection.disconnect();
} catch (NullPointerException | IllegalArgumentException e) {
String message = "Wait... but why? WTF!? " +
"Really shouldn't happen any more after fixing https://github.com/danikula/AndroidVideoCache/issues/43. " +
"If you read it on your device log, please, notify me danikula@gmail.com or create issue here https://github.com/danikula/AndroidVideoCache/issues.";
throw new RuntimeException(message, e);
}
}
}
@Override
public int read(byte[] buffer) throws ProxyCacheException {
if (inputStream == null) {
throw new ProxyCacheException("Error reading data from " + url + ": connection is absent!");
throw new ProxyCacheException("Error reading data from " + sourceInfo.url + ": connection is absent!");
}
try {
return inputStream.read(buffer, 0, buffer.length);
} catch (InterruptedIOException e) {
throw new InterruptedProxyCacheException("Reading source " + url + " is interrupted", e);
throw new InterruptedProxyCacheException("Reading source " + sourceInfo.url + " is interrupted", e);
} catch (IOException e) {
throw new ProxyCacheException("Error reading data from " + url, e);
throw new ProxyCacheException("Error reading data from " + sourceInfo.url, e);
}
}
private void fetchContentInfo() throws ProxyCacheException {
Log.d(LOG_TAG, "Read content info from " + url);
LOG.debug("Read content info from " + sourceInfo.url);
HttpURLConnection urlConnection = null;
InputStream inputStream = null;
try {
urlConnection = openConnection(0, "HEAD", 10000);
length = urlConnection.getContentLength();
mime = urlConnection.getContentType();
urlConnection = openConnection(0, 10000);
int length = urlConnection.getContentLength();
String mime = urlConnection.getContentType();
inputStream = urlConnection.getInputStream();
Log.i(LOG_TAG, "Content info for `" + url + "`: mime: " + mime + ", content-length: " + length);
this.sourceInfo = new SourceInfo(sourceInfo.url, length, mime);
this.sourceInfoStorage.put(sourceInfo.url, sourceInfo);
LOG.debug("Source info fetched: " + sourceInfo);
} catch (IOException e) {
Log.e(LOG_TAG, "Error fetching info from " + url, e);
LOG.error("Error fetching info from " + sourceInfo.url, e);
} finally {
ProxyCacheUtils.close(inputStream);
if (urlConnection != null) {
@@ -114,15 +132,14 @@ public class HttpUrlSource implements Source {
}
}
private HttpURLConnection openConnection(int offset, String method, int timeout) throws IOException, ProxyCacheException {
private HttpURLConnection openConnection(int offset, int timeout) throws IOException, ProxyCacheException {
HttpURLConnection connection;
boolean redirected;
int redirectCount = 0;
String url = this.url;
String url = this.sourceInfo.url;
do {
Log.d(LOG_TAG, "Open connection " + (offset > 0 ? " with offset " + offset : "") + " to " + url);
LOG.debug("Open connection " + (offset > 0 ? " with offset " + offset : "") + " to " + url);
connection = (HttpURLConnection) new URL(url).openConnection();
connection.setRequestMethod(method);
if (offset > 0) {
connection.setRequestProperty("Range", "bytes=" + offset + "-");
}
@@ -145,18 +162,18 @@ public class HttpUrlSource implements Source {
}
public synchronized String getMime() throws ProxyCacheException {
if (TextUtils.isEmpty(mime)) {
if (TextUtils.isEmpty(sourceInfo.mime)) {
fetchContentInfo();
}
return mime;
return sourceInfo.mime;
}
public String getUrl() {
return url;
return sourceInfo.url;
}
@Override
public String toString() {
return "HttpUrlSource{url='" + url + "}";
return "HttpUrlSource{sourceInfo='" + sourceInfo + "}";
}
}

View File

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

View File

@@ -1,15 +1,15 @@
package com.danikula.videocache;
final class Preconditions {
public final class Preconditions {
static <T> T checkNotNull(T reference) {
public static <T> T checkNotNull(T reference) {
if (reference == null) {
throw new NullPointerException();
}
return reference;
}
static void checkAllNotNull(Object... references) {
public static void checkAllNotNull(Object... references) {
for (Object reference : references) {
if (reference == null) {
throw new NullPointerException();
@@ -17,7 +17,7 @@ final class Preconditions {
}
}
static <T> T checkNotNull(T reference, String errorMessage) {
public static <T> T checkNotNull(T reference, String errorMessage) {
if (reference == null) {
throw new NullPointerException(errorMessage);
}

View File

@@ -1,11 +1,11 @@
package com.danikula.videocache;
import android.util.Log;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.atomic.AtomicInteger;
import static com.danikula.videocache.Preconditions.checkNotNull;
import static com.danikula.videocache.ProxyCacheUtils.LOG_TAG;
/**
* Proxy for {@link Source} with caching support ({@link Cache}).
@@ -18,6 +18,7 @@ import static com.danikula.videocache.ProxyCacheUtils.LOG_TAG;
*/
class ProxyCache {
private static final Logger LOG = LoggerFactory.getLogger("ProxyCache");
private static final int MAX_READ_SOURCE_ATTEMPTS = 1;
private final Source source;
@@ -61,7 +62,7 @@ class ProxyCache {
public void shutdown() {
synchronized (stopLock) {
Log.d(LOG_TAG, "Shutdown proxy for " + source);
LOG.debug("Shutdown proxy for " + source);
try {
stopped = true;
if (sourceReaderThread != null) {
@@ -100,10 +101,11 @@ class ProxyCache {
}
}
protected void onCacheAvailable(long cacheAvailable, long sourceAvailable) {
int percents = (int) (cacheAvailable * 100 / sourceAvailable);
protected void onCacheAvailable(long cacheAvailable, long sourceLength) {
boolean zeroLengthSource = sourceLength == 0;
int percents = zeroLengthSource ? 100 : (int) (cacheAvailable * 100 / sourceLength);
boolean percentsChanged = percents != percentsAvailable;
boolean sourceLengthKnown = sourceAvailable >= 0;
boolean sourceLengthKnown = sourceLength >= 0;
if (sourceLengthKnown && percentsChanged) {
onCachePercentsAvailableChanged(percents);
}
@@ -133,6 +135,7 @@ class ProxyCache {
notifyNewCacheDataAvailable(offset, sourceAvailable);
}
tryComplete();
onSourceRead();
} catch (Throwable e) {
readSourceErrorsCount.incrementAndGet();
onError(e);
@@ -142,6 +145,12 @@ class ProxyCache {
}
}
private void onSourceRead() {
// guaranteed notify listeners after source read and cache completed
percentsAvailable = 100;
onCachePercentsAvailableChanged(percentsAvailable);
}
private void tryComplete() throws ProxyCacheException {
synchronized (stopLock) {
if (!isStopped() && cache.available() == source.length()) {
@@ -165,9 +174,9 @@ class ProxyCache {
protected final void onError(final Throwable e) {
boolean interruption = e instanceof InterruptedProxyCacheException;
if (interruption) {
Log.d(LOG_TAG, "ProxyCache is interrupted");
LOG.debug("ProxyCache is interrupted");
} else {
Log.e(LOG_TAG, "ProxyCache error", e);
LOG.error("ProxyCache error", e);
}
}

View File

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

View File

@@ -0,0 +1,28 @@
package com.danikula.videocache;
/**
* Stores source's info.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
public class SourceInfo {
public final String url;
public final int length;
public final String mime;
public SourceInfo(String url, int length, String mime) {
this.url = url;
this.length = length;
this.mime = mime;
}
@Override
public String toString() {
return "SourceInfo{" +
"url='" + url + '\'' +
", length=" + length +
", mime='" + mime + '\'' +
'}';
}
}

View File

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

View File

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

View File

@@ -0,0 +1,98 @@
package com.danikula.videocache.sourcestorage;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import com.danikula.videocache.SourceInfo;
import static com.danikula.videocache.Preconditions.checkAllNotNull;
import static com.danikula.videocache.Preconditions.checkNotNull;
/**
* Database based {@link SourceInfoStorage}.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
class DatabaseSourceInfoStorage extends SQLiteOpenHelper implements SourceInfoStorage {
private static final String TABLE = "SourceInfo";
private static final String COLUMN_ID = "_id";
private static final String COLUMN_URL = "url";
private static final String COLUMN_LENGTH = "length";
private static final String COLUMN_MIME = "mime";
private static final String[] ALL_COLUMNS = new String[]{COLUMN_ID, COLUMN_URL, COLUMN_LENGTH, COLUMN_MIME};
private static final String CREATE_SQL =
"CREATE TABLE " + TABLE + " (" +
COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," +
COLUMN_URL + " TEXT NOT NULL," +
COLUMN_MIME + " TEXT," +
COLUMN_LENGTH + " INTEGER" +
");";
DatabaseSourceInfoStorage(Context context) {
super(context, "AndroidVideoCache.db", null, 1);
checkNotNull(context);
}
@Override
public void onCreate(SQLiteDatabase db) {
checkNotNull(db);
db.execSQL(CREATE_SQL);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
throw new IllegalStateException("Should not be called. There is no any migration");
}
@Override
public SourceInfo get(String url) {
checkNotNull(url);
Cursor cursor = null;
try {
cursor = getReadableDatabase().query(TABLE, ALL_COLUMNS, COLUMN_URL + "=?", new String[]{url}, null, null, null);
return cursor == null || !cursor.moveToFirst() ? null : convert(cursor);
} finally {
if (cursor != null) {
cursor.close();
}
}
}
@Override
public void put(String url, SourceInfo sourceInfo) {
checkAllNotNull(url, sourceInfo);
SourceInfo sourceInfoFromDb = get(url);
boolean exist = sourceInfoFromDb != null;
ContentValues contentValues = convert(sourceInfo);
if (exist) {
getWritableDatabase().update(TABLE, contentValues, COLUMN_URL + "=?", new String[]{url});
} else {
getWritableDatabase().insert(TABLE, null, contentValues);
}
}
@Override
public void release() {
close();
}
private SourceInfo convert(Cursor cursor) {
return new SourceInfo(
cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_URL)),
cursor.getInt(cursor.getColumnIndexOrThrow(COLUMN_LENGTH)),
cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_MIME))
);
}
private ContentValues convert(SourceInfo sourceInfo) {
ContentValues values = new ContentValues();
values.put(COLUMN_URL, sourceInfo.url);
values.put(COLUMN_LENGTH, sourceInfo.length);
values.put(COLUMN_MIME, sourceInfo.mime);
return values;
}
}

View File

@@ -0,0 +1,24 @@
package com.danikula.videocache.sourcestorage;
import com.danikula.videocache.SourceInfo;
/**
* {@link SourceInfoStorage} that does nothing.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
public class NoSourceInfoStorage implements SourceInfoStorage {
@Override
public SourceInfo get(String url) {
return null;
}
@Override
public void put(String url, SourceInfo sourceInfo) {
}
@Override
public void release() {
}
}

View File

@@ -0,0 +1,17 @@
package com.danikula.videocache.sourcestorage;
import com.danikula.videocache.SourceInfo;
/**
* Storage for {@link SourceInfo}.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
public interface SourceInfoStorage {
SourceInfo get(String url);
void put(String url, SourceInfo sourceInfo);
void release();
}

View File

@@ -0,0 +1,19 @@
package com.danikula.videocache.sourcestorage;
import android.content.Context;
/**
* Simple factory for {@link SourceInfoStorage}.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
public class SourceInfoStorageFactory {
public static SourceInfoStorage newSourceInfoStorage(Context context) {
return new DatabaseSourceInfoStorage(context);
}
public static SourceInfoStorage newEmptySourceInfoStorage() {
return new NoSourceInfoStorage();
}
}

View File

@@ -16,10 +16,10 @@ apply plugin: 'com.neenbedankt.android-apt'
android {
compileSdkVersion 23
buildToolsVersion '23.0.1'
buildToolsVersion '22.0.1'
defaultConfig {
applicationId "com.danikula.videocache.sample"
applicationId 'com.danikula.videocache.sample'
minSdkVersion 15
targetSdkVersion 23
versionCode 1
@@ -38,7 +38,7 @@ dependencies {
// compile project(':library')
compile 'com.android.support:support-v4:23.1.0'
compile 'org.androidannotations:androidannotations-api:3.3.2'
compile 'com.danikula:videocache:2.3.2'
compile 'com.danikula:videocache:2.6.4'
compile 'com.viewpagerindicator:library:2.4.2-SNAPSHOT@aar'
apt 'org.androidannotations:androidannotations:3.3.2'
}

View File

@@ -6,11 +6,11 @@ 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");
ORANGE_1(Config.ROOT + "orange1.mp4"),
ORANGE_2(Config.ROOT + "orange2.mp4"),
ORANGE_3(Config.ROOT + "orange3.mp4"),
ORANGE_4(Config.ROOT + "orange4.mp4"),
ORANGE_5(Config.ROOT + "orange5.mp4");
public final String url;
@@ -21,4 +21,8 @@ public enum Video {
public File getCacheFile(Context context) {
return new File(context.getExternalCacheDir(), name());
}
private class Config {
private static final String ROOT = "https://raw.githubusercontent.com/danikula/AndroidVideoCache/master/files/";
}
}

View File

@@ -5,6 +5,7 @@ import android.os.Handler;
import android.os.Message;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.VideoView;
@@ -27,6 +28,7 @@ public class VideoFragment extends Fragment implements CacheListener {
@FragmentArg String url;
@FragmentArg String cachePath;
@ViewById ImageView cacheStatusImageView;
@ViewById VideoView videoView;
@ViewById ProgressBar progressBar;
@@ -45,13 +47,25 @@ public class VideoFragment extends Fragment implements CacheListener {
@AfterViews
void afterViewInjected() {
checkCachedState();
startVideo();
}
private void checkCachedState() {
HttpProxyCacheServer proxy = App.getProxy(getActivity());
boolean fullyCached = proxy.isCached(url);
setCachedState(fullyCached);
if (fullyCached) {
progressBar.setSecondaryProgress(100);
}
}
private void startVideo() {
HttpProxyCacheServer proxy = App.getProxy(getActivity());
proxy.registerCacheListener(this, url);
videoView.setVideoPath(proxy.getProxyUrl(url));
String proxyUrl = proxy.getProxyUrl(url);
Log.d(LOG_TAG, "Use proxy url " + proxyUrl + " instead of original url " + url);
videoView.setVideoPath(proxyUrl);
videoView.start();
}
@@ -78,6 +92,7 @@ public class VideoFragment extends Fragment implements CacheListener {
@Override
public void onCacheAvailable(File file, String url, int percentsAvailable) {
progressBar.setSecondaryProgress(percentsAvailable);
setCachedState(percentsAvailable == 100);
Log.d(LOG_TAG, String.format("onCacheAvailable. percents: %d, file: %s, url: %s", percentsAvailable, file, url));
}
@@ -92,6 +107,11 @@ public class VideoFragment extends Fragment implements CacheListener {
videoView.seekTo(videoPosition);
}
private void setCachedState(boolean cached) {
int statusIconId = cached ? R.drawable.ic_cloud_done : R.drawable.ic_cloud_download;
cacheStatusImageView.setImageResource(statusIconId);
}
private final class VideoProgressUpdater extends Handler {
public void start() {

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 417 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 672 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 610 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 855 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 789 B

View File

@@ -10,6 +10,13 @@
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ImageView
android:id="@+id/cacheStatusImageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:src="@drawable/ic_cloud_download" />
<SeekBar
android:id="@+id/progressBar"
style="@android:style/Widget.Holo.ProgressBar.Horizontal"

View File

@@ -5,24 +5,16 @@ repositories {
apply plugin: 'com.android.application'
android {
compileSdkVersion 22
compileSdkVersion 23
buildToolsVersion '22.0.1'
defaultConfig {
applicationId 'com.danikula.proxycache.test'
minSdkVersion 16
targetSdkVersion 22
targetSdkVersion 23
versionCode 1
versionName '0.1'
}
buildTypes {
debug {
buildConfigField "int", "MIN_SDK_VERSION", Integer.toString(android.defaultConfig.minSdkVersion.apiLevel)
}
release {
buildConfigField "int", "MIN_SDK_VERSION", Integer.toString(android.defaultConfig.minSdkVersion.apiLevel)
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
@@ -30,13 +22,10 @@ android {
}
dependencies {
compile project (':library')
compile project(':library')
testCompile 'org.slf4j:slf4j-simple:1.7.21'
testCompile 'junit:junit:4.12'
testCompile('org.robolectric:robolectric:3.0-rc2') {
exclude group: 'commons-logging', module: 'commons-logging'
exclude group: 'org.apache.httpcomponents', module: 'httpclient'
}
testCompile 'org.robolectric:robolectric:3.1'
testCompile 'com.squareup:fest-android:1.0.0'
testCompile 'com.google.guava:guava-jdk5:17.0'
testCompile('com.danikula:android-garden:2.1.4') {

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

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

View File

@@ -2,12 +2,8 @@ package com.danikula.videocache;
import com.danikula.videocache.file.FileNameGenerator;
import com.danikula.videocache.file.Md5FileNameGenerator;
import com.danikula.videocache.test.BuildConfig;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.annotation.Config;
import java.io.File;
@@ -19,9 +15,7 @@ import static org.fest.assertions.api.Assertions.fail;
*
* @author Alexey Danilov (danikula@gmail.com).
*/
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, emulateSdk = BuildConfig.MIN_SDK_VERSION)
public class FileNameGeneratorTest {
public class FileNameGeneratorTest extends BaseTest {
@Test
public void testMd5SimpleName() throws Exception {

View File

@@ -1,11 +1,6 @@
package com.danikula.videocache;
import com.danikula.videocache.test.BuildConfig;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.annotation.Config;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
@@ -16,9 +11,7 @@ import static org.fest.assertions.api.Assertions.fail;
/**
* @author Alexey Danilov (danikula@gmail.com).
*/
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, emulateSdk = BuildConfig.MIN_SDK_VERSION)
public class GetRequestTest {
public class GetRequestTest extends BaseTest {
@Test
public void testPartialHttpGet() throws Exception {

View File

@@ -1,5 +1,6 @@
package com.danikula.videocache;
import android.net.Uri;
import android.util.Pair;
import com.danikula.android.garden.io.IoUtils;
@@ -7,14 +8,10 @@ import com.danikula.videocache.file.FileNameGenerator;
import com.danikula.videocache.file.Md5FileNameGenerator;
import com.danikula.videocache.support.ProxyCacheTestUtils;
import com.danikula.videocache.support.Response;
import com.danikula.videocache.test.BuildConfig;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import java.io.File;
import java.io.IOException;
@@ -33,6 +30,7 @@ import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_URL_
import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_URL_6_REDIRECTS;
import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_URL_ONE_REDIRECT;
import static com.danikula.videocache.support.ProxyCacheTestUtils.getFileContent;
import static com.danikula.videocache.support.ProxyCacheTestUtils.getPort;
import static com.danikula.videocache.support.ProxyCacheTestUtils.loadAssetFile;
import static com.danikula.videocache.support.ProxyCacheTestUtils.readProxyResponse;
import static org.fest.assertions.api.Assertions.assertThat;
@@ -40,9 +38,7 @@ 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 {
public class HttpProxyCacheServerTest extends BaseTest {
private File cacheFolder;
@@ -82,7 +78,7 @@ public class HttpProxyCacheServerTest {
@Test
public void testMimeFromResponse() throws Exception {
Pair<File, Response> response = readProxyData("https://dl.dropboxusercontent.com/u/15506779/persistent/proxycache/android");
Pair<File, Response> response = readProxyData("https://raw.githubusercontent.com/danikula/AndroidVideoCache/master/files/android");
assertThat(response.second.contentType).isEqualTo("application/octet-stream");
}
@@ -210,10 +206,99 @@ public class HttpProxyCacheServerTest {
assertThat(file(cacheFolder, HTTP_DATA_URL_6_REDIRECTS)).exists();
}
@Test
public void testCheckFileExistForNotCachedUrl() throws Exception {
HttpProxyCacheServer proxy = newProxy(cacheFolder);
proxy.shutdown();
assertThat(proxy.isCached(HTTP_DATA_URL)).isFalse();
}
@Test
public void testCheckFileExistForFullyCachedUrl() throws Exception {
HttpProxyCacheServer proxy = newProxy(cacheFolder);
readProxyResponse(proxy, HTTP_DATA_URL, 0);
proxy.shutdown();
assertThat(proxy.isCached(HTTP_DATA_URL)).isTrue();
}
@Test
public void testCheckFileExistForPartiallyCachedUrl() throws Exception {
File cacheDir = RuntimeEnvironment.application.getExternalCacheDir();
File file = file(cacheDir, HTTP_DATA_URL);
int partialCacheSize = 1000;
byte[] partialData = ProxyCacheTestUtils.generate(partialCacheSize);
File partialCacheFile = ProxyCacheTestUtils.getTempFile(file);
IoUtils.saveToFile(partialData, partialCacheFile);
HttpProxyCacheServer proxy = newProxy(cacheDir);
assertThat(proxy.isCached(HTTP_DATA_URL)).isFalse();
readProxyResponse(proxy, HTTP_DATA_URL);
proxy.shutdown();
assertThat(proxy.isCached(HTTP_DATA_URL)).isTrue();
}
@Test
public void testCheckFileExistForDeletedCacheFile() throws Exception {
HttpProxyCacheServer proxy = newProxy(cacheFolder);
readProxyResponse(proxy, HTTP_DATA_URL, 0);
proxy.shutdown();
File cacheFile = file(cacheFolder, HTTP_DATA_URL);
boolean deleted = cacheFile.delete();
assertThat(deleted).isTrue();
assertThat(proxy.isCached(HTTP_DATA_URL)).isFalse();
}
@Test
public void testGetProxiedUrlForEmptyCache() throws Exception {
HttpProxyCacheServer proxy = newProxy(cacheFolder);
String expectedUrl = "http://127.0.0.1:" + getPort(proxy) + "/" + ProxyCacheUtils.encode(HTTP_DATA_URL);
assertThat(proxy.getProxyUrl(HTTP_DATA_URL)).isEqualTo(expectedUrl);
assertThat(proxy.getProxyUrl(HTTP_DATA_URL, true)).isEqualTo(expectedUrl);
assertThat(proxy.getProxyUrl(HTTP_DATA_URL, false)).isEqualTo(expectedUrl);
proxy.shutdown();
}
@Test
public void testGetProxiedUrlForPartialCache() throws Exception {
File cacheDir = RuntimeEnvironment.application.getExternalCacheDir();
File file = new File(cacheDir, new Md5FileNameGenerator().generate(HTTP_DATA_URL));
int partialCacheSize = 1000;
byte[] partialData = ProxyCacheTestUtils.generate(partialCacheSize);
File partialCacheFile = ProxyCacheTestUtils.getTempFile(file);
IoUtils.saveToFile(partialData, partialCacheFile);
HttpProxyCacheServer proxy = newProxy(cacheFolder);
String expectedUrl = "http://127.0.0.1:" + getPort(proxy) + "/" + ProxyCacheUtils.encode(HTTP_DATA_URL);
assertThat(proxy.getProxyUrl(HTTP_DATA_URL)).isEqualTo(expectedUrl);
assertThat(proxy.getProxyUrl(HTTP_DATA_URL, true)).isEqualTo(expectedUrl);
assertThat(proxy.getProxyUrl(HTTP_DATA_URL, false)).isEqualTo(expectedUrl);
proxy.shutdown();
}
@Test
public void testGetProxiedUrlForExistedCache() throws Exception {
HttpProxyCacheServer proxy = newProxy(cacheFolder);
readProxyResponse(proxy, HTTP_DATA_URL, 0);
String proxiedUrl = "http://127.0.0.1:" + getPort(proxy) + "/" + ProxyCacheUtils.encode(HTTP_DATA_URL);
File cachedFile = file(cacheFolder, HTTP_DATA_URL);
String cachedFileUri = Uri.fromFile(cachedFile).toString();
assertThat(proxy.getProxyUrl(HTTP_DATA_URL)).isEqualTo(cachedFileUri);
assertThat(proxy.getProxyUrl(HTTP_DATA_URL, true)).isEqualTo(cachedFileUri);
assertThat(proxy.getProxyUrl(HTTP_DATA_URL, false)).isEqualTo(proxiedUrl);
proxy.shutdown();
}
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);
File file = file(cacheFolder, url);
HttpProxyCacheServer proxy = newProxy(cacheFolder);
Response response = readProxyResponse(proxy, url, offset);
proxy.shutdown();

View File

@@ -1,25 +1,41 @@
package com.danikula.videocache;
import com.danikula.android.garden.io.IoUtils;
import com.danikula.videocache.file.FileCache;
import com.danikula.videocache.sourcestorage.SourceInfoStorage;
import com.danikula.videocache.sourcestorage.SourceInfoStorageFactory;
import com.danikula.videocache.support.ProxyCacheTestUtils;
import com.danikula.videocache.support.Response;
import com.danikula.videocache.test.BuildConfig;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.annotation.Config;
import org.robolectric.RuntimeEnvironment;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.net.Socket;
import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
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_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.loadAssetFile;
import static com.danikula.videocache.support.ProxyCacheTestUtils.loadTestData;
import static com.danikula.videocache.support.ProxyCacheTestUtils.newCacheFile;
import static org.fest.assertions.api.Assertions.assertThat;
import static org.fest.assertions.api.Assertions.fail;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyLong;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -29,43 +45,26 @@ import static org.mockito.Mockito.when;
*
* @author Alexey Danilov (danikula@gmail.com).
*/
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, emulateSdk = BuildConfig.MIN_SDK_VERSION)
public class HttpProxyCacheTest {
public class HttpProxyCacheTest extends BaseTest {
@Test
public void testProcessRequestNoCache() throws Exception {
HttpUrlSource source = new HttpUrlSource(HTTP_DATA_URL);
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());
Response response = processRequest(HTTP_DATA_URL, "GET /" + HTTP_DATA_URL + " HTTP/1.1");
assertThat(response.data).isEqualTo(loadTestData());
assertThat(response.code).isEqualTo(200);
assertThat(response.contentLength).isEqualTo(ProxyCacheTestUtils.HTTP_DATA_SIZE);
assertThat(response.contentLength).isEqualTo(HTTP_DATA_SIZE);
assertThat(response.contentType).isEqualTo("image/jpeg");
}
@Test
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());
String httpRequest = "GET /" + HTTP_DATA_URL + " HTTP/1.1\nRange: bytes=2000-";
Response response = processRequest(HTTP_DATA_URL, httpRequest, spyFileCache);
byte[] fullData = loadTestData();
byte[] partialData = new byte[fullData.length - 2000];
@@ -73,4 +72,188 @@ public class HttpProxyCacheTest {
assertThat(response.data).isEqualTo(partialData);
assertThat(response.code).isEqualTo(206);
}
@Test // https://github.com/danikula/AndroidVideoCache/issues/43
public void testPreventClosingOriginalSourceForNewPartialRequestWithoutCache() throws Exception {
HttpUrlSource source = new HttpUrlSource(HTTP_DATA_BIG_URL);
FileCache fileCache = new FileCache(ProxyCacheTestUtils.newCacheFile());
HttpProxyCache proxyCache = new HttpProxyCache(source, fileCache);
ExecutorService executor = Executors.newFixedThreadPool(5);
Future<Response> firstRequestFeature = processAsync(executor, proxyCache, "GET /" + HTTP_DATA_URL + " HTTP/1.1");
Thread.sleep(100); // wait for first request started to process
int offset = 30000;
String partialRequest = "GET /" + HTTP_DATA_URL + " HTTP/1.1\nRange: bytes=" + offset + "-";
Future<Response> secondRequestFeature = processAsync(executor, proxyCache, partialRequest);
Response secondResponse = secondRequestFeature.get();
Response firstResponse = firstRequestFeature.get();
byte[] responseData = loadAssetFile(ASSETS_DATA_BIG_NAME);
assertThat(firstResponse.data).isEqualTo(responseData);
byte[] partialData = new byte[responseData.length - offset];
System.arraycopy(responseData, offset, partialData, 0, partialData.length);
assertThat(secondResponse.data).isEqualTo(partialData);
}
@Test
public void testProcessManyThreads() throws Exception {
final String url = "https://raw.githubusercontent.com/danikula/AndroidVideoCache/master/files/space.jpg";
HttpUrlSource source = new HttpUrlSource(url);
FileCache fileCache = new FileCache(ProxyCacheTestUtils.newCacheFile());
final HttpProxyCache proxyCache = new HttpProxyCache(source, fileCache);
final byte[] loadedData = loadAssetFile("space.jpg");
final Random random = new Random(System.currentTimeMillis());
int concurrentRequests = 10;
ExecutorService executor = Executors.newFixedThreadPool(concurrentRequests);
Future[] results = new Future[concurrentRequests];
int[] offsets = new int[concurrentRequests];
final CountDownLatch finishLatch = new CountDownLatch(concurrentRequests);
final CountDownLatch startLatch = new CountDownLatch(1);
for (int i = 0; i < concurrentRequests; i++) {
final int offset = random.nextInt(loadedData.length);
offsets[i] = offset;
results[i] = executor.submit(new Callable<Response>() {
@Override
public Response call() throws Exception {
try {
startLatch.await();
String partialRequest = "GET /" + url + " HTTP/1.1\nRange: bytes=" + offset + "-";
return processRequest(proxyCache, partialRequest);
} finally {
finishLatch.countDown();
}
}
});
}
startLatch.countDown();
finishLatch.await();
for (int i = 0; i < results.length; i++) {
Response response = (Response) results[i].get();
int offset = offsets[i];
byte[] partialData = new byte[loadedData.length - offset];
System.arraycopy(loadedData, offset, partialData, 0, partialData.length);
assertThat(response.data).isEqualTo(partialData);
}
}
@Test
public void testLoadEmptyFile() throws Exception {
String zeroSizeUrl = "https://raw.githubusercontent.com/danikula/AndroidVideoCache/master/files/empty.txt";
HttpUrlSource source = new HttpUrlSource(zeroSizeUrl);
HttpProxyCache proxyCache = new HttpProxyCache(source, new FileCache(ProxyCacheTestUtils.newCacheFile()));
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);
CacheListener listener = Mockito.mock(CacheListener.class);
proxyCache.registerCacheListener(listener);
proxyCache.processRequest(request, socket);
proxyCache.registerCacheListener(null);
Response response = new Response(out.toByteArray());
Mockito.verify(listener).onCacheAvailable(Mockito.<File>any(), eq(zeroSizeUrl), eq(100));
assertThat(response.data).isEmpty();
}
@Test
public void testCacheListenerCalledAtTheEnd() throws Exception {
File file = ProxyCacheTestUtils.newCacheFile();
File tempFile = ProxyCacheTestUtils.getTempFile(file);
HttpProxyCache proxyCache = new HttpProxyCache(new HttpUrlSource(HTTP_DATA_URL), new FileCache(file));
CacheListener listener = Mockito.mock(CacheListener.class);
proxyCache.registerCacheListener(listener);
processRequest(proxyCache, "GET /" + HTTP_DATA_URL + " HTTP/1.1");
Mockito.verify(listener).onCacheAvailable(tempFile, HTTP_DATA_URL, 100); // must be called for temp file ...
Mockito.verify(listener).onCacheAvailable(file, HTTP_DATA_URL, 100); // .. and for original file too
}
@Test(expected = ProxyCacheException.class)
public void testTouchSourceForAbsentSourceInfoAndCache() throws Exception {
SourceInfoStorage sourceInfoStorage = SourceInfoStorageFactory.newEmptySourceInfoStorage();
HttpUrlSource source = ProxyCacheTestUtils.newNotOpenableHttpUrlSource(HTTP_DATA_URL, sourceInfoStorage);
HttpProxyCache proxyCache = new HttpProxyCache(source, new FileCache(newCacheFile()));
processRequest(proxyCache, "GET /" + HTTP_DATA_URL + " HTTP/1.1");
proxyCache.shutdown();
fail("Angry source should throw error! There is no file and caches source info");
}
@Test(expected = ProxyCacheException.class)
public void testTouchSourceForExistedSourceInfoAndAbsentCache() throws Exception {
SourceInfoStorage sourceInfoStorage = SourceInfoStorageFactory.newSourceInfoStorage(RuntimeEnvironment.application);
sourceInfoStorage.put(HTTP_DATA_URL, new SourceInfo(HTTP_DATA_URL, HTTP_DATA_SIZE, "image/jpg"));
HttpUrlSource source = ProxyCacheTestUtils.newNotOpenableHttpUrlSource(HTTP_DATA_URL, sourceInfoStorage);
HttpProxyCache proxyCache = new HttpProxyCache(source, new FileCache(newCacheFile()));
processRequest(proxyCache, "GET /" + HTTP_DATA_URL + " HTTP/1.1");
proxyCache.shutdown();
fail("Angry source should throw error! There is no cache file");
}
@Test
public void testTouchSourceForExistedSourceInfoAndCache() throws Exception {
SourceInfoStorage sourceInfoStorage = SourceInfoStorageFactory.newSourceInfoStorage(RuntimeEnvironment.application);
sourceInfoStorage.put(HTTP_DATA_URL, new SourceInfo(HTTP_DATA_URL, HTTP_DATA_SIZE, "cached/mime"));
HttpUrlSource source = ProxyCacheTestUtils.newNotOpenableHttpUrlSource(HTTP_DATA_URL, sourceInfoStorage);
File file = newCacheFile();
IoUtils.saveToFile(loadAssetFile(ASSETS_DATA_NAME), file);
HttpProxyCache proxyCache = new HttpProxyCache(source, new FileCache(file));
Response response = processRequest(proxyCache, "GET /" + HTTP_DATA_URL + " HTTP/1.1");
proxyCache.shutdown();
assertThat(response.data).isEqualTo(loadAssetFile(ASSETS_DATA_NAME));
assertThat(response.contentLength).isEqualTo(HTTP_DATA_SIZE);
assertThat(response.contentType).isEqualTo("cached/mime");
}
@Test
public void testReuseSourceInfo() throws Exception {
SourceInfoStorage sourceInfoStorage = SourceInfoStorageFactory.newSourceInfoStorage(RuntimeEnvironment.application);
HttpUrlSource source = new HttpUrlSource(HTTP_DATA_URL, sourceInfoStorage);
File cacheFile = newCacheFile();
HttpProxyCache proxyCache = new HttpProxyCache(source, new FileCache(cacheFile));
processRequest(proxyCache, "GET /" + HTTP_DATA_URL + " HTTP/1.1");
HttpUrlSource notOpenableSource = ProxyCacheTestUtils.newNotOpenableHttpUrlSource(HTTP_DATA_URL, sourceInfoStorage);
HttpProxyCache proxyCache2 = new HttpProxyCache(notOpenableSource, new FileCache(cacheFile));
Response response = processRequest(proxyCache2, "GET /" + HTTP_DATA_URL + " HTTP/1.1");
proxyCache.shutdown();
assertThat(response.data).isEqualTo(loadAssetFile(ASSETS_DATA_NAME));
assertThat(response.contentLength).isEqualTo(HTTP_DATA_SIZE);
assertThat(response.contentType).isEqualTo("image/jpeg");
}
private Response processRequest(String sourceUrl, String httpRequest) throws ProxyCacheException, IOException {
FileCache fileCache = new FileCache(ProxyCacheTestUtils.newCacheFile());
return processRequest(sourceUrl, httpRequest, fileCache);
}
private Response processRequest(String sourceUrl, String httpRequest, FileCache fileCache) throws ProxyCacheException, IOException {
HttpUrlSource source = new HttpUrlSource(sourceUrl);
HttpProxyCache proxyCache = new HttpProxyCache(source, fileCache);
return processRequest(proxyCache, httpRequest);
}
private Response processRequest(HttpProxyCache proxyCache, String httpRequest) throws ProxyCacheException, IOException {
GetRequest request = new GetRequest(httpRequest);
ByteArrayOutputStream out = new ByteArrayOutputStream();
Socket socket = mock(Socket.class);
when(socket.getOutputStream()).thenReturn(out);
proxyCache.processRequest(request, socket);
return new Response(out.toByteArray());
}
private Future<Response> processAsync(ExecutorService executor, final HttpProxyCache proxyCache, final String httpRequest) {
return executor.submit(new Callable<Response>() {
@Override
public Response call() throws Exception {
return processRequest(proxyCache, httpRequest);
}
});
}
}

View File

@@ -1,16 +1,17 @@
package com.danikula.videocache;
import com.danikula.videocache.test.BuildConfig;
import com.danikula.videocache.sourcestorage.SourceInfoStorage;
import com.danikula.videocache.sourcestorage.SourceInfoStorageFactory;
import com.danikula.videocache.support.ProxyCacheTestUtils;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.annotation.Config;
import org.mockito.Mockito;
import java.io.ByteArrayOutputStream;
import java.util.Arrays;
import static com.danikula.videocache.ProxyCacheUtils.DEFAULT_BUFFER_SIZE;
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;
@@ -23,13 +24,12 @@ import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_URL_
import static com.danikula.videocache.support.ProxyCacheTestUtils.loadAssetFile;
import static org.fest.assertions.api.Assertions.assertThat;
import static org.fest.assertions.api.Assertions.fail;
import static org.mockito.Matchers.any;
/**
* @author Alexey Danilov (danikula@gmail.com).
*/
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, emulateSdk = BuildConfig.MIN_SDK_VERSION)
public class HttpUrlSourceTest {
public class HttpUrlSourceTest extends BaseTest {
@Test
public void testHttpUrlSourceRange() throws Exception {
@@ -83,7 +83,7 @@ public class HttpUrlSourceTest {
HttpUrlSource source = new HttpUrlSource(HTTP_DATA_URL_ONE_REDIRECT);
source.open(0);
byte[] readData = new byte[HTTP_DATA_SIZE];
source.read(readData);
readSource(source, readData);
source.close();
byte[] expectedData = Arrays.copyOfRange(loadAssetFile(ASSETS_DATA_NAME), 0, HTTP_DATA_SIZE);
@@ -96,7 +96,7 @@ public class HttpUrlSourceTest {
HttpUrlSource source = new HttpUrlSource(HTTP_DATA_URL_ONE_REDIRECT);
source.open(offset);
byte[] readData = new byte[HTTP_DATA_SIZE - offset];
source.read(readData);
readSource(source, readData);
source.close();
byte[] expectedData = Arrays.copyOfRange(loadAssetFile(ASSETS_DATA_NAME), offset, HTTP_DATA_SIZE);
@@ -109,7 +109,7 @@ public class HttpUrlSourceTest {
HttpUrlSource source = new HttpUrlSource(HTTP_DATA_URL_3_REDIRECTS);
source.open(offset);
byte[] readData = new byte[HTTP_DATA_SIZE - offset];
source.read(readData);
readSource(source, readData);
source.close();
byte[] expectedData = Arrays.copyOfRange(loadAssetFile(ASSETS_DATA_NAME), offset, HTTP_DATA_SIZE);
@@ -130,4 +130,39 @@ public class HttpUrlSourceTest {
assertThat(new HttpUrlSource("http://mysite.by/video.mp4").getMime()).isEqualTo("video/mp4");
assertThat(new HttpUrlSource(HTTP_DATA_URL).getMime()).isEqualTo("image/jpeg");
}
@Test(expected = RuntimeException.class)
public void testAngryHttpUrlSourceLength() throws Exception {
ProxyCacheTestUtils.newAngryHttpUrlSource().length();
fail("source.length() should throw exception");
}
@Test(expected = RuntimeException.class)
public void testAngryHttpUrlSourceOpen() throws Exception {
ProxyCacheTestUtils.newAngryHttpUrlSource().open(Mockito.anyInt());
fail("source.open() should throw exception");
}
@Test(expected = RuntimeException.class)
public void testAngryHttpUrlSourceRead() throws Exception {
ProxyCacheTestUtils.newAngryHttpUrlSource().read(any(byte[].class));
fail("source.read() should throw exception");
}
@Test(expected = RuntimeException.class)
public void testNotOpenableHttpUrlSourceOpen() throws Exception {
SourceInfoStorage sourceInfoStorage = SourceInfoStorageFactory.newEmptySourceInfoStorage();
ProxyCacheTestUtils.newNotOpenableHttpUrlSource("", sourceInfoStorage).open(Mockito.anyInt());
fail("source.open() should throw exception");
}
private void readSource(Source source, byte[] target) throws ProxyCacheException {
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
int totalRead = 0;
int readBytes;
while ((readBytes = source.read(buffer)) != -1) {
System.arraycopy(buffer, 0, target, totalRead, readBytes);
totalRead += readBytes;
}
}
}

View File

@@ -0,0 +1,56 @@
package com.danikula.videocache;
import org.junit.Test;
import org.robolectric.RuntimeEnvironment;
import java.io.ByteArrayOutputStream;
import java.net.Socket;
import static com.danikula.videocache.support.ProxyCacheTestUtils.getPort;
import static org.fest.assertions.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* Tests {@link Pinger}.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
public class PingerTest extends BaseTest {
@Test
public void testPingSuccess() throws Exception {
HttpProxyCacheServer server = new HttpProxyCacheServer(RuntimeEnvironment.application);
Pinger pinger = new Pinger("127.0.0.1", getPort(server));
boolean pinged = pinger.ping(1, 100);
assertThat(pinged).isTrue();
server.shutdown();
}
@Test
public void testPingFail() throws Exception {
Pinger pinger = new Pinger("127.0.0.1", 33);
boolean pinged = pinger.ping(3, 70);
assertThat(pinged).isFalse();
}
@Test
public void testIsPingRequest() throws Exception {
Pinger pinger = new Pinger("127.0.0.1", 1);
assertThat(pinger.isPingRequest("ping")).isTrue();
assertThat(pinger.isPingRequest("notPing")).isFalse();
}
@Test
public void testResponseToPing() throws Exception {
Pinger pinger = new Pinger("127.0.0.1", 1);
ByteArrayOutputStream out = new ByteArrayOutputStream();
Socket socket = mock(Socket.class);
when(socket.getOutputStream()).thenReturn(out);
pinger.responseToPing(socket);
assertThat(out.toString()).isEqualTo("HTTP/1.1 200 OK\n\nping ok");
}
}

View File

@@ -2,14 +2,9 @@ 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 com.danikula.videocache.support.ProxyCacheTestUtils;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.annotation.Config;
import java.io.File;
import java.util.Arrays;
@@ -23,14 +18,13 @@ 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.newPhlegmaticSource;
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 ProxyCacheTest {
public class ProxyCacheTest extends BaseTest {
@Test
public void testNoCache() throws Exception {
@@ -95,18 +89,17 @@ public class ProxyCacheTest {
public void testProxyWithPhlegmaticSource() throws Exception {
int dataSize = 100000;
byte[] sourceData = generate(dataSize);
Source source = new PhlegmaticByteArraySource(sourceData, 200);
Source source = newPhlegmaticSource(sourceData, 200);
ProxyCache proxyCache = new ProxyCache(source, new FileCache(newCacheFile()));
byte[] readData = new byte[dataSize];
proxyCache.read(readData, 0, dataSize);
assertThat(readData).isEqualTo(sourceData);
}
@Test
public void testReadEnd() throws Exception {
int capacity = 5323;
Source source = new PhlegmaticByteArraySource(generate(capacity), 200);
Source source = newPhlegmaticSource(generate(capacity), 200);
Cache cache = new FileCache(newCacheFile());
ProxyCache proxyCache = new ProxyCache(source, cache);
proxyCache.read(new byte[1], capacity - 1, 1);
@@ -118,7 +111,7 @@ public class ProxyCacheTest {
public void testReadRandomParts() throws Exception {
int dataSize = 123456;
byte[] sourceData = generate(dataSize);
Source source = new PhlegmaticByteArraySource(sourceData, 300);
Source source = newPhlegmaticSource(sourceData, 300);
File file = newCacheFile();
Cache cache = new FileCache(file);
ProxyCache proxyCache = new ProxyCache(source, cache);
@@ -176,8 +169,9 @@ public class ProxyCacheTest {
byte[] data = generate(dataSize);
File file = newCacheFile();
IoUtils.saveToFile(data, file);
ProxyCache proxyCache = new ProxyCache(new AngryHttpUrlSource(), new FileCache(file));
Source source = ProxyCacheTestUtils.newAngryHttpUrlSource();
ProxyCache proxyCache = new ProxyCache(source, new FileCache(file));
byte[] readData = new byte[dataSize];
proxyCache.read(readData, 0, dataSize);

View File

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

View File

@@ -2,16 +2,13 @@ package com.danikula.videocache.file;
import com.danikula.android.garden.io.Files;
import com.danikula.android.garden.io.IoUtils;
import com.danikula.videocache.BaseTest;
import com.danikula.videocache.Cache;
import com.danikula.videocache.ProxyCacheException;
import com.danikula.videocache.test.BuildConfig;
import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.annotation.Config;
import java.io.File;
import java.util.Arrays;
@@ -27,9 +24,7 @@ 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 FileCacheTest {
public class FileCacheTest extends BaseTest {
@Test
public void testWriteReadDiscCache() throws Exception {

View File

@@ -1,12 +1,9 @@
package com.danikula.videocache.file;
import com.danikula.videocache.BaseTest;
import com.danikula.videocache.support.ProxyCacheTestUtils;
import com.danikula.videocache.test.BuildConfig;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.annotation.Config;
import java.io.File;
@@ -17,9 +14,7 @@ import static org.fest.assertions.api.Assertions.assertThat;
*
* @author Alexey Danilov (danikula@gmail.com).
*/
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, emulateSdk = BuildConfig.MIN_SDK_VERSION)
public class FilesTest {
public class FilesTest extends BaseTest {
@Test
public void testModify() throws Exception {

View File

@@ -0,0 +1,84 @@
package com.danikula.videocache.sourcestorage;
import com.danikula.videocache.BaseTest;
import com.danikula.videocache.SourceInfo;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.robolectric.RuntimeEnvironment;
import static org.fest.assertions.api.Assertions.assertThat;
import static org.fest.assertions.api.Assertions.fail;
/**
* Tests for {@link SourceInfoStorage}.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
public class SourceInfoStorageTest extends BaseTest {
private SourceInfoStorage storage;
@Before
public void setUp() throws Exception {
storage = SourceInfoStorageFactory.newSourceInfoStorage(RuntimeEnvironment.application);
}
@After
public void tearDown() throws Exception {
storage.release();
}
@Test
public void testGetAbsent() throws Exception {
SourceInfo sourceInfo = storage.get(":-)");
assertThat(sourceInfo).isNull();
}
@Test
public void testSaving() throws Exception {
storage.put(":-)", new SourceInfo(":-)", 42, "text/plain"));
storage.put(":-(", new SourceInfo(":-(", 43, "video/mp4"));
SourceInfo sourceInfo = storage.get(":-)");
assertThat(sourceInfo.url).isEqualTo(":-)");
assertThat(sourceInfo.length).isEqualTo(42);
assertThat(sourceInfo.mime).isEqualTo("text/plain");
SourceInfo sourceInfo2 = storage.get(":-(");
assertThat(sourceInfo2.url).isEqualTo(":-(");
assertThat(sourceInfo2.length).isEqualTo(43);
assertThat(sourceInfo2.mime).isEqualTo("video/mp4");
}
@Test
public void testUpdating() throws Exception {
String url = ":-)";
storage.put(url, new SourceInfo(url, 42, "text/plain"));
storage.put(url, new SourceInfo(url, 43, "video/mp4"));
SourceInfo sourceInfo = storage.get(url);
assertThat(sourceInfo.url).isEqualTo(url);
assertThat(sourceInfo.length).isEqualTo(43);
assertThat(sourceInfo.mime).isEqualTo("video/mp4");
}
@Test(expected = NullPointerException.class)
public void testNpeForGetting() throws Exception {
storage.get(null);
fail("null is not acceptable");
}
@Test(expected = NullPointerException.class)
public void testNpeForPuttingUrl() throws Exception {
storage.put(null, new SourceInfo("", 0, ""));
fail("null is not acceptable");
}
@Test(expected = NullPointerException.class)
public void testNpeForPuttingSource() throws Exception {
storage.put("url", null);
fail("null is not acceptable");
}
}

View File

@@ -1,33 +0,0 @@
package com.danikula.videocache.support;
import com.danikula.videocache.ProxyCacheException;
import com.danikula.videocache.Source;
/**
* {@link Source} that throws exception in all methods.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
@Deprecated // use Mockito to throw error
public class AngryHttpUrlSource implements Source {
@Override
public int length() throws ProxyCacheException {
throw new IllegalStateException();
}
@Override
public void open(int offset) throws ProxyCacheException {
throw new IllegalStateException();
}
@Override
public void close() throws ProxyCacheException {
throw new IllegalStateException();
}
@Override
public int read(byte[] buffer) throws ProxyCacheException {
throw new IllegalStateException();
}
}

View File

@@ -1,32 +0,0 @@
package com.danikula.videocache.support;
import com.danikula.videocache.ByteArraySource;
import com.danikula.videocache.ProxyCacheException;
import java.util.Random;
/**
* @author Alexey Danilov (danikula@gmail.com).
*/
@Deprecated // TODO: use Mockito to mock delay
public class PhlegmaticByteArraySource extends ByteArraySource {
private final Random delayGenerator;
private final int maxDelayMs;
public PhlegmaticByteArraySource(byte[] data, int maxDelayMs) {
super(data);
this.maxDelayMs = maxDelayMs;
this.delayGenerator = new Random(System.currentTimeMillis());
}
@Override
public int read(byte[] buffer) throws ProxyCacheException {
try {
Thread.sleep(delayGenerator.nextInt(maxDelayMs));
} catch (InterruptedException e) {
throw new ProxyCacheException("Error sleeping", e);
}
return super.read(buffer);
}
}

View File

@@ -1,9 +1,16 @@
package com.danikula.videocache.support;
import com.danikula.android.garden.io.IoUtils;
import com.danikula.videocache.ByteArraySource;
import com.danikula.videocache.HttpProxyCacheServer;
import com.danikula.videocache.HttpUrlSource;
import com.danikula.videocache.ProxyCacheException;
import com.danikula.videocache.Source;
import com.danikula.videocache.sourcestorage.SourceInfoStorage;
import com.google.common.io.Files;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.robolectric.RuntimeEnvironment;
import java.io.ByteArrayOutputStream;
@@ -14,18 +21,28 @@ import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Random;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.fest.assertions.api.Assertions.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
/**
* @author Alexey Danilov (danikula@gmail.com).
*/
public class ProxyCacheTestUtils {
public static final String HTTP_DATA_URL = "https://dl.dropboxusercontent.com/u/15506779/persistent/proxycache/android.jpg";
public static final String HTTP_DATA_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 HTTP_DATA_URL = "https://raw.githubusercontent.com/danikula/AndroidVideoCache/master/files/android.jpg";
public static final String HTTP_DATA_URL_ONE_REDIRECT = "http://bit.ly/1LAJKAy";
public static final String HTTP_DATA_URL_3_REDIRECTS = "http://bit.ly/1QtKJiB";
public static final String HTTP_DATA_URL_6_REDIRECTS = "http://ow.ly/Z17wz";
public static final String HTTP_DATA_BIG_URL = "https://raw.githubusercontent.com/danikula/AndroidVideoCache/master/files/phones.jpg";
public static final String HTTP_DATA_BIG_URL_ONE_REDIRECT = "http://bit.ly/24DdZ06";
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;
@@ -40,7 +57,11 @@ public class ProxyCacheTestUtils {
}
public static Response readProxyResponse(HttpProxyCacheServer proxy, String url, int offset) throws IOException {
URL proxiedUrl = new URL(proxy.getProxyUrl(url));
String proxyUrl = proxy.getProxyUrl(url, false);
if (!proxyUrl.startsWith("http://127.0.0.1")) {
throw new IllegalStateException("Proxy url " + proxyUrl + " is not proxied! Original url is " + url);
}
URL proxiedUrl = new URL(proxyUrl);
HttpURLConnection connection = (HttpURLConnection) proxiedUrl.openConnection();
try {
if (offset >= 0) {
@@ -79,4 +100,51 @@ public class ProxyCacheTestUtils {
random.nextBytes(result);
return result;
}
public static HttpUrlSource newAngryHttpUrlSource() throws ProxyCacheException {
HttpUrlSource source = mock(HttpUrlSource.class);
doThrow(new RuntimeException()).when(source).getMime();
doThrow(new RuntimeException()).when(source).read(any(byte[].class));
doThrow(new RuntimeException()).when(source).open(anyInt());
doThrow(new RuntimeException()).when(source).length();
doThrow(new RuntimeException()).when(source).getUrl();
doThrow(new RuntimeException()).when(source).close();
return source;
}
public static HttpUrlSource newNotOpenableHttpUrlSource(String url, SourceInfoStorage sourceInfoStorage) throws ProxyCacheException {
HttpUrlSource httpUrlSource = new HttpUrlSource(url, sourceInfoStorage);
HttpUrlSource source = spy(httpUrlSource);
doAnswer(new Answer() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
System.out.print("Can't open!!!");
throw new RuntimeException();
}
}).when(source).open(anyInt());
return source;
}
public static Source newPhlegmaticSource(byte[] data, final int maxDelayMs) throws ProxyCacheException {
Source spySource = spy(new ByteArraySource(data));
final Random delayGenerator = new Random(System.currentTimeMillis());
doAnswer(new Answer() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
Thread.sleep(delayGenerator.nextInt(maxDelayMs));
return null;
}
}).doCallRealMethod().when(spySource).read(any(byte[].class));
return spySource;
}
public static int getPort(HttpProxyCacheServer server) {
String proxyUrl = server.getProxyUrl("test");
Pattern pattern = Pattern.compile("http://127.0.0.1:(\\d*)/test");
Matcher matcher = pattern.matcher(proxyUrl);
assertThat(matcher.find()).isTrue();
String portAsString = matcher.group(1);
return Integer.parseInt(portAsString);
}
}