mirror of
https://github.com/zhigang1992/AndroidVideoCache.git
synced 2026-06-15 09:57:55 +08:00
Compare commits
50 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99284420a2 | ||
|
|
f02351be69 | ||
|
|
36fea1c2ad | ||
|
|
acdf747a99 | ||
|
|
89ab5937c6 | ||
|
|
1b597ec1ad | ||
|
|
7e48955383 | ||
|
|
355e83b4ba | ||
|
|
c300b2e479 | ||
|
|
8726693015 | ||
|
|
02bc21f882 | ||
|
|
ba6ba4ebe9 | ||
|
|
f5dd92efff | ||
|
|
351cf2b986 | ||
|
|
2253b16797 | ||
|
|
6cd18555d3 | ||
|
|
01bba67289 | ||
|
|
e0ee4a7dde | ||
|
|
699aa05a2d | ||
|
|
4cf2a6bb68 | ||
|
|
3cc6f591c5 | ||
|
|
94692447db | ||
|
|
3023e4a1ee | ||
|
|
090f5ec7f9 | ||
|
|
8603d5ef7f | ||
|
|
b8a6110aa8 | ||
|
|
9353cde8f4 | ||
|
|
0d1131b0fc | ||
|
|
44318d87d9 | ||
|
|
76c13ba827 | ||
|
|
b94e829448 | ||
|
|
293d2d4089 | ||
|
|
99b8330679 | ||
|
|
0f522f200d | ||
|
|
71c6301eb4 | ||
|
|
372542c2dc | ||
|
|
22cfddb2c9 | ||
|
|
8fc4a9ac7d | ||
|
|
6c996ea66c | ||
|
|
582832f8b5 | ||
|
|
cd0b4111a4 | ||
|
|
eb67640212 | ||
|
|
1d50807fe0 | ||
|
|
6da9650030 | ||
|
|
a1d00fec7b | ||
|
|
f5a779266d | ||
|
|
ca6f36ada3 | ||
|
|
33b7e5e9d3 | ||
|
|
0aad13f118 | ||
|
|
a270460c8d |
9
.travis.yml
Normal file
9
.travis.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
language: android
|
||||
jdk: oraclejdk8
|
||||
android:
|
||||
components:
|
||||
- platform-tools
|
||||
- tools
|
||||
- build-tools-25.0.2
|
||||
- android-23
|
||||
- extra
|
||||
2
LICENSE
2
LICENSE
@@ -186,7 +186,7 @@
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
Copyright 2014-2016 Alexey Danilov
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
110
README.md
110
README.md
@@ -1,9 +1,27 @@
|
||||
# Video cache support for Android
|
||||
[](http://android-arsenal.com/details/1/1751)
|
||||
## Video cache support for Android
|
||||
[](http://android-arsenal.com/details/1/1751) [](https://travis-ci.org/danikula/AndroidVideoCache/) [  ](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)
|
||||
- [Providing names for cached files](#providing-names-for-cached-files)
|
||||
- [Adding custom http headers](#adding-custom-http-headers)
|
||||
- [Using exoPlayer](#using-exoplayer)
|
||||
- [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!
|
||||
`AndroidVideoCache` allows to add caching support to your `VideoView/MediaPlayer`, [ExoPlayer](https://github.com/danikula/ExoPlayer/commit/6110be8559f003f98020ada8c5e09691b67aaff4) or any another player with help of single line!
|
||||
`AndroidVideoCache` allows to add caching support to your `VideoView/MediaPlayer`, [ExoPlayer](https://github.com/danikula/AndroidVideoCache/tree/exoPlayer) or any another player with help of single line!
|
||||
|
||||
## Features
|
||||
- caching to disk during streaming;
|
||||
@@ -14,14 +32,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.7.1'
|
||||
}
|
||||
```
|
||||
|
||||
@@ -64,6 +79,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 +99,72 @@ private HttpProxyCacheServer newProxy() {
|
||||
.maxCacheFilesCount(20)
|
||||
.build();
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
See `sample` app for details.
|
||||
or even implement your own `DiskUsage` strategy:
|
||||
```java
|
||||
private HttpProxyCacheServer newProxy() {
|
||||
return new HttpProxyCacheServer.Builder(this)
|
||||
.diskUsage(new MyCoolDiskUsageStrategy())
|
||||
.build();
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Listen caching progress
|
||||
Use `HttpProxyCacheServer.registerCacheListener(CacheListener listener)` method to set listener with callback `onCacheAvailable(File cacheFile, String url, int percentsAvailable)` to be aware of caching progress. Do not forget to to unsubscribe listener with help of `HttpProxyCacheServer.unregisterCacheListener(CacheListener listener)` method to avoid memory leaks.
|
||||
|
||||
Use `HttpProxyCacheServer.isCached(String url)` method to check was url's content fully cached to file or not.
|
||||
|
||||
See `sample` app for more details.
|
||||
|
||||
### Providing names for cached files
|
||||
By default `AndroidVideoCache` uses MD5 of video url as file name. But in some cases url is not stable and it can contain some generated parts (e.g. session token). In this case caching mechanism will be broken. To fix it you have to provide own `FileNameGenerator`:
|
||||
``` java
|
||||
public class MyFileNameGenerator implements FileNameGenerator {
|
||||
|
||||
// Urls contain mutable parts (parameter 'sessionToken') and stable video's id (parameter 'videoId').
|
||||
// e. g. http://example.com?videoId=abcqaz&sessionToken=xyz987
|
||||
public String generate(String url) {
|
||||
Uri uri = Uri.parse(url);
|
||||
String videoId = uri.getQueryParameter("videoId");
|
||||
return videoId + ".mp4";
|
||||
}
|
||||
}
|
||||
|
||||
...
|
||||
HttpProxyCacheServer proxy = HttpProxyCacheServer.Builder(context)
|
||||
.fileNameGenerator(new MyFileNameGenerator())
|
||||
.build()
|
||||
```
|
||||
|
||||
### Adding custom http headers
|
||||
You can add custom headers to requests with help of `HeadersInjector`:
|
||||
``` java
|
||||
public class UserAgentHeadersInjector implements HeaderInjector {
|
||||
|
||||
@Override
|
||||
public Map<String, String> addHeaders(String url) {
|
||||
return Maps.newHashMap("User-Agent", "Cool app v1.1");
|
||||
}
|
||||
}
|
||||
|
||||
private HttpProxyCacheServer newProxy() {
|
||||
return new HttpProxyCacheServer.Builder(this)
|
||||
.headerInjector(new UserAgentHeadersInjector())
|
||||
.build();
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Using exoPlayer
|
||||
You can use [`exoPlayer`](https://google.github.io/ExoPlayer/) with `AndroidVideoCache`. See `sample` app in [`exoPlayer`](https://github.com/danikula/AndroidVideoCache/tree/exoPlayer) branch. Note [exoPlayer supports](https://github.com/google/ExoPlayer/commit/bd7be1b5e7cc41a59ebbc348d394820fc857db92) cache as well.
|
||||
|
||||
### Sample
|
||||
See `sample` app.
|
||||
|
||||
## Known problems
|
||||
- In some cases clients [can't connect](https://github.com/danikula/AndroidVideoCache/issues/134) to local proxy server ('Error pinging server' error). May be it is result of previous error. Note in this case video will be played, but without caching.
|
||||
|
||||
## Whats new
|
||||
See Release Notes [here](https://github.com/danikula/AndroidVideoCache/releases)
|
||||
@@ -94,9 +174,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 +188,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-2017 Alexey Danilov
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:1.3.0'
|
||||
classpath 'com.android.tools.build:gradle:7.2.0'
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
jcenter()
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
BIN
files/android
Normal file
BIN
files/android
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.7 KiB |
BIN
files/android.jpg
Normal file
BIN
files/android.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.7 KiB |
BIN
files/devbytes.mp4
Normal file
BIN
files/devbytes.mp4
Normal file
Binary file not shown.
0
files/empty.txt
Normal file
0
files/empty.txt
Normal file
BIN
files/orange1.mp4
Normal file
BIN
files/orange1.mp4
Normal file
Binary file not shown.
BIN
files/orange2.mp4
Normal file
BIN
files/orange2.mp4
Normal file
Binary file not shown.
BIN
files/orange3.mp4
Normal file
BIN
files/orange3.mp4
Normal file
Binary file not shown.
BIN
files/orange4.mp4
Normal file
BIN
files/orange4.mp4
Normal file
Binary file not shown.
BIN
files/orange5.mp4
Normal file
BIN
files/orange5.mp4
Normal file
Binary file not shown.
BIN
files/phones.jpg
Normal file
BIN
files/phones.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 92 KiB |
BIN
files/space.jpg
Normal file
BIN
files/space.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
3
gradle/wrapper/gradle-wrapper.properties
vendored
3
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,5 @@
|
||||
#Wed Apr 10 15:27:10 PDT 2013
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
|
||||
|
||||
@@ -1,15 +1,29 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
jcenter()
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.novoda:bintray-release:0.2.10'
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'idea'
|
||||
apply plugin: 'java'
|
||||
apply plugin: 'bintray-release'
|
||||
apply plugin: 'maven-publish'
|
||||
|
||||
android {
|
||||
compileSdkVersion 23
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 9
|
||||
targetSdkVersion 23
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_7
|
||||
targetCompatibility JavaVersion.VERSION_1_7
|
||||
}
|
||||
}
|
||||
|
||||
idea {
|
||||
module {
|
||||
@@ -19,14 +33,25 @@ idea {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile 'com.google.android:android:1.6_r2'
|
||||
implementation 'org.slf4j:slf4j-android:1.7.34'
|
||||
}
|
||||
|
||||
publish {
|
||||
userOrg = 'alexeydanilov'
|
||||
groupId = 'com.danikula'
|
||||
artifactId = 'videocache'
|
||||
publishVersion = '2.3.2'
|
||||
description = 'Cache support for android VideoView'
|
||||
website = 'https://github.com/danikula/AndroidVideoCache'
|
||||
publishing {
|
||||
publications {
|
||||
myPulication(MavenPublication) {
|
||||
groupId 'com.danikula'
|
||||
version '2.7.1'
|
||||
artifactId 'videocache'
|
||||
artifact("$buildDir/outputs/aar/library-release.aar")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//publish {
|
||||
// userOrg = 'alexeydanilov'
|
||||
// groupId = 'com.danikula'
|
||||
// artifactId = 'videocache'
|
||||
// publishVersion = android.defaultConfig.versionName
|
||||
// description = 'Cache support for android VideoView'
|
||||
// website = 'https://github.com/danikula/AndroidVideoCache'
|
||||
//}
|
||||
|
||||
5
library/src/main/AndroidManifest.xml
Normal file
5
library/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest
|
||||
package="com.danikula.videocache">
|
||||
<application />
|
||||
</manifest>
|
||||
@@ -33,7 +33,7 @@ public class ByteArrayCache implements Cache {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int available() throws ProxyCacheException {
|
||||
public long available() throws ProxyCacheException {
|
||||
return data.length;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,12 +22,12 @@ public class ByteArraySource implements Source {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int length() throws ProxyCacheException {
|
||||
public long length() throws ProxyCacheException {
|
||||
return data.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void open(int offset) throws ProxyCacheException {
|
||||
public void open(long offset) throws ProxyCacheException {
|
||||
arrayInputStream = new ByteArrayInputStream(data);
|
||||
arrayInputStream.skip(offset);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ package com.danikula.videocache;
|
||||
*/
|
||||
public interface Cache {
|
||||
|
||||
int available() throws ProxyCacheException;
|
||||
long available() throws ProxyCacheException;
|
||||
|
||||
int read(byte[] buffer, long offset, int length) throws ProxyCacheException;
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ package com.danikula.videocache;
|
||||
|
||||
import com.danikula.videocache.file.DiskUsage;
|
||||
import com.danikula.videocache.file.FileNameGenerator;
|
||||
import com.danikula.videocache.headers.HeaderInjector;
|
||||
import com.danikula.videocache.sourcestorage.SourceInfoStorage;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
@@ -15,11 +17,15 @@ class Config {
|
||||
public final File cacheRoot;
|
||||
public final FileNameGenerator fileNameGenerator;
|
||||
public final DiskUsage diskUsage;
|
||||
public final SourceInfoStorage sourceInfoStorage;
|
||||
public final HeaderInjector headerInjector;
|
||||
|
||||
Config(File cacheRoot, FileNameGenerator fileNameGenerator, DiskUsage diskUsage) {
|
||||
Config(File cacheRoot, FileNameGenerator fileNameGenerator, DiskUsage diskUsage, SourceInfoStorage sourceInfoStorage, HeaderInjector headerInjector) {
|
||||
this.cacheRoot = cacheRoot;
|
||||
this.fileNameGenerator = fileNameGenerator;
|
||||
this.diskUsage = diskUsage;
|
||||
this.sourceInfoStorage = sourceInfoStorage;
|
||||
this.headerInjector = headerInjector;
|
||||
}
|
||||
|
||||
File generateCacheFile(String url) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import java.io.BufferedOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.Socket;
|
||||
import java.util.Locale;
|
||||
|
||||
import static com.danikula.videocache.ProxyCacheUtils.DEFAULT_BUFFER_SIZE;
|
||||
|
||||
@@ -48,9 +49,9 @@ class HttpProxyCache extends ProxyCache {
|
||||
}
|
||||
|
||||
private boolean isUseCache(GetRequest request) throws ProxyCacheException {
|
||||
int sourceLength = source.length();
|
||||
long sourceLength = source.length();
|
||||
boolean sourceLengthKnown = sourceLength > 0;
|
||||
int cacheAvailable = cache.available();
|
||||
long cacheAvailable = cache.available();
|
||||
// do not use cache for partial requests which too far from available cache. It seems user seek video.
|
||||
return !sourceLengthKnown || !request.partial || request.rangeOffset <= cacheAvailable + sourceLength * NO_CACHE_BARRIER;
|
||||
}
|
||||
@@ -58,16 +59,16 @@ class HttpProxyCache extends ProxyCache {
|
||||
private String newResponseHeaders(GetRequest request) throws IOException, ProxyCacheException {
|
||||
String mime = source.getMime();
|
||||
boolean mimeKnown = !TextUtils.isEmpty(mime);
|
||||
int length = cache.isCompleted() ? cache.available() : source.length();
|
||||
long length = cache.isCompleted() ? cache.available() : source.length();
|
||||
boolean lengthKnown = length >= 0;
|
||||
long contentLength = request.partial ? length - request.rangeOffset : length;
|
||||
boolean addRange = lengthKnown && request.partial;
|
||||
return new StringBuilder()
|
||||
.append(request.partial ? "HTTP/1.1 206 PARTIAL CONTENT\n" : "HTTP/1.1 200 OK\n")
|
||||
.append("Accept-Ranges: bytes\n")
|
||||
.append(lengthKnown ? String.format("Content-Length: %d\n", contentLength) : "")
|
||||
.append(addRange ? String.format("Content-Range: bytes %d-%d/%d\n", request.rangeOffset, length - 1, length) : "")
|
||||
.append(mimeKnown ? String.format("Content-Type: %s\n", mime) : "")
|
||||
.append(lengthKnown ? format("Content-Length: %d\n", contentLength) : "")
|
||||
.append(addRange ? format("Content-Range: bytes %d-%d/%d\n", request.rangeOffset, length - 1, length) : "")
|
||||
.append(mimeKnown ? format("Content-Type: %s\n", mime) : "")
|
||||
.append("\n") // headers end
|
||||
.toString();
|
||||
}
|
||||
@@ -83,25 +84,29 @@ 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();
|
||||
}
|
||||
}
|
||||
|
||||
private String format(String pattern, Object... args) {
|
||||
return String.format(Locale.US, pattern, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCachePercentsAvailableChanged(int percents) {
|
||||
if (listener != null) {
|
||||
listener.onCacheAvailable(cache.file, source.url, percents);
|
||||
listener.onCacheAvailable(cache.file, source.getUrl(), percents);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,36 @@
|
||||
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.headers.EmptyHeadersInjector;
|
||||
import com.danikula.videocache.headers.HeaderInjector;
|
||||
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 +38,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 +63,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());
|
||||
@@ -77,69 +75,51 @@ public class HttpProxyCacheServer {
|
||||
InetAddress inetAddress = InetAddress.getByName(PROXY_HOST);
|
||||
this.serverSocket = new ServerSocket(0, 8, inetAddress);
|
||||
this.port = serverSocket.getLocalPort();
|
||||
IgnoreHostProxySelector.install(PROXY_HOST, port);
|
||||
CountDownLatch startSignal = new CountDownLatch(1);
|
||||
this.waitConnectionThread = new Thread(new WaitRequestsRunnable(startSignal));
|
||||
this.waitConnectionThread.start();
|
||||
startSignal.await(); // freeze thread, wait for server starts
|
||||
Log.i(LOG_TAG, "Proxy cache server started. Ping it...");
|
||||
makeSureServerWorks();
|
||||
this.pinger = new Pinger(PROXY_HOST, port);
|
||||
LOG.info("Proxy cache server started. Is it alive? " + isAlive());
|
||||
} catch (IOException | InterruptedException e) {
|
||||
socketProcessor.shutdown();
|
||||
throw new IllegalStateException("Error starting local proxy server", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void makeSureServerWorks() {
|
||||
int maxPingAttempts = 3;
|
||||
int delay = 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 +128,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 +139,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 +153,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 +181,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 +216,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 +227,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 +238,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 +282,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 +290,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 +309,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,38 +341,34 @@ 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;
|
||||
private HeaderInjector headerInjector;
|
||||
|
||||
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();
|
||||
this.headerInjector = new EmptyHeadersInjector();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 +392,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 +409,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.
|
||||
@@ -414,6 +419,28 @@ public class HttpProxyCacheServer {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set custom DiskUsage logic for handling when to keep or clean cache.
|
||||
*
|
||||
* @param diskUsage a disk usage strategy, cant be {@code null}.
|
||||
* @return a builder.
|
||||
*/
|
||||
public Builder diskUsage(DiskUsage diskUsage) {
|
||||
this.diskUsage = checkNotNull(diskUsage);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add headers along the request to the server
|
||||
*
|
||||
* @param headerInjector to inject header base on url
|
||||
* @return a builder
|
||||
*/
|
||||
public Builder headerInjector(HeaderInjector headerInjector) {
|
||||
this.headerInjector = checkNotNull(headerInjector);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds new instance of {@link HttpProxyCacheServer}.
|
||||
*
|
||||
@@ -425,7 +452,7 @@ public class HttpProxyCacheServer {
|
||||
}
|
||||
|
||||
private Config buildConfig() {
|
||||
return new Config(cacheRoot, fileNameGenerator, diskUsage);
|
||||
return new Config(cacheRoot, fileNameGenerator, diskUsage, sourceInfoStorage, headerInjector);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ final class HttpProxyCacheServerClients {
|
||||
}
|
||||
|
||||
private HttpProxyCache newHttpProxyCache() throws ProxyCacheException {
|
||||
HttpUrlSource source = new HttpUrlSource(url);
|
||||
HttpUrlSource source = new HttpUrlSource(url, config.sourceInfoStorage, config.headerInjector);
|
||||
FileCache cache = new FileCache(config.generateCacheFile(url), config.diskUsage);
|
||||
HttpProxyCache httpProxyCache = new HttpProxyCache(source, cache);
|
||||
httpProxyCache.registerCacheListener(uiCacheListener);
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
package com.danikula.videocache;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import com.danikula.videocache.headers.EmptyHeadersInjector;
|
||||
import com.danikula.videocache.headers.HeaderInjector;
|
||||
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;
|
||||
@@ -9,9 +16,10 @@ import java.io.InputStream;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.util.Map;
|
||||
|
||||
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 +33,117 @@ 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 final HeaderInjector headerInjector;
|
||||
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(url, sourceInfoStorage, new EmptyHeadersInjector());
|
||||
}
|
||||
|
||||
public HttpUrlSource(String url, SourceInfoStorage sourceInfoStorage, HeaderInjector headerInjector) {
|
||||
this.sourceInfoStorage = checkNotNull(sourceInfoStorage);
|
||||
this.headerInjector = checkNotNull(headerInjector);
|
||||
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;
|
||||
this.headerInjector = source.headerInjector;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized int length() throws ProxyCacheException {
|
||||
if (length == Integer.MIN_VALUE) {
|
||||
public synchronized long length() throws ProxyCacheException {
|
||||
if (sourceInfo.length == Integer.MIN_VALUE) {
|
||||
fetchContentInfo();
|
||||
}
|
||||
return length;
|
||||
return sourceInfo.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void open(int offset) throws ProxyCacheException {
|
||||
public void open(long 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());
|
||||
long 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();
|
||||
private long readSourceAvailableBytes(HttpURLConnection connection, long offset, int responseCode) throws IOException {
|
||||
long contentLength = getContentLength(connection);
|
||||
return responseCode == HTTP_OK ? contentLength
|
||||
: responseCode == HTTP_PARTIAL ? contentLength + offset : length;
|
||||
: responseCode == HTTP_PARTIAL ? contentLength + offset : sourceInfo.length;
|
||||
}
|
||||
|
||||
private long getContentLength(HttpURLConnection connection) {
|
||||
String contentLengthValue = connection.getHeaderField("Content-Length");
|
||||
return contentLengthValue == null ? -1 : Long.parseLong(contentLengthValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws ProxyCacheException {
|
||||
if (connection != null) {
|
||||
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);
|
||||
} catch (ArrayIndexOutOfBoundsException e) {
|
||||
LOG.error("Error closing connection correctly. Should happen only on Android L. " +
|
||||
"If anybody know how to fix it, please visit https://github.com/danikula/AndroidVideoCache/issues/88. " +
|
||||
"Until good solution is not know, just ignore this issue :(", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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);
|
||||
long length = getContentLength(urlConnection);
|
||||
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 +152,15 @@ public class HttpUrlSource implements Source {
|
||||
}
|
||||
}
|
||||
|
||||
private HttpURLConnection openConnection(int offset, String method, int timeout) throws IOException, ProxyCacheException {
|
||||
private HttpURLConnection openConnection(long offset, int timeout) throws IOException, ProxyCacheException {
|
||||
HttpURLConnection connection;
|
||||
boolean redirected;
|
||||
int redirectCount = 0;
|
||||
String url = this.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);
|
||||
injectCustomHeaders(connection, url);
|
||||
if (offset > 0) {
|
||||
connection.setRequestProperty("Range", "bytes=" + offset + "-");
|
||||
}
|
||||
@@ -144,19 +182,26 @@ public class HttpUrlSource implements Source {
|
||||
return connection;
|
||||
}
|
||||
|
||||
private void injectCustomHeaders(HttpURLConnection connection, String url) {
|
||||
Map<String, String> extraHeaders = headerInjector.addHeaders(url);
|
||||
for (Map.Entry<String, String> header : extraHeaders.entrySet()) {
|
||||
connection.setRequestProperty(header.getKey(), header.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
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 + "}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.danikula.videocache;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.Proxy;
|
||||
import java.net.ProxySelector;
|
||||
import java.net.SocketAddress;
|
||||
import java.net.URI;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static com.danikula.videocache.Preconditions.checkNotNull;
|
||||
|
||||
/**
|
||||
* {@link ProxySelector} that ignore system default proxies for concrete host.
|
||||
* <p>
|
||||
* It is important to <a href="https://github.com/danikula/AndroidVideoCache/issues/28">ignore system proxy</a> for localhost connection.
|
||||
*
|
||||
* @author Alexey Danilov (danikula@gmail.com).
|
||||
*/
|
||||
class IgnoreHostProxySelector extends ProxySelector {
|
||||
|
||||
private static final List<Proxy> NO_PROXY_LIST = Arrays.asList(Proxy.NO_PROXY);
|
||||
|
||||
private final ProxySelector defaultProxySelector;
|
||||
private final String hostToIgnore;
|
||||
private final int portToIgnore;
|
||||
|
||||
IgnoreHostProxySelector(ProxySelector defaultProxySelector, String hostToIgnore, int portToIgnore) {
|
||||
this.defaultProxySelector = checkNotNull(defaultProxySelector);
|
||||
this.hostToIgnore = checkNotNull(hostToIgnore);
|
||||
this.portToIgnore = portToIgnore;
|
||||
}
|
||||
|
||||
static void install(String hostToIgnore, int portToIgnore) {
|
||||
ProxySelector defaultProxySelector = ProxySelector.getDefault();
|
||||
ProxySelector ignoreHostProxySelector = new IgnoreHostProxySelector(defaultProxySelector, hostToIgnore, portToIgnore);
|
||||
ProxySelector.setDefault(ignoreHostProxySelector);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Proxy> select(URI uri) {
|
||||
boolean ignored = hostToIgnore.equals(uri.getHost()) && portToIgnore == uri.getPort();
|
||||
return ignored ? NO_PROXY_LIST : defaultProxySelector.select(uri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connectFailed(URI uri, SocketAddress address, IOException failure) {
|
||||
defaultProxySelector.connectFailed(uri, address, failure);
|
||||
}
|
||||
}
|
||||
127
library/src/main/java/com/danikula/videocache/Pinger.java
Normal file
127
library/src/main/java/com/danikula/videocache/Pinger.java
Normal file
@@ -0,0 +1,127 @@
|
||||
package com.danikula.videocache;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.Proxy;
|
||||
import java.net.ProxySelector;
|
||||
import java.net.Socket;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import static com.danikula.videocache.Preconditions.checkArgument;
|
||||
import static com.danikula.videocache.Preconditions.checkNotNull;
|
||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||
|
||||
/**
|
||||
* Pings {@link HttpProxyCacheServer} to make sure it works.
|
||||
*
|
||||
* @author Alexey Danilov (danikula@gmail.com).
|
||||
*/
|
||||
|
||||
class Pinger {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger("Pinger");
|
||||
private static final String PING_REQUEST = "ping";
|
||||
private static final String PING_RESPONSE = "ping ok";
|
||||
|
||||
private final ExecutorService pingExecutor = Executors.newSingleThreadExecutor();
|
||||
private final String host;
|
||||
private final int port;
|
||||
|
||||
Pinger(String host, int port) {
|
||||
this.host = checkNotNull(host);
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
boolean ping(int maxAttempts, int startTimeout) {
|
||||
checkArgument(maxAttempts >= 1);
|
||||
checkArgument(startTimeout > 0);
|
||||
|
||||
int timeout = startTimeout;
|
||||
int attempts = 0;
|
||||
while (attempts < maxAttempts) {
|
||||
try {
|
||||
Future<Boolean> pingFuture = pingExecutor.submit(new PingCallable());
|
||||
boolean pinged = pingFuture.get(timeout, MILLISECONDS);
|
||||
if (pinged) {
|
||||
return true;
|
||||
}
|
||||
} catch (TimeoutException e) {
|
||||
LOG.warn("Error pinging server (attempt: " + attempts + ", timeout: " + timeout + "). ");
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
LOG.error("Error pinging server due to unexpected error", e);
|
||||
}
|
||||
attempts++;
|
||||
timeout *= 2;
|
||||
}
|
||||
String error = String.format(Locale.US, "Error pinging server (attempts: %d, max timeout: %d). " +
|
||||
"If you see this message, please, report at https://github.com/danikula/AndroidVideoCache/issues/134. " +
|
||||
"Default proxies are: %s"
|
||||
, attempts, timeout / 2, getDefaultProxies());
|
||||
LOG.error(error, new ProxyCacheException(error));
|
||||
return false;
|
||||
}
|
||||
|
||||
private List<Proxy> getDefaultProxies() {
|
||||
try {
|
||||
ProxySelector defaultProxySelector = ProxySelector.getDefault();
|
||||
return defaultProxySelector.select(new URI(getPingUrl()));
|
||||
} catch (URISyntaxException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
boolean isPingRequest(String request) {
|
||||
return PING_REQUEST.equals(request);
|
||||
}
|
||||
|
||||
void responseToPing(Socket socket) throws IOException {
|
||||
OutputStream out = socket.getOutputStream();
|
||||
out.write("HTTP/1.1 200 OK\n\n".getBytes());
|
||||
out.write(PING_RESPONSE.getBytes());
|
||||
}
|
||||
|
||||
private boolean pingServer() throws ProxyCacheException {
|
||||
String pingUrl = getPingUrl();
|
||||
HttpUrlSource source = new HttpUrlSource(pingUrl);
|
||||
try {
|
||||
byte[] expectedResponse = PING_RESPONSE.getBytes();
|
||||
source.open(0);
|
||||
byte[] response = new byte[expectedResponse.length];
|
||||
source.read(response);
|
||||
boolean pingOk = Arrays.equals(expectedResponse, response);
|
||||
LOG.info("Ping response: `" + new String(response) + "`, pinged? " + pingOk);
|
||||
return pingOk;
|
||||
} catch (ProxyCacheException e) {
|
||||
LOG.error("Error reading ping response", e);
|
||||
return false;
|
||||
} finally {
|
||||
source.close();
|
||||
}
|
||||
}
|
||||
|
||||
private String getPingUrl() {
|
||||
return String.format(Locale.US, "http://%s:%d/%s", host, port, PING_REQUEST);
|
||||
}
|
||||
|
||||
private class PingCallable implements Callable<Boolean> {
|
||||
|
||||
@Override
|
||||
public Boolean call() throws Exception {
|
||||
return pingServer();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) ((float) cacheAvailable / sourceLength * 100);
|
||||
boolean percentsChanged = percents != percentsAvailable;
|
||||
boolean sourceLengthKnown = sourceAvailable >= 0;
|
||||
boolean sourceLengthKnown = sourceLength >= 0;
|
||||
if (sourceLengthKnown && percentsChanged) {
|
||||
onCachePercentsAvailableChanged(percents);
|
||||
}
|
||||
@@ -114,8 +116,8 @@ class ProxyCache {
|
||||
}
|
||||
|
||||
private void readSource() {
|
||||
int sourceAvailable = -1;
|
||||
int offset = 0;
|
||||
long sourceAvailable = -1;
|
||||
long offset = 0;
|
||||
try {
|
||||
offset = cache.available();
|
||||
source.open(offset);
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,15 +7,17 @@ package com.danikula.videocache;
|
||||
*/
|
||||
public class ProxyCacheException extends Exception {
|
||||
|
||||
private static final String LIBRARY_VERSION = ". Version: 2.7.1";
|
||||
|
||||
public ProxyCacheException(String message) {
|
||||
super(message);
|
||||
super(message + LIBRARY_VERSION);
|
||||
}
|
||||
|
||||
public ProxyCacheException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
super(message + LIBRARY_VERSION, cause);
|
||||
}
|
||||
|
||||
public ProxyCacheException(Throwable cause) {
|
||||
super(cause);
|
||||
super("No explanation error" + LIBRARY_VERSION, cause);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ public interface Source {
|
||||
* @param offset offset in bytes for source.
|
||||
* @throws ProxyCacheException if error occur while opening source.
|
||||
*/
|
||||
void open(int offset) throws ProxyCacheException;
|
||||
void open(long offset) throws ProxyCacheException;
|
||||
|
||||
/**
|
||||
* Returns length bytes or <b>negative value</b> if length is unknown.
|
||||
@@ -21,12 +21,13 @@ public interface Source {
|
||||
* @return bytes length
|
||||
* @throws ProxyCacheException if error occur while fetching source data.
|
||||
*/
|
||||
int length() throws ProxyCacheException;
|
||||
long length() throws ProxyCacheException;
|
||||
|
||||
/**
|
||||
* Read data to byte buffer from source with current offset.
|
||||
*
|
||||
* @param buffer a buffer to be used for reading data.
|
||||
* @return a count of read bytes
|
||||
* @throws ProxyCacheException if error occur while reading source.
|
||||
*/
|
||||
int read(byte[] buffer) throws ProxyCacheException;
|
||||
|
||||
@@ -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 long length;
|
||||
public final String mime;
|
||||
|
||||
public SourceInfo(String url, long length, String mime) {
|
||||
this.url = url;
|
||||
this.length = length;
|
||||
this.mime = mime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SourceInfo{" +
|
||||
"url='" + url + '\'' +
|
||||
", length=" + length +
|
||||
", mime='" + mime + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ public class FileCache implements Cache {
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized int available() throws ProxyCacheException {
|
||||
public synchronized long available() throws ProxyCacheException {
|
||||
try {
|
||||
return (int) dataFile.length();
|
||||
} catch (IOException e) {
|
||||
@@ -100,6 +100,7 @@ public class FileCache implements Cache {
|
||||
file = completedFile;
|
||||
try {
|
||||
dataFile = new RandomAccessFile(file, "r");
|
||||
diskUsage.touch(file);
|
||||
} catch (IOException e) {
|
||||
throw new ProxyCacheException("Error opening " + file + " as disc cache", e);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
package com.danikula.videocache.file;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
@@ -16,6 +20,8 @@ import java.util.List;
|
||||
*/
|
||||
class Files {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger("Files");
|
||||
|
||||
static void makeDir(File directory) throws IOException {
|
||||
if (directory.exists()) {
|
||||
if (!directory.isDirectory()) {
|
||||
@@ -46,7 +52,8 @@ class Files {
|
||||
if (!modified) {
|
||||
modify(file);
|
||||
if (file.lastModified() < now) {
|
||||
throw new IOException("Error set last modified date to " + file);
|
||||
// NOTE: apparently this is a known issue (see: http://stackoverflow.com/questions/6633748/file-lastmodified-is-never-what-was-set-with-file-setlastmodified)
|
||||
LOG.warn("Last modified date {} is not set for file {}", new Date(file.lastModified()), file.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.danikula.videocache.file;
|
||||
|
||||
import android.util.Log;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -14,9 +15,9 @@ import java.util.concurrent.Executors;
|
||||
*
|
||||
* @author Alexey Danilov (danikula@gmail.com).
|
||||
*/
|
||||
abstract class LruDiskUsage implements DiskUsage {
|
||||
public abstract class LruDiskUsage implements DiskUsage {
|
||||
|
||||
private static final String LOG_TAG = "ProxyCache";
|
||||
private static final Logger LOG = LoggerFactory.getLogger("LruDiskUsage");
|
||||
private final ExecutorService workerThread = Executors.newSingleThreadExecutor();
|
||||
|
||||
@Override
|
||||
@@ -43,9 +44,9 @@ abstract class LruDiskUsage implements DiskUsage {
|
||||
if (deleted) {
|
||||
totalCount--;
|
||||
totalSize -= fileSize;
|
||||
Log.i(LOG_TAG, "Cache file " + file + " is deleted because it exceeds cache limit");
|
||||
LOG.info("Cache file " + file + " is deleted because it exceeds cache limit");
|
||||
} else {
|
||||
Log.e(LOG_TAG, "Error deleting file " + file + " for trimming cache");
|
||||
LOG.error("Error deleting file " + file + " for trimming cache");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.danikula.videocache.headers;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Empty {@link HeaderInjector} implementation.
|
||||
*
|
||||
* @author Lucas Nelaupe (https://github.com/lucas34).
|
||||
*/
|
||||
public class EmptyHeadersInjector implements HeaderInjector {
|
||||
|
||||
@Override
|
||||
public Map<String, String> addHeaders(String url) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.danikula.videocache.headers;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Allows to add custom headers to server's requests.
|
||||
*
|
||||
* @author Lucas Nelaupe (https://github.com/lucas34).
|
||||
*/
|
||||
public interface HeaderInjector {
|
||||
|
||||
/**
|
||||
* Adds headers to server's requests for corresponding url.
|
||||
*
|
||||
* @param url an url headers will be added for
|
||||
* @return a map with headers, where keys are header's names, and values are header's values. {@code null} is not acceptable!
|
||||
*/
|
||||
Map<String, String> addHeaders(String url);
|
||||
|
||||
}
|
||||
@@ -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.getLong(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;
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.4'
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
maven { url 'https://github.com/dahlgren/vpi-aar/raw/master' }
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'com.neenbedankt.android-apt'
|
||||
|
||||
android {
|
||||
compileSdkVersion 23
|
||||
buildToolsVersion '23.0.1'
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.danikula.videocache.sample"
|
||||
minSdkVersion 15
|
||||
targetSdkVersion 23
|
||||
versionCode 1
|
||||
versionName '1.0'
|
||||
}
|
||||
}
|
||||
|
||||
apt {
|
||||
arguments {
|
||||
androidManifestFile variant.outputs[0].processResources.manifestFile
|
||||
resourcePackageName android.defaultConfig.applicationId
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// compile project(':library')
|
||||
compile 'com.android.support:support-v4:23.1.0'
|
||||
compile 'org.androidannotations:androidannotations-api:3.3.2'
|
||||
compile 'com.danikula:videocache:2.3.2'
|
||||
compile 'com.viewpagerindicator:library:2.4.2-SNAPSHOT@aar'
|
||||
apt 'org.androidannotations:androidannotations:3.3.2'
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest
|
||||
package="com.danikula.videocache.sample"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
<application
|
||||
android:name=".App"
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@android:style/Theme.Holo.Light.NoActionBar">
|
||||
<activity
|
||||
android:name=".MenuActivity_"
|
||||
android:label="@string/app_name">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name=".SingleVideoActivity_" />
|
||||
<activity android:name=".MultipleVideosActivity_" />
|
||||
<activity android:name=".VideoGalleryActivity_" />
|
||||
<activity android:name=".SharedCacheActivity_" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -1,23 +0,0 @@
|
||||
package com.danikula.videocache.sample;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
|
||||
import com.danikula.videocache.HttpProxyCacheServer;
|
||||
|
||||
/**
|
||||
* @author Alexey Danilov (danikula@gmail.com).
|
||||
*/
|
||||
public class App extends Application {
|
||||
|
||||
private HttpProxyCacheServer proxy;
|
||||
|
||||
public static HttpProxyCacheServer getProxy(Context context) {
|
||||
App app = (App) context.getApplicationContext();
|
||||
return app.proxy == null ? (app.proxy = app.newProxy()) : app.proxy;
|
||||
}
|
||||
|
||||
private HttpProxyCacheServer newProxy() {
|
||||
return new HttpProxyCacheServer(this);
|
||||
}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
package com.danikula.videocache.sample;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.VideoView;
|
||||
|
||||
import com.danikula.videocache.CacheListener;
|
||||
import com.danikula.videocache.HttpProxyCacheServer;
|
||||
|
||||
import org.androidannotations.annotations.AfterViews;
|
||||
import org.androidannotations.annotations.EFragment;
|
||||
import org.androidannotations.annotations.FragmentArg;
|
||||
import org.androidannotations.annotations.InstanceState;
|
||||
import org.androidannotations.annotations.SeekBarTouchStop;
|
||||
import org.androidannotations.annotations.ViewById;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
@EFragment(R.layout.fragment_video)
|
||||
public class GalleryVideoFragment extends Fragment implements CacheListener {
|
||||
|
||||
private static final String LOG_TAG = "VideoFragment";
|
||||
|
||||
@FragmentArg String url;
|
||||
@FragmentArg String cachePath;
|
||||
|
||||
@InstanceState int position;
|
||||
@InstanceState boolean playerStarted;
|
||||
|
||||
@ViewById VideoView videoView;
|
||||
@ViewById ProgressBar progressBar;
|
||||
|
||||
private boolean visibleForUser;
|
||||
|
||||
private final VideoProgressUpdater updater = new VideoProgressUpdater();
|
||||
|
||||
public static Fragment build(Context context, Video video) {
|
||||
return build(video.url, video.getCacheFile(context).getAbsolutePath());
|
||||
}
|
||||
|
||||
public static Fragment build(String url, String cachePath) {
|
||||
return GalleryVideoFragment_.builder()
|
||||
.url(url)
|
||||
.cachePath(cachePath)
|
||||
.build();
|
||||
}
|
||||
|
||||
@AfterViews
|
||||
void afterViewInjected() {
|
||||
startProxy();
|
||||
|
||||
if (visibleForUser) {
|
||||
startPlayer();
|
||||
}
|
||||
}
|
||||
|
||||
private void startPlayer() {
|
||||
videoView.seekTo(position);
|
||||
videoView.start();
|
||||
playerStarted = true;
|
||||
}
|
||||
|
||||
private void startProxy() {
|
||||
HttpProxyCacheServer proxy = App.getProxy(getActivity());
|
||||
proxy.registerCacheListener(this, url);
|
||||
videoView.setVideoPath(proxy.getProxyUrl(url));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUserVisibleHint(boolean isVisibleToUser) {
|
||||
super.setUserVisibleHint(isVisibleToUser);
|
||||
|
||||
visibleForUser = isVisibleToUser;
|
||||
if (videoView != null) {
|
||||
if (visibleForUser) {
|
||||
startPlayer();
|
||||
} else if (playerStarted) {
|
||||
position = videoView.getCurrentPosition();
|
||||
videoView.pause();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
updater.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
updater.stop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
videoView.stopPlayback();
|
||||
App.getProxy(getActivity()).unregisterCacheListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCacheAvailable(File file, String url, int percentsAvailable) {
|
||||
progressBar.setSecondaryProgress(percentsAvailable);
|
||||
}
|
||||
|
||||
private void updateVideoProgress() {
|
||||
int videoProgress = videoView.getCurrentPosition() * 100 / videoView.getDuration();
|
||||
progressBar.setProgress(videoProgress);
|
||||
}
|
||||
|
||||
@SeekBarTouchStop(R.id.progressBar)
|
||||
void seekVideo() {
|
||||
int videoPosition = videoView.getDuration() * progressBar.getProgress() / 100;
|
||||
videoView.seekTo(videoPosition);
|
||||
}
|
||||
|
||||
private final class VideoProgressUpdater extends Handler {
|
||||
|
||||
public void start() {
|
||||
sendEmptyMessage(0);
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
removeMessages(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
updateVideoProgress();
|
||||
sendEmptyMessageDelayed(0, 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
package com.danikula.videocache.sample;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
import android.util.Log;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ListAdapter;
|
||||
import android.widget.ListView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.androidannotations.annotations.AfterViews;
|
||||
import org.androidannotations.annotations.Click;
|
||||
import org.androidannotations.annotations.EActivity;
|
||||
import org.androidannotations.annotations.ItemClick;
|
||||
import org.androidannotations.annotations.ViewById;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@EActivity(R.layout.activity_menu)
|
||||
public class MenuActivity extends FragmentActivity {
|
||||
|
||||
@ViewById ListView listView;
|
||||
|
||||
@AfterViews
|
||||
void onViewInjected() {
|
||||
ListAdapter adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, android.R.id.text1, buildListData());
|
||||
listView.setAdapter(adapter);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private List<ListEntry> buildListData() {
|
||||
return Arrays.asList(
|
||||
new ListEntry("Single Video", SingleVideoActivity_.class),
|
||||
new ListEntry("Multiple Videos", MultipleVideosActivity_.class),
|
||||
new ListEntry("Video Gallery with pre-caching", VideoGalleryActivity_.class),
|
||||
new ListEntry("Shared Cache", SharedCacheActivity_.class)
|
||||
);
|
||||
}
|
||||
|
||||
@ItemClick(R.id.listView)
|
||||
void onListItemClicked(int position) {
|
||||
ListEntry item = (ListEntry) listView.getAdapter().getItem(position);
|
||||
startActivity(new Intent(this, item.activityClass));
|
||||
}
|
||||
|
||||
@Click(R.id.cleanCacheButton)
|
||||
void onClearCacheButtonClick() {
|
||||
try {
|
||||
Utils.cleanDirectory(getExternalCacheDir());
|
||||
} catch (IOException e) {
|
||||
Log.e(null, "Error cleaning cache", e);
|
||||
Toast.makeText(this, "Error cleaning cache", Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ListEntry {
|
||||
|
||||
private final String title;
|
||||
private final Class activityClass;
|
||||
|
||||
public ListEntry(String title, Class activityClass) {
|
||||
this.title = title;
|
||||
this.activityClass = activityClass;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return title;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package com.danikula.videocache.sample;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
|
||||
import org.androidannotations.annotations.EActivity;
|
||||
|
||||
@EActivity(R.layout.activity_multiple_videos)
|
||||
public class MultipleVideosActivity extends FragmentActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle state) {
|
||||
super.onCreate(state);
|
||||
|
||||
if (state == null) {
|
||||
addVideoFragment(Video.ORANGE_1, R.id.videoContainer0);
|
||||
addVideoFragment(Video.ORANGE_2, R.id.videoContainer1);
|
||||
addVideoFragment(Video.ORANGE_3, R.id.videoContainer2);
|
||||
addVideoFragment(Video.ORANGE_4, R.id.videoContainer3);
|
||||
}
|
||||
}
|
||||
|
||||
private void addVideoFragment(Video video, int containerViewId) {
|
||||
getSupportFragmentManager()
|
||||
.beginTransaction()
|
||||
.add(containerViewId, VideoFragment.build(this, video))
|
||||
.commit();
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package com.danikula.videocache.sample;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
|
||||
import org.androidannotations.annotations.EActivity;
|
||||
|
||||
@EActivity(R.layout.activity_multiple_videos)
|
||||
public class SharedCacheActivity extends FragmentActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle state) {
|
||||
super.onCreate(state);
|
||||
|
||||
if (state == null) {
|
||||
addVideoFragment(Video.ORANGE_1, R.id.videoContainer0);
|
||||
addVideoFragment(Video.ORANGE_1, R.id.videoContainer1);
|
||||
addVideoFragment(Video.ORANGE_1, R.id.videoContainer2);
|
||||
addVideoFragment(Video.ORANGE_1, R.id.videoContainer3);
|
||||
}
|
||||
}
|
||||
|
||||
private void addVideoFragment(Video video, int containerViewId) {
|
||||
getSupportFragmentManager()
|
||||
.beginTransaction()
|
||||
.add(containerViewId, VideoFragment.build(this, video))
|
||||
.commit();
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package com.danikula.videocache.sample;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
|
||||
import org.androidannotations.annotations.EActivity;
|
||||
|
||||
@EActivity(R.layout.activity_single_video)
|
||||
public class SingleVideoActivity extends FragmentActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle state) {
|
||||
super.onCreate(state);
|
||||
|
||||
if (state == null) {
|
||||
getSupportFragmentManager()
|
||||
.beginTransaction()
|
||||
.add(R.id.containerView, VideoFragment.build(this, Video.ORANGE_1))
|
||||
.commit();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package com.danikula.videocache.sample;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Some utils methods.
|
||||
*
|
||||
* @author Alexey Danilov (danikula@gmail.com).
|
||||
*/
|
||||
public class Utils {
|
||||
|
||||
public static void cleanDirectory(File file) throws IOException {
|
||||
if (!file.exists()) {
|
||||
return;
|
||||
}
|
||||
File[] contentFiles = file.listFiles();
|
||||
if (contentFiles != null) {
|
||||
for (File contentFile : contentFiles) {
|
||||
delete(contentFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void delete(File file) throws IOException {
|
||||
if (file.isFile() && file.exists()) {
|
||||
deleteOrThrow(file);
|
||||
} else {
|
||||
cleanDirectory(file);
|
||||
deleteOrThrow(file);
|
||||
}
|
||||
}
|
||||
|
||||
private static void deleteOrThrow(File file) throws IOException {
|
||||
if (file.exists()) {
|
||||
boolean isDeleted = file.delete();
|
||||
if (!isDeleted) {
|
||||
throw new IOException(String.format("File %s can't be deleted", file.getAbsolutePath()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package com.danikula.videocache.sample;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public enum Video {
|
||||
|
||||
ORANGE_1("https://dl.dropboxusercontent.com/u/15506779/persistent/proxycache/orange1.mp4"),
|
||||
ORANGE_2("https://dl.dropboxusercontent.com/u/15506779/persistent/proxycache/orange2.mp4"),
|
||||
ORANGE_3("https://dl.dropboxusercontent.com/u/15506779/persistent/proxycache/orange3.mp4"),
|
||||
ORANGE_4("https://dl.dropboxusercontent.com/u/15506779/persistent/proxycache/orange4.mp4"),
|
||||
ORANGE_5("https://dl.dropboxusercontent.com/u/15506779/persistent/proxycache/orange5.mp4");
|
||||
|
||||
public final String url;
|
||||
|
||||
Video(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public File getCacheFile(Context context) {
|
||||
return new File(context.getExternalCacheDir(), name());
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
package com.danikula.videocache.sample;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.util.Log;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.VideoView;
|
||||
|
||||
import com.danikula.videocache.CacheListener;
|
||||
import com.danikula.videocache.HttpProxyCacheServer;
|
||||
|
||||
import org.androidannotations.annotations.AfterViews;
|
||||
import org.androidannotations.annotations.EFragment;
|
||||
import org.androidannotations.annotations.FragmentArg;
|
||||
import org.androidannotations.annotations.SeekBarTouchStop;
|
||||
import org.androidannotations.annotations.ViewById;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
@EFragment(R.layout.fragment_video)
|
||||
public class VideoFragment extends Fragment implements CacheListener {
|
||||
|
||||
private static final String LOG_TAG = "VideoFragment";
|
||||
|
||||
@FragmentArg String url;
|
||||
@FragmentArg String cachePath;
|
||||
|
||||
@ViewById VideoView videoView;
|
||||
@ViewById ProgressBar progressBar;
|
||||
|
||||
private final VideoProgressUpdater updater = new VideoProgressUpdater();
|
||||
|
||||
public static Fragment build(Context context, Video video) {
|
||||
return build(video.url, video.getCacheFile(context).getAbsolutePath());
|
||||
}
|
||||
|
||||
public static Fragment build(String url, String cachePath) {
|
||||
return VideoFragment_.builder()
|
||||
.url(url)
|
||||
.cachePath(cachePath)
|
||||
.build();
|
||||
}
|
||||
|
||||
@AfterViews
|
||||
void afterViewInjected() {
|
||||
startVideo();
|
||||
}
|
||||
|
||||
private void startVideo() {
|
||||
HttpProxyCacheServer proxy = App.getProxy(getActivity());
|
||||
proxy.registerCacheListener(this, url);
|
||||
videoView.setVideoPath(proxy.getProxyUrl(url));
|
||||
videoView.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
updater.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
updater.stop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
videoView.stopPlayback();
|
||||
App.getProxy(getActivity()).unregisterCacheListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCacheAvailable(File file, String url, int percentsAvailable) {
|
||||
progressBar.setSecondaryProgress(percentsAvailable);
|
||||
Log.d(LOG_TAG, String.format("onCacheAvailable. percents: %d, file: %s, url: %s", percentsAvailable, file, url));
|
||||
}
|
||||
|
||||
private void updateVideoProgress() {
|
||||
int videoProgress = videoView.getCurrentPosition() * 100 / videoView.getDuration();
|
||||
progressBar.setProgress(videoProgress);
|
||||
}
|
||||
|
||||
@SeekBarTouchStop(R.id.progressBar)
|
||||
void seekVideo() {
|
||||
int videoPosition = videoView.getDuration() * progressBar.getProgress() / 100;
|
||||
videoView.seekTo(videoPosition);
|
||||
}
|
||||
|
||||
private final class VideoProgressUpdater extends Handler {
|
||||
|
||||
public void start() {
|
||||
sendEmptyMessage(0);
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
removeMessages(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
updateVideoProgress();
|
||||
sendEmptyMessageDelayed(0, 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package com.danikula.videocache.sample;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
import android.support.v4.app.FragmentStatePagerAdapter;
|
||||
import android.support.v4.view.ViewPager;
|
||||
|
||||
import com.viewpagerindicator.CirclePageIndicator;
|
||||
|
||||
import org.androidannotations.annotations.AfterViews;
|
||||
import org.androidannotations.annotations.EActivity;
|
||||
import org.androidannotations.annotations.ViewById;
|
||||
|
||||
@EActivity(R.layout.activity_video_gallery)
|
||||
public class VideoGalleryActivity extends FragmentActivity {
|
||||
|
||||
@ViewById ViewPager viewPager;
|
||||
@ViewById CirclePageIndicator viewPagerIndicator;
|
||||
|
||||
@AfterViews
|
||||
void afterViewInjected() {
|
||||
ViewsPagerAdapter viewsPagerAdapter = new ViewsPagerAdapter(this);
|
||||
viewPager.setAdapter(viewsPagerAdapter);
|
||||
viewPagerIndicator.setViewPager(viewPager);
|
||||
}
|
||||
|
||||
private static final class ViewsPagerAdapter extends FragmentStatePagerAdapter {
|
||||
|
||||
private final Context context;
|
||||
|
||||
public ViewsPagerAdapter(FragmentActivity activity) {
|
||||
super(activity.getSupportFragmentManager());
|
||||
this.context = activity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Fragment getItem(int position) {
|
||||
Video video = Video.values()[position];
|
||||
return GalleryVideoFragment.build(context, video);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return Video.values().length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getPageTitle(int position) {
|
||||
return Video.values()[position].name();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
<LinearLayout
|
||||
android:id="@+id/containerView"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:divider="@android:drawable/divider_horizontal_dark"
|
||||
android:orientation="vertical"
|
||||
android:showDividers="middle"
|
||||
tools:context=".MenuActivity">
|
||||
|
||||
<Button
|
||||
android:id="@+id/cleanCacheButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Clean cache" />
|
||||
|
||||
<ListView
|
||||
android:id="@+id/listView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -1,58 +0,0 @@
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#CCC"
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/videoContainer0"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="8dp"
|
||||
android:layout_weight="1"
|
||||
android:background="#FFF" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/videoContainer1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="8dp"
|
||||
android:layout_weight="1"
|
||||
android:background="#FFF" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/videoContainer2"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="8dp"
|
||||
android:layout_weight="1"
|
||||
android:background="#FFF" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/videoContainer3"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="8dp"
|
||||
android:layout_weight="1"
|
||||
android:background="#FFF" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
@@ -1,7 +0,0 @@
|
||||
<FrameLayout
|
||||
android:id="@+id/containerView"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".VideoActivity" />
|
||||
@@ -1,23 +0,0 @@
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<android.support.v4.view.ViewPager
|
||||
android:id="@+id/viewPager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<com.viewpagerindicator.CirclePageIndicator
|
||||
android:id="@+id/viewPagerIndicator"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="8dp"
|
||||
app:fillColor="#800F"
|
||||
app:radius="10dp"
|
||||
app:strokeColor="#800F" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -1,23 +0,0 @@
|
||||
<RelativeLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".VideoActivity">
|
||||
|
||||
<VideoView
|
||||
android:id="@+id/videoView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<SeekBar
|
||||
android:id="@+id/progressBar"
|
||||
style="@android:style/Widget.Holo.ProgressBar.Horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_margin="16dp"
|
||||
android:max="100" />
|
||||
|
||||
</RelativeLayout>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 7.5 KiB |
@@ -1,3 +0,0 @@
|
||||
<resources>
|
||||
<string name="app_name">VideoCacheSample</string>
|
||||
</resources>
|
||||
@@ -1 +1 @@
|
||||
include ':sample', ':library', ':test'
|
||||
include ':library', ':test'
|
||||
|
||||
@@ -5,42 +5,66 @@ repositories {
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
compileSdkVersion 22
|
||||
buildToolsVersion '22.0.1'
|
||||
compileSdkVersion 23
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile project (':library')
|
||||
// temporary workaround for Robolectric issue https://github.com/robolectric/robolectric/issues/2647
|
||||
android.applicationVariants.all { variant ->
|
||||
def productFlavor = variant.productFlavors[0] != null ? "${variant.productFlavors[0].name.capitalize()}" : ""
|
||||
def buildType = "${variant.buildType.name.capitalize()}"
|
||||
tasks["compile${productFlavor}${buildType}UnitTestSources"].dependsOn(tasks["merge${productFlavor}${buildType}Assets"])
|
||||
}
|
||||
|
||||
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'
|
||||
// display test progress http://stackoverflow.com/a/36130467/999458
|
||||
tasks.withType(Test) {
|
||||
testLogging {
|
||||
// set options for log level LIFECYCLE
|
||||
events "passed", "skipped", "failed", "standardOut"
|
||||
showExceptions true
|
||||
exceptionFormat "full"
|
||||
showCauses true
|
||||
showStackTraces true
|
||||
|
||||
// set options for log level DEBUG and INFO
|
||||
debug {
|
||||
events "started", "passed", "skipped", "failed", "standardOut", "standardError"
|
||||
exceptionFormat "full"
|
||||
}
|
||||
info.events = debug.events
|
||||
info.exceptionFormat = debug.exceptionFormat
|
||||
|
||||
afterSuite { desc, result ->
|
||||
if (!desc.parent) { // will match the outermost suite
|
||||
def output = "Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} successes, ${result.failedTestCount} failures, ${result.skippedTestCount} skipped)"
|
||||
def startItem = '| ', endItem = ' |'
|
||||
def repeatLength = startItem.length() + output.length() + endItem.length()
|
||||
println('\n' + ('-' * repeatLength) + '\n' + startItem + output + endItem + '\n' + ('-' * repeatLength))
|
||||
}
|
||||
}
|
||||
}
|
||||
testCompile 'com.squareup:fest-android:1.0.0'
|
||||
testCompile 'com.google.guava:guava-jdk5:17.0'
|
||||
testCompile('com.danikula:android-garden:2.1.4') {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':library')
|
||||
testImplementation 'org.slf4j:slf4j-simple:1.7.21'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'org.robolectric:robolectric:3.3.2'
|
||||
testImplementation 'com.squareup:fest-android:1.0.0'
|
||||
testImplementation 'com.google.guava:guava-jdk5:17.0'
|
||||
testImplementation('com.danikula:android-garden:2.1.4') {
|
||||
exclude group: 'com.google.android'
|
||||
}
|
||||
testCompile 'org.mockito:mockito-all:1.9.5'
|
||||
testImplementation 'org.mockito:mockito-all:1.9.5'
|
||||
}
|
||||
|
||||
BIN
test/src/main/assets/space.jpg
Normal file
BIN
test/src/main/assets/space.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
17
test/src/test/java/com/danikula/videocache/BaseTest.java
Normal file
17
test/src/test/java/com/danikula/videocache/BaseTest.java
Normal file
@@ -0,0 +1,17 @@
|
||||
package com.danikula.videocache;
|
||||
|
||||
import com.danikula.videocache.test.BuildConfig;
|
||||
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(constants = BuildConfig.class)
|
||||
public abstract class BaseTest {
|
||||
|
||||
static {
|
||||
System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "trace");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
package com.danikula.videocache;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.util.Pair;
|
||||
|
||||
import com.danikula.android.garden.io.IoUtils;
|
||||
import com.danikula.videocache.file.FileNameGenerator;
|
||||
import com.danikula.videocache.file.Md5FileNameGenerator;
|
||||
import com.danikula.videocache.headers.HeaderInjector;
|
||||
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.mockito.Mockito;
|
||||
import org.robolectric.RuntimeEnvironment;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -33,16 +32,19 @@ import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_URL_
|
||||
import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_URL_6_REDIRECTS;
|
||||
import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_URL_ONE_REDIRECT;
|
||||
import static com.danikula.videocache.support.ProxyCacheTestUtils.getFileContent;
|
||||
import static com.danikula.videocache.support.ProxyCacheTestUtils.getPort;
|
||||
import static com.danikula.videocache.support.ProxyCacheTestUtils.installExternalSystemProxy;
|
||||
import static com.danikula.videocache.support.ProxyCacheTestUtils.loadAssetFile;
|
||||
import static com.danikula.videocache.support.ProxyCacheTestUtils.readProxyResponse;
|
||||
import static com.danikula.videocache.support.ProxyCacheTestUtils.resetSystemProxy;
|
||||
import static org.fest.assertions.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* @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;
|
||||
|
||||
@@ -51,6 +53,7 @@ public class HttpProxyCacheServerTest {
|
||||
cacheFolder = ProxyCacheTestUtils.newCacheFile();
|
||||
createDirectory(cacheFolder);
|
||||
cleanDirectory(cacheFolder);
|
||||
resetSystemProxy();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -82,7 +85,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 +213,174 @@ 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();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTrimFileCacheForTotalCountLru() throws Exception {
|
||||
FileNameGenerator fileNameGenerator = new Md5FileNameGenerator();
|
||||
HttpProxyCacheServer proxy = new HttpProxyCacheServer.Builder(RuntimeEnvironment.application)
|
||||
.cacheDirectory(cacheFolder)
|
||||
.fileNameGenerator(fileNameGenerator)
|
||||
.maxCacheFilesCount(2)
|
||||
.build();
|
||||
readProxyResponse(proxy, proxy.getProxyUrl(HTTP_DATA_URL), 0);
|
||||
assertThat(new File(cacheFolder, fileNameGenerator.generate(HTTP_DATA_URL))).exists();
|
||||
|
||||
readProxyResponse(proxy, proxy.getProxyUrl(HTTP_DATA_URL_ONE_REDIRECT), 0);
|
||||
assertThat(new File(cacheFolder, fileNameGenerator.generate(HTTP_DATA_URL_ONE_REDIRECT))).exists();
|
||||
|
||||
readProxyResponse(proxy, proxy.getProxyUrl(HTTP_DATA_URL_3_REDIRECTS), 0);
|
||||
assertThat(new File(cacheFolder, fileNameGenerator.generate(HTTP_DATA_URL_3_REDIRECTS))).exists();
|
||||
|
||||
waitForAsyncTrimming();
|
||||
assertThat(new File(cacheFolder, fileNameGenerator.generate(HTTP_DATA_URL))).doesNotExist();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTrimFileCacheForTotalSizeLru() throws Exception {
|
||||
FileNameGenerator fileNameGenerator = new Md5FileNameGenerator();
|
||||
HttpProxyCacheServer proxy = new HttpProxyCacheServer.Builder(RuntimeEnvironment.application)
|
||||
.cacheDirectory(cacheFolder)
|
||||
.fileNameGenerator(fileNameGenerator)
|
||||
.maxCacheSize(HTTP_DATA_SIZE * 3 - 1)
|
||||
.build();
|
||||
readProxyResponse(proxy, proxy.getProxyUrl(HTTP_DATA_URL), 0);
|
||||
assertThat(new File(cacheFolder, fileNameGenerator.generate(HTTP_DATA_URL))).exists();
|
||||
|
||||
readProxyResponse(proxy, proxy.getProxyUrl(HTTP_DATA_URL_ONE_REDIRECT), 0);
|
||||
assertThat(new File(cacheFolder, fileNameGenerator.generate(HTTP_DATA_URL_ONE_REDIRECT))).exists();
|
||||
|
||||
readProxyResponse(proxy, proxy.getProxyUrl(HTTP_DATA_URL_3_REDIRECTS), 0);
|
||||
assertThat(new File(cacheFolder, fileNameGenerator.generate(HTTP_DATA_URL_3_REDIRECTS))).exists();
|
||||
|
||||
waitForAsyncTrimming();
|
||||
assertThat(new File(cacheFolder, fileNameGenerator.generate(HTTP_DATA_URL))).doesNotExist();
|
||||
}
|
||||
|
||||
@Test // https://github.com/danikula/AndroidVideoCache/issues/28
|
||||
public void testWorkWithExternalProxy() throws Exception {
|
||||
installExternalSystemProxy();
|
||||
|
||||
Pair<File, Response> response = readProxyData(HTTP_DATA_URL, 0);
|
||||
assertThat(response.second.data).isEqualTo(loadAssetFile(ASSETS_DATA_NAME));
|
||||
}
|
||||
|
||||
@Test // https://github.com/danikula/AndroidVideoCache/issues/28
|
||||
public void testDoesNotWorkWithoutCustomProxySelector() throws Exception {
|
||||
HttpProxyCacheServer httpProxyCacheServer = new HttpProxyCacheServer(RuntimeEnvironment.application);
|
||||
// IgnoreHostProxySelector is set in HttpProxyCacheServer constructor. So let reset it by custom.
|
||||
installExternalSystemProxy();
|
||||
|
||||
String proxiedUrl = httpProxyCacheServer.getProxyUrl(HTTP_DATA_URL);
|
||||
// server can't proxy this url due to it is not alive (can't ping itself), so it returns original url
|
||||
assertThat(proxiedUrl).isEqualTo(HTTP_DATA_URL);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHeadersInjectorIsInvoked() throws Exception {
|
||||
HeaderInjector mockedHeaderInjector = Mockito.mock(HeaderInjector.class);
|
||||
|
||||
HttpProxyCacheServer proxy = new HttpProxyCacheServer.Builder(RuntimeEnvironment.application)
|
||||
.headerInjector(mockedHeaderInjector)
|
||||
.build();
|
||||
|
||||
readProxyResponse(proxy, HTTP_DATA_URL);
|
||||
proxy.shutdown();
|
||||
|
||||
verify(mockedHeaderInjector, times(2)).addHeaders(HTTP_DATA_URL); // content info & fetch data requests
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -236,4 +403,8 @@ public class HttpProxyCacheServerTest {
|
||||
.cacheDirectory(cacheDir)
|
||||
.build();
|
||||
}
|
||||
|
||||
private void waitForAsyncTrimming() throws InterruptedException {
|
||||
Thread.sleep(500);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
package com.danikula.videocache;
|
||||
|
||||
import com.danikula.videocache.test.BuildConfig;
|
||||
import com.danikula.videocache.headers.HeaderInjector;
|
||||
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 +25,13 @@ 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;
|
||||
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 HttpUrlSourceTest {
|
||||
public class HttpUrlSourceTest extends BaseTest {
|
||||
|
||||
@Test
|
||||
public void testHttpUrlSourceRange() throws Exception {
|
||||
@@ -70,7 +72,7 @@ public class HttpUrlSourceTest {
|
||||
public void testFetchInfoWithRedirect() throws Exception {
|
||||
HttpUrlSource source = new HttpUrlSource(HTTP_DATA_URL_ONE_REDIRECT);
|
||||
source.open(0);
|
||||
int available = source.length();
|
||||
long available = source.length();
|
||||
String mime = source.getMime();
|
||||
source.close();
|
||||
|
||||
@@ -83,7 +85,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 +98,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 +111,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 +132,49 @@ 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");
|
||||
}
|
||||
|
||||
@Test(expected = NullPointerException.class)
|
||||
public void testHeaderInjectorNullNotAcceptable() throws Exception {
|
||||
HeaderInjector mockedHeaderInjector = Mockito.mock(HeaderInjector.class);
|
||||
when(mockedHeaderInjector.addHeaders(Mockito.anyString())).thenReturn(null);
|
||||
SourceInfoStorage emptySourceInfoStorage = SourceInfoStorageFactory.newEmptySourceInfoStorage();
|
||||
HttpUrlSource source = new HttpUrlSource(HTTP_DATA_URL_ONE_REDIRECT, emptySourceInfoStorage, mockedHeaderInjector);
|
||||
source.open(0);
|
||||
fail("source.open should throw NPE!");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
82
test/src/test/java/com/danikula/videocache/PingerTest.java
Normal file
82
test/src/test/java/com/danikula/videocache/PingerTest.java
Normal file
@@ -0,0 +1,82 @@
|
||||
package com.danikula.videocache;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.robolectric.RuntimeEnvironment;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.net.Socket;
|
||||
|
||||
import static com.danikula.videocache.support.ProxyCacheTestUtils.getPort;
|
||||
import static com.danikula.videocache.support.ProxyCacheTestUtils.getPortWithoutPing;
|
||||
import static com.danikula.videocache.support.ProxyCacheTestUtils.installExternalSystemProxy;
|
||||
import static com.danikula.videocache.support.ProxyCacheTestUtils.resetSystemProxy;
|
||||
import static org.fest.assertions.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Tests {@link Pinger}.
|
||||
*
|
||||
* @author Alexey Danilov (danikula@gmail.com).
|
||||
*/
|
||||
public class PingerTest extends BaseTest {
|
||||
|
||||
@Before
|
||||
public void setup() throws Exception {
|
||||
resetSystemProxy();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPingSuccess() throws Exception {
|
||||
HttpProxyCacheServer server = new HttpProxyCacheServer(RuntimeEnvironment.application);
|
||||
Pinger pinger = new Pinger("127.0.0.1", getPort(server));
|
||||
boolean pinged = pinger.ping(1, 100);
|
||||
assertThat(pinged).isTrue();
|
||||
|
||||
server.shutdown();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPingFail() throws Exception {
|
||||
Pinger pinger = new Pinger("127.0.0.1", 33);
|
||||
boolean pinged = pinger.ping(3, 70);
|
||||
assertThat(pinged).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsPingRequest() throws Exception {
|
||||
Pinger pinger = new Pinger("127.0.0.1", 1);
|
||||
assertThat(pinger.isPingRequest("ping")).isTrue();
|
||||
assertThat(pinger.isPingRequest("notPing")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResponseToPing() throws Exception {
|
||||
Pinger pinger = new Pinger("127.0.0.1", 1);
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
Socket socket = mock(Socket.class);
|
||||
when(socket.getOutputStream()).thenReturn(out);
|
||||
pinger.responseToPing(socket);
|
||||
assertThat(out.toString()).isEqualTo("HTTP/1.1 200 OK\n\nping ok");
|
||||
}
|
||||
|
||||
@Test // https://github.com/danikula/AndroidVideoCache/issues/28
|
||||
public void testPingedWithExternalProxy() throws Exception {
|
||||
installExternalSystemProxy();
|
||||
|
||||
HttpProxyCacheServer server = new HttpProxyCacheServer(RuntimeEnvironment.application);
|
||||
Pinger pinger = new Pinger("127.0.0.1", getPortWithoutPing(server));
|
||||
assertThat(pinger.ping(1, 100)).isTrue();
|
||||
}
|
||||
|
||||
@Test // https://github.com/danikula/AndroidVideoCache/issues/28
|
||||
public void testIsNotPingedWithoutCustomProxySelector() throws Exception {
|
||||
HttpProxyCacheServer server = new HttpProxyCacheServer(RuntimeEnvironment.application);
|
||||
// IgnoreHostProxySelector is set in HttpProxyCacheServer constructor. So let reset it by custom.
|
||||
installExternalSystemProxy();
|
||||
|
||||
Pinger pinger = new Pinger("127.0.0.1", getPortWithoutPing(server));
|
||||
assertThat(pinger.ping(1, 100)).isFalse();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.danikula.videocache;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Proxy;
|
||||
import java.net.ProxySelector;
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
|
||||
import static com.danikula.videocache.support.ProxyCacheTestUtils.resetSystemProxy;
|
||||
import static org.fest.assertions.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Tests {@link IgnoreHostProxySelector}.
|
||||
*
|
||||
* @author Alexey Danilov (danikula@gmail.com).
|
||||
*/
|
||||
public class ProxySelectorTest extends BaseTest {
|
||||
|
||||
@Before
|
||||
public void setup() throws Exception {
|
||||
resetSystemProxy();
|
||||
}
|
||||
|
||||
@Test // https://github.com/danikula/AndroidVideoCache/issues/28
|
||||
public void testIgnoring() throws Exception {
|
||||
InetSocketAddress proxyAddress = new InetSocketAddress("proxy.com", 80);
|
||||
Proxy systemProxy = new Proxy(Proxy.Type.HTTP, proxyAddress);
|
||||
ProxySelector mockedProxySelector = Mockito.mock(ProxySelector.class);
|
||||
when(mockedProxySelector.select(Mockito.<URI>any())).thenReturn(Lists.newArrayList(systemProxy));
|
||||
ProxySelector.setDefault(mockedProxySelector);
|
||||
|
||||
IgnoreHostProxySelector.install("localhost", 42);
|
||||
|
||||
ProxySelector proxySelector = ProxySelector.getDefault();
|
||||
List<Proxy> githubProxies = proxySelector.select(new URI("http://github.com"));
|
||||
assertThat(githubProxies).hasSize(1);
|
||||
assertThat(githubProxies.get(0).address()).isEqualTo(proxyAddress);
|
||||
|
||||
List<Proxy> localhostProxies = proxySelector.select(new URI("http://localhost:42"));
|
||||
assertThat(localhostProxies).hasSize(1);
|
||||
assertThat(localhostProxies.get(0)).isEqualTo(Proxy.NO_PROXY);
|
||||
|
||||
List<Proxy> localhostPort69Proxies = proxySelector.select(new URI("http://localhost:69"));
|
||||
assertThat(localhostPort69Proxies).hasSize(1);
|
||||
assertThat(localhostPort69Proxies.get(0).address()).isEqualTo(proxyAddress);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
package com.danikula.videocache.file;
|
||||
|
||||
import com.danikula.android.garden.io.Files;
|
||||
import com.danikula.android.garden.io.IoUtils;
|
||||
import com.danikula.videocache.BaseTest;
|
||||
import com.danikula.videocache.Cache;
|
||||
import com.danikula.videocache.ProxyCacheException;
|
||||
import com.danikula.videocache.test.BuildConfig;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RobolectricGradleTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import static com.danikula.videocache.support.ProxyCacheTestUtils.ASSETS_DATA_NAME;
|
||||
@@ -22,14 +19,13 @@ import static com.danikula.videocache.support.ProxyCacheTestUtils.getFileContent
|
||||
import static com.danikula.videocache.support.ProxyCacheTestUtils.getTempFile;
|
||||
import static com.danikula.videocache.support.ProxyCacheTestUtils.loadAssetFile;
|
||||
import static com.danikula.videocache.support.ProxyCacheTestUtils.newCacheFile;
|
||||
import static com.google.common.io.Files.write;
|
||||
import static org.fest.assertions.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Alexey Danilov (danikula@gmail.com).
|
||||
*/
|
||||
@RunWith(RobolectricGradleTestRunner.class)
|
||||
@Config(constants = BuildConfig.class, emulateSdk = BuildConfig.MIN_SDK_VERSION)
|
||||
public class FileCacheTest {
|
||||
public class FileCacheTest extends BaseTest {
|
||||
|
||||
@Test
|
||||
public void testWriteReadDiscCache() throws Exception {
|
||||
@@ -90,7 +86,7 @@ public class FileCacheTest {
|
||||
fileCache.read(readData, firstPortionLength, secondPortionLength);
|
||||
assertThat(readData).isEqualTo(wroteSecondPortion);
|
||||
|
||||
readData = new byte[fileCache.available()];
|
||||
readData = new byte[(int)fileCache.available()];
|
||||
fileCache.read(readData, 0, readData.length);
|
||||
byte[] fileContent = getFileContent(getTempFile(file));
|
||||
assertThat(readData).isEqualTo(fileContent);
|
||||
@@ -100,7 +96,8 @@ public class FileCacheTest {
|
||||
public void testIsFileCacheCompleted() throws Exception {
|
||||
File file = newCacheFile();
|
||||
File partialFile = new File(file.getParentFile(), file.getName() + ".download");
|
||||
IoUtils.saveToFile(loadAssetFile(ASSETS_DATA_NAME), partialFile);
|
||||
write(loadAssetFile(ASSETS_DATA_NAME), partialFile);
|
||||
write(loadAssetFile(ASSETS_DATA_NAME), partialFile);
|
||||
Cache fileCache = new FileCache(partialFile);
|
||||
|
||||
assertThat(file.exists()).isFalse();
|
||||
@@ -119,7 +116,7 @@ public class FileCacheTest {
|
||||
@Test(expected = ProxyCacheException.class)
|
||||
public void testErrorWritingCompletedCache() throws Exception {
|
||||
File file = newCacheFile();
|
||||
IoUtils.saveToFile(loadAssetFile(ASSETS_DATA_NAME), file);
|
||||
write(loadAssetFile(ASSETS_DATA_NAME), file);
|
||||
FileCache fileCache = new FileCache(file);
|
||||
fileCache.append(generate(100), 20);
|
||||
Assert.fail();
|
||||
@@ -129,7 +126,7 @@ public class FileCacheTest {
|
||||
public void testErrorWritingAfterCompletion() throws Exception {
|
||||
File file = newCacheFile();
|
||||
File partialFile = new File(file.getParentFile(), file.getName() + ".download");
|
||||
IoUtils.saveToFile(loadAssetFile(ASSETS_DATA_NAME), partialFile);
|
||||
write(loadAssetFile(ASSETS_DATA_NAME), partialFile);
|
||||
FileCache fileCache = new FileCache(partialFile);
|
||||
fileCache.complete();
|
||||
fileCache.append(generate(100), 20);
|
||||
@@ -145,4 +142,48 @@ public class FileCacheTest {
|
||||
fileCache.available();
|
||||
Assert.fail();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTrimAfterCompletionForTotalCountLru() throws Exception {
|
||||
File cacheDir = newCacheFile();
|
||||
DiskUsage diskUsage = new TotalCountLruDiskUsage(2);
|
||||
byte[] data = loadAssetFile(ASSETS_DATA_NAME);
|
||||
saveAndCompleteCache(diskUsage, data,
|
||||
new File(cacheDir, "0.dat"),
|
||||
new File(cacheDir, "1.dat"),
|
||||
new File(cacheDir, "2.dat")
|
||||
);
|
||||
waitForAsyncTrimming();
|
||||
assertThat(new File(cacheDir, "0.dat")).doesNotExist();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTrimAfterCompletionForTotalSizeLru() throws Exception {
|
||||
File cacheDir = newCacheFile();
|
||||
byte[] data = loadAssetFile(ASSETS_DATA_NAME);
|
||||
DiskUsage diskUsage = new TotalSizeLruDiskUsage(data.length * 3 - 1);
|
||||
saveAndCompleteCache(diskUsage, data,
|
||||
new File(cacheDir, "0.dat"),
|
||||
new File(cacheDir, "1.dat"),
|
||||
new File(cacheDir, "2.dat")
|
||||
);
|
||||
waitForAsyncTrimming();
|
||||
File deletedFile = new File(cacheDir, "0.dat");
|
||||
assertThat(deletedFile).doesNotExist();
|
||||
}
|
||||
|
||||
private void saveAndCompleteCache(DiskUsage diskUsage, byte[] data, File... files) throws ProxyCacheException, IOException, InterruptedException {
|
||||
for (File file : files) {
|
||||
FileCache fileCache = new FileCache(file, diskUsage);
|
||||
fileCache.append(data, data.length);
|
||||
fileCache.complete();
|
||||
assertThat(file).exists();
|
||||
fileCache.close();
|
||||
Thread.sleep(1000); // last modified date wrote in seconds.
|
||||
}
|
||||
}
|
||||
|
||||
private void waitForAsyncTrimming() throws InterruptedException {
|
||||
Thread.sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,19 @@
|
||||
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.collect.Lists;
|
||||
import com.google.common.io.Files;
|
||||
|
||||
import org.apache.tools.ant.util.ReflectUtil;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.mockito.stubbing.Answer;
|
||||
import org.robolectric.RuntimeEnvironment;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
@@ -11,21 +21,36 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Proxy;
|
||||
import java.net.ProxySelector;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.fest.assertions.api.Assertions.assertThat;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.anyInt;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* @author Alexey Danilov (danikula@gmail.com).
|
||||
*/
|
||||
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 +65,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 +108,69 @@ 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);
|
||||
}
|
||||
|
||||
public static int getPortWithoutPing(HttpProxyCacheServer server) {
|
||||
return (Integer) ReflectUtil.getField(server, "port");
|
||||
}
|
||||
|
||||
public static void installExternalSystemProxy() {
|
||||
// see proxies list at http://proxylist.hidemyass.com/
|
||||
Proxy systemProxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("162.8.230.7", 11180));
|
||||
ProxySelector mockedProxySelector = Mockito.mock(ProxySelector.class);
|
||||
when(mockedProxySelector.select(Mockito.<URI>any())).thenReturn(Lists.newArrayList(systemProxy));
|
||||
ProxySelector.setDefault(mockedProxySelector);
|
||||
}
|
||||
|
||||
public static void resetSystemProxy() {
|
||||
ProxySelector mockedProxySelector = Mockito.mock(ProxySelector.class);
|
||||
when(mockedProxySelector.select(Mockito.<URI>any())).thenReturn(Lists.newArrayList(Proxy.NO_PROXY));
|
||||
ProxySelector.setDefault(mockedProxySelector);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user