7 Commits

Author SHA1 Message Date
Alexey Danilov
63ee20f93c fix offline work 2015-09-25 17:07:46 +03:00
Alexey Danilov
88da0aa5c0 fix markdown 2015-09-21 23:50:06 +03:00
Alexey Danilov
3d50eb64c3 update 'whats new' section in readme 2015-09-21 23:46:17 +03:00
Alexey Danilov
f4b9e5c8f5 fix too long file name for cache. Md5FileNameGenerator tests 2015-09-21 23:38:57 +03:00
Alexey Danilov
15c5388f6c tests for url redirections 2015-09-21 21:24:37 +03:00
Relex
8263814aea unit test for url redirect 2015-09-14 11:29:16 +08:00
Relex
1c7cb32a97 Support redirection 2015-09-12 15:50:56 +08:00
10 changed files with 266 additions and 45 deletions

View File

@@ -12,7 +12,7 @@ repositories {
maven { url 'https://dl.bintray.com/alexeydanilov/maven' }
}
dependencies {
compile 'com.danikula:videocache:2.0.9'
compile 'com.danikula:videocache:2.1.2'
}
```
@@ -58,7 +58,14 @@ More preferable way is use some dependency injector like [Dagger](http://square.
See `sample` app for details.
## Whats new in 2.0?
## Whats new
### 2.1.2
- fix offline work
### 2.1.1
- fix for too long cache file name
- url redirects support (thanks [ongakuer](https://github.com/ongakuer) for [PR](https://github.com/danikula/AndroidVideoCache/pull/12))
### 2.0
- simpler api
- single cache for multiple clients
- cache file name policy

View File

@@ -26,7 +26,7 @@ publish {
userOrg = 'alexeydanilov'
groupId = 'com.danikula'
artifactId = 'videocache'
publishVersion = '2.0.9'
publishVersion = '2.1.2'
description = 'Cache support for android VideoView'
website = 'https://github.com/danikula/AndroidVideoCache'
}

View File

@@ -12,8 +12,11 @@ import java.net.URL;
import static com.danikula.videocache.ProxyCacheUtils.DEFAULT_BUFFER_SIZE;
import static com.danikula.videocache.ProxyCacheUtils.LOG_TAG;
import static java.net.HttpURLConnection.HTTP_MOVED_PERM;
import static java.net.HttpURLConnection.HTTP_MOVED_TEMP;
import static java.net.HttpURLConnection.HTTP_OK;
import static java.net.HttpURLConnection.HTTP_PARTIAL;
import static java.net.HttpURLConnection.HTTP_SEE_OTHER;
/**
* {@link Source} that uses http resource as source for {@link ProxyCache}.
@@ -22,6 +25,7 @@ import static java.net.HttpURLConnection.HTTP_PARTIAL;
*/
public class HttpUrlSource implements Source {
private static final int MAX_REDIRECTS = 5;
public final String url;
private HttpURLConnection connection;
private InputStream inputStream;
@@ -48,25 +52,19 @@ public class HttpUrlSource implements Source {
@Override
public void open(int offset) throws ProxyCacheException {
try {
Log.d(LOG_TAG, "Open connection " + (offset > 0 ? " with offset " + offset : "") + " to " + url);
connection = (HttpURLConnection) new URL(url).openConnection();
if (offset > 0) {
connection.setRequestProperty("Range", "bytes=" + offset + "-");
}
connection = openConnection(offset, "GET", -1);
mime = connection.getContentType();
inputStream = new BufferedInputStream(connection.getInputStream(), DEFAULT_BUFFER_SIZE);
available = readSourceAvailableBytes(connection, offset);
available = readSourceAvailableBytes(connection, offset, connection.getResponseCode());
} catch (IOException e) {
throw new ProxyCacheException("Error opening connection for " + url + " with offset " + offset, e);
}
}
private int readSourceAvailableBytes(HttpURLConnection connection, int offset) throws IOException {
private int readSourceAvailableBytes(HttpURLConnection connection, int offset, int responseCode) throws IOException {
int contentLength = connection.getContentLength();
int responseCode = connection.getResponseCode();
return responseCode == HTTP_OK ? contentLength :
responseCode == HTTP_PARTIAL ? contentLength + offset :
available;
return responseCode == HTTP_OK ? contentLength
: responseCode == HTTP_PARTIAL ? contentLength + offset : available;
}
@Override
@@ -95,16 +93,13 @@ public class HttpUrlSource implements Source {
HttpURLConnection urlConnection = null;
InputStream inputStream = null;
try {
urlConnection = (HttpURLConnection) new URL(url).openConnection();
urlConnection.setConnectTimeout(10000);
urlConnection.setReadTimeout(10000);
urlConnection.setRequestMethod("HEAD");
urlConnection = openConnection(0, "HEAD", 10000);
available = urlConnection.getContentLength();
mime = urlConnection.getContentType();
inputStream = urlConnection.getInputStream();
Log.i(LOG_TAG, "Content info for `" + url + "`: mime: " + mime + ", content-length: " + available);
} catch (IOException e) {
throw new ProxyCacheException("Error fetching Content-Length from " + url);
Log.e(LOG_TAG, "Error fetching info from " + url, e);
} finally {
ProxyCacheUtils.close(inputStream);
if (urlConnection != null) {
@@ -113,6 +108,36 @@ public class HttpUrlSource implements Source {
}
}
private HttpURLConnection openConnection(int offset, String method, int timeout) throws IOException, ProxyCacheException {
HttpURLConnection connection;
boolean redirected;
int redirectCount = 0;
String url = this.url;
do {
Log.d(LOG_TAG, "Open connection " + (offset > 0 ? " with offset " + offset : "") + " to " + url);
connection = (HttpURLConnection) new URL(url).openConnection();
connection.setRequestMethod(method);
if (offset > 0) {
connection.setRequestProperty("Range", "bytes=" + offset + "-");
}
if (timeout > 0) {
connection.setConnectTimeout(timeout);
connection.setReadTimeout(timeout);
}
int code = connection.getResponseCode();
redirected = code == HTTP_MOVED_PERM || code == HTTP_MOVED_TEMP || code == HTTP_SEE_OTHER;
if (redirected) {
url = connection.getHeaderField("Location");
redirectCount++;
connection.disconnect();
}
if (redirectCount > MAX_REDIRECTS) {
throw new ProxyCacheException("Too many redirects: " + redirectCount);
}
} while (redirected);
return connection;
}
public synchronized String getMime() throws ProxyCacheException {
if (TextUtils.isEmpty(mime)) {
fetchContentInfo();
@@ -120,6 +145,10 @@ public class HttpUrlSource implements Source {
return mime;
}
public String getUrl() {
return url;
}
@Override
public String toString() {
return "HttpUrlSource{url='" + url + "}";

View File

@@ -3,8 +3,6 @@ package com.danikula.videocache;
import android.text.TextUtils;
import java.io.File;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import static com.danikula.videocache.Preconditions.checkNotNull;
@@ -15,6 +13,7 @@ import static com.danikula.videocache.Preconditions.checkNotNull;
*/
public class Md5FileNameGenerator implements FileNameGenerator {
private static final int MAX_EXTENSION_LENGTH = 4;
private final File cacheDirectory;
public Md5FileNameGenerator(File cacheDirectory) {
@@ -25,32 +24,15 @@ public class Md5FileNameGenerator implements FileNameGenerator {
public File generate(String url) {
checkNotNull(url);
String extension = getExtension(url);
String name = computeMD5(url);
String name = ProxyCacheUtils.computeMD5(url);
name = TextUtils.isEmpty(extension) ? name : name + "." + extension;
return new File(cacheDirectory, name);
}
private String getExtension(String url) {
int dotIndex = url.lastIndexOf('.');
int slashIndex = url.lastIndexOf(File.separator);
return dotIndex != -1 && dotIndex > slashIndex ? url.substring(dotIndex + 1, url.length()) : "";
}
private String computeMD5(String string) {
try {
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
byte[] digestBytes = messageDigest.digest(string.getBytes());
return bytesToHexString(digestBytes);
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
}
}
private String bytesToHexString(byte[] bytes) {
StringBuffer sb = new StringBuffer();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
int slashIndex = url.lastIndexOf('/');
return dotIndex != -1 && dotIndex > slashIndex && dotIndex + 2 + MAX_EXTENSION_LENGTH > url.length() ?
url.substring(dotIndex + 1, url.length()) : "";
}
}

View File

@@ -10,6 +10,8 @@ import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import static com.danikula.videocache.Preconditions.checkArgument;
@@ -86,4 +88,23 @@ class ProxyCacheUtils {
}
}
}
static String computeMD5(String string) {
try {
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
byte[] digestBytes = messageDigest.digest(string.getBytes());
return bytesToHexString(digestBytes);
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
}
}
static String bytesToHexString(byte[] bytes) {
StringBuffer sb = new StringBuffer();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
}

View File

@@ -17,7 +17,7 @@ apply plugin: 'com.neenbedankt.android-apt'
android {
compileSdkVersion 23
buildToolsVersion '23.0.0'
buildToolsVersion '23.0.1'
defaultConfig {
applicationId "com.danikula.videocache.sample"
@@ -37,9 +37,9 @@ apt {
dependencies {
// compile project(':library')
compile 'com.android.support:support-v4:23.0.0'
compile 'com.android.support:support-v4:23.0.1'
compile 'org.androidannotations:androidannotations-api:3.3.2'
compile 'com.danikula:videocache:2.0.9'
compile 'com.danikula:videocache:2.1.2'
compile 'com.viewpagerindicator:library:2.4.2-SNAPSHOT@aar'
apt 'org.androidannotations:androidannotations:3.3.2'
}

View File

@@ -0,0 +1,82 @@
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.File;
import static org.fest.assertions.api.Assertions.assertThat;
import static org.fest.assertions.api.Assertions.fail;
/**
* Tests for {@link FileNameGenerator} and implementations.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, emulateSdk = BuildConfig.MIN_SDK_VERSION)
public class FileNameGeneratorTest {
@Test
public void testMd5SimpleName() throws Exception {
String url = "http://host.com/videos/video.mpeg";
String path = generateMd5Name("/home", url);
String expected = "/home/" + ProxyCacheUtils.computeMD5(url) + ".mpeg";
assertThat(path).isEqualTo(expected);
}
@Test
public void testMd5NoExtension() throws Exception {
String url = "http://host.com/video";
String path = generateMd5Name("/home", url);
String expected = "/home/" + ProxyCacheUtils.computeMD5(url);
assertThat(path).isEqualTo(expected);
}
@Test
public void testMd5TooLongExtension() throws Exception {
String url = "http://host.com/videos/video-with-dot-.12345";
String path = generateMd5Name("/home", url);
String expected = "/home/" + ProxyCacheUtils.computeMD5(url);
assertThat(path).isEqualTo(expected);
}
@Test
public void testMd5InvalidExtension() throws Exception {
String url = "http://host.com/videos/video.mp4?token=-648729473536183645";
String path = generateMd5Name("/home", url);
String expected = "/home/" + ProxyCacheUtils.computeMD5(url);
assertThat(path).isEqualTo(expected);
}
@Test
public void testMd5ExtraLongExtension() throws Exception {
// https://github.com/danikula/AndroidVideoCache/issues/14
String url = "https://d1wst0behutosd.cloudfront.net/videos/4367900/10807247.480p.mp4?Expires=1442849176&Signature=JXV~3AoI0rWcGuZBywg3-ukf6Ycw2X8v7Htog3lyvuFwp8o6VUEDFUsTC9-XtIGu-ULxCd7dP3fvB306lRyGFxdvf-sXLX~ar~HCQ7lullNyeLtp8BJOT5Y~W5rJE7X-AZaueNcycGtLFRhRtr5ySTguwtmJNaO3T1apX~-oVrFh1dWStEKbuPoXY04RgkmhMHoFgtwgXMC1ctIDeQHxZeXLi6LLyZnQsgzlUDffCx4P16iiW0uh2-Z~HUOi9BLBwHMQ5k5lYwZqdQ6DhhYoWlniRfQz6mp1IEiMgr4L3Z1ijgGITV4cYeF31CmFzCxaJTE7IIAC5tMDQSTt7M9Q4A__&Key-Pair-Id=APKAJJ6WELAPEP47UKWQ";
String path = generateMd5Name("/home", url);
String expected = "/home/" + ProxyCacheUtils.computeMD5(url);
assertThat(path).isEqualTo(expected);
}
@Test(expected = NullPointerException.class)
public void testAssertNullRoot() throws Exception {
new Md5FileNameGenerator(null);
fail("Root folder should be not null");
}
@Test(expected = NullPointerException.class)
public void testAssertNullUrl() throws Exception {
FileNameGenerator nameGenerator = new Md5FileNameGenerator(new File("/"));
nameGenerator.generate(null);
fail("Url should be not null");
}
private String generateMd5Name(String rootFolder, String url) {
FileNameGenerator nameGenerator = new Md5FileNameGenerator(new File(rootFolder));
return nameGenerator.generate(url).getAbsolutePath();
}
}

View File

@@ -21,6 +21,7 @@ import static com.danikula.videocache.support.ProxyCacheTestUtils.ASSETS_DATA_BI
import static com.danikula.videocache.support.ProxyCacheTestUtils.ASSETS_DATA_NAME;
import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_BIG_SIZE;
import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_BIG_URL;
import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_BIG_URL_ONE_REDIRECT;
import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_URL;
import static com.danikula.videocache.support.ProxyCacheTestUtils.getFileContent;
import static com.danikula.videocache.support.ProxyCacheTestUtils.loadAssetFile;
@@ -81,6 +82,20 @@ public class HttpProxyCacheServerTest {
assertThat(response.second.data).isEqualTo(loadAssetFile(ASSETS_DATA_BIG_NAME));
}
@Test
public void testProxyFullResponseWithRedirect() throws Exception {
Pair<File, Response> response = readProxyData(HTTP_DATA_BIG_URL_ONE_REDIRECT);
assertThat(response.second.code).isEqualTo(200);
assertThat(response.second.contentLength).isEqualTo(HTTP_DATA_BIG_SIZE);
assertThat(response.second.contentType).isEqualTo("image/jpeg");
assertThat(response.second.headers.containsKey("Accept-Ranges")).isTrue();
assertThat(response.second.headers.get("Accept-Ranges").get(0)).isEqualTo("bytes");
assertThat(response.second.headers.containsKey("Content-Range")).isFalse();
assertThat(response.second.data).isEqualTo(getFileContent(response.first));
assertThat(response.second.data).isEqualTo(loadAssetFile(ASSETS_DATA_BIG_NAME));
}
@Test
public void testProxyPartialResponse() throws Exception {
int offset = 42000;
@@ -99,6 +114,24 @@ public class HttpProxyCacheServerTest {
assertThat(getFileContent(response.first)).isEqualTo(loadAssetFile(ASSETS_DATA_BIG_NAME));
}
@Test
public void testProxyPartialResponseWithRedirect() throws Exception {
int offset = 42000;
Pair<File, Response> response = readProxyData(HTTP_DATA_BIG_URL_ONE_REDIRECT, offset);
assertThat(response.second.code).isEqualTo(206);
assertThat(response.second.contentLength).isEqualTo(HTTP_DATA_BIG_SIZE - offset);
assertThat(response.second.contentType).isEqualTo("image/jpeg");
assertThat(response.second.headers.containsKey("Accept-Ranges")).isTrue();
assertThat(response.second.headers.get("Accept-Ranges").get(0)).isEqualTo("bytes");
assertThat(response.second.headers.containsKey("Content-Range")).isTrue();
String rangeHeader = String.format("bytes %d-%d/%d", offset, HTTP_DATA_BIG_SIZE, HTTP_DATA_BIG_SIZE);
assertThat(response.second.headers.get("Content-Range").get(0)).isEqualTo(rangeHeader);
byte[] expectedData = Arrays.copyOfRange(loadAssetFile(ASSETS_DATA_BIG_NAME), offset, HTTP_DATA_BIG_SIZE);
assertThat(response.second.data).isEqualTo(expectedData);
assertThat(getFileContent(response.first)).isEqualTo(loadAssetFile(ASSETS_DATA_BIG_NAME));
}
private Pair<File, Response> readProxyData(String url, int offset) throws IOException {
File externalCacheDir = RuntimeEnvironment.application.getExternalCacheDir();
FileNameGenerator fileNameGenerator = new Md5FileNameGenerator(externalCacheDir);

View File

@@ -15,9 +15,14 @@ import static com.danikula.videocache.support.ProxyCacheTestUtils.ASSETS_DATA_BI
import static com.danikula.videocache.support.ProxyCacheTestUtils.ASSETS_DATA_NAME;
import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_BIG_SIZE;
import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_BIG_URL;
import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_SIZE;
import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_URL;
import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_URL_3_REDIRECTS;
import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_URL_6_REDIRECTS;
import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_URL_ONE_REDIRECT;
import static com.danikula.videocache.support.ProxyCacheTestUtils.loadAssetFile;
import static org.fest.assertions.api.Assertions.assertThat;
import static org.fest.assertions.api.Assertions.fail;
/**
* @author Alexey Danilov (danikula@gmail.com).
@@ -61,6 +66,64 @@ public class HttpUrlSourceTest {
assertThat(source.available()).isEqualTo(loadAssetFile(ASSETS_DATA_NAME).length);
}
@Test
public void testFetchInfoWithRedirect() throws Exception {
HttpUrlSource source = new HttpUrlSource(HTTP_DATA_URL_ONE_REDIRECT);
source.open(0);
int available = source.available();
String mime = source.getMime();
source.close();
assertThat(available).isEqualTo(HTTP_DATA_SIZE);
assertThat(mime).isEqualTo("image/jpeg");
}
@Test
public void testFetchDataWithRedirect() throws Exception {
HttpUrlSource source = new HttpUrlSource(HTTP_DATA_URL_ONE_REDIRECT);
source.open(0);
byte[] readData = new byte[HTTP_DATA_SIZE];
source.read(readData);
source.close();
byte[] expectedData = Arrays.copyOfRange(loadAssetFile(ASSETS_DATA_NAME), 0, HTTP_DATA_SIZE);
assertThat(readData).isEqualTo(expectedData);
}
@Test
public void testFetchPartialDataWithRedirect() throws Exception {
int offset = 42;
HttpUrlSource source = new HttpUrlSource(HTTP_DATA_URL_ONE_REDIRECT);
source.open(offset);
byte[] readData = new byte[HTTP_DATA_SIZE - offset];
source.read(readData);
source.close();
byte[] expectedData = Arrays.copyOfRange(loadAssetFile(ASSETS_DATA_NAME), offset, HTTP_DATA_SIZE);
assertThat(readData).isEqualTo(expectedData);
}
@Test
public void testFetchPartialDataWithMultiRedirects() throws Exception {
int offset = 42;
HttpUrlSource source = new HttpUrlSource(HTTP_DATA_URL_3_REDIRECTS);
source.open(offset);
byte[] readData = new byte[HTTP_DATA_SIZE - offset];
source.read(readData);
source.close();
byte[] expectedData = Arrays.copyOfRange(loadAssetFile(ASSETS_DATA_NAME), offset, HTTP_DATA_SIZE);
assertThat(readData).isEqualTo(expectedData);
}
@Ignore("To test it fairly we should disable caching connection.setUseCaches(false), but it will decrease performance")
@Test(expected = ProxyCacheException.class)
public void testExceedingRedirects() throws Exception {
HttpUrlSource source = new HttpUrlSource(HTTP_DATA_URL_6_REDIRECTS);
source.open(0);
fail("Too many redirects");
}
@Ignore("Seems Robolectric bug: MimeTypeMap.getFileExtensionFromUrl always returns null")
@Test
public void testMimeByUrl() throws Exception {

View File

@@ -21,7 +21,11 @@ import java.util.UUID;
public class ProxyCacheTestUtils {
public static final String HTTP_DATA_URL = "https://dl.dropboxusercontent.com/u/15506779/persistent/proxycache/android.jpg";
public static final String HTTP_DATA_URL_ONE_REDIRECT = "http://bit.ly/1V5PeY5";
public static final String HTTP_DATA_URL_3_REDIRECTS = "http://bit.ly/1KvVmgZ";
public static final String HTTP_DATA_URL_6_REDIRECTS = "http://ow.ly/SugRH";
public static final String HTTP_DATA_BIG_URL = "https://dl.dropboxusercontent.com/u/15506779/persistent/proxycache/phones.jpg";
public static final String HTTP_DATA_BIG_URL_ONE_REDIRECT = "http://bit.ly/1iJ69yA";
public static final String ASSETS_DATA_NAME = "android.jpg";
public static final String ASSETS_DATA_BIG_NAME = "phones.jpg";
public static final int HTTP_DATA_SIZE = 4768;