diff --git a/README.md b/README.md index f080651..4a652c2 100644 --- a/README.md +++ b/README.md @@ -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.0' } ``` diff --git a/library/build.gradle b/library/build.gradle index ba7f651..ecea303 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -26,7 +26,7 @@ publish { userOrg = 'alexeydanilov' groupId = 'com.danikula' artifactId = 'videocache' - publishVersion = '2.0.9' + publishVersion = '2.1.0' description = 'Cache support for android VideoView' website = 'https://github.com/danikula/AndroidVideoCache' } diff --git a/library/src/main/java/com/danikula/videocache/HttpUrlSource.java b/library/src/main/java/com/danikula/videocache/HttpUrlSource.java index 55a2644..601a35d 100644 --- a/library/src/main/java/com/danikula/videocache/HttpUrlSource.java +++ b/library/src/main/java/com/danikula/videocache/HttpUrlSource.java @@ -2,6 +2,7 @@ package com.danikula.videocache; import android.text.TextUtils; import android.util.Log; + import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; @@ -11,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}. @@ -21,7 +25,8 @@ import static java.net.HttpURLConnection.HTTP_PARTIAL; */ public class HttpUrlSource implements Source { - public String url; + private static final int MAX_REDIRECTS = 5; + public final String url; private HttpURLConnection connection; private InputStream inputStream; private volatile int available = Integer.MIN_VALUE; @@ -47,33 +52,10 @@ public class HttpUrlSource implements Source { @Override public void open(int offset) throws ProxyCacheException { try { - boolean isRedirected; - int redirectCount = 0; - int responseCode; - do { - Log.d(ProxyCacheUtils.LOG_TAG, "Open connection " + (offset > 0 ? " with offset " + offset : "") + " to " + url); - connection = (HttpURLConnection) new URL(url).openConnection(); - if (offset > 0) { - connection.setRequestProperty("Range", "bytes=" + offset + "-"); - } - responseCode = connection.getResponseCode(); - if ((responseCode == HttpURLConnection.HTTP_MOVED_PERM - || responseCode == HttpURLConnection.HTTP_MOVED_TEMP - || responseCode == HttpURLConnection.HTTP_SEE_OTHER)) { - url = connection.getHeaderField("Location"); - isRedirected = true; - redirectCount++; - } else { - isRedirected = false; - } - if (redirectCount > ProxyCacheUtils.MAX_REDIRECTS) { - throw new ProxyCacheException("Too many redirects"); - } - } while (isRedirected); - + connection = openConnection(offset, "GET", -1); mime = connection.getContentType(); inputStream = new BufferedInputStream(connection.getInputStream(), DEFAULT_BUFFER_SIZE); - available = readSourceAvailableBytes(connection, offset, responseCode); + available = readSourceAvailableBytes(connection, offset, connection.getResponseCode()); } catch (IOException e) { throw new ProxyCacheException("Error opening connection for " + url + " with offset " + offset, e); } @@ -111,28 +93,7 @@ public class HttpUrlSource implements Source { HttpURLConnection urlConnection = null; InputStream inputStream = null; try { - boolean isRedirected; - int redirectCount = 0; - do { - urlConnection = (HttpURLConnection) new URL(url).openConnection(); - urlConnection.setConnectTimeout(10000); - urlConnection.setReadTimeout(10000); - urlConnection.setRequestMethod("HEAD"); - int responseCode = urlConnection.getResponseCode(); - if ((responseCode == HttpURLConnection.HTTP_MOVED_PERM - || responseCode == HttpURLConnection.HTTP_MOVED_TEMP - || responseCode == HttpURLConnection.HTTP_SEE_OTHER)) { - url = urlConnection.getHeaderField("Location"); - isRedirected = true; - redirectCount++; - } else { - isRedirected = false; - } - if (redirectCount > ProxyCacheUtils.MAX_REDIRECTS) { - throw new ProxyCacheException("Too many redirects"); - } - } while (isRedirected); - + urlConnection = openConnection(0, "HEAD", 10000); available = urlConnection.getContentLength(); mime = urlConnection.getContentType(); inputStream = urlConnection.getInputStream(); @@ -147,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(); @@ -154,6 +145,10 @@ public class HttpUrlSource implements Source { return mime; } + public String getUrl() { + return url; + } + @Override public String toString() { return "HttpUrlSource{url='" + url + "}"; diff --git a/library/src/main/java/com/danikula/videocache/ProxyCacheUtils.java b/library/src/main/java/com/danikula/videocache/ProxyCacheUtils.java index 23a9c05..be57331 100644 --- a/library/src/main/java/com/danikula/videocache/ProxyCacheUtils.java +++ b/library/src/main/java/com/danikula/videocache/ProxyCacheUtils.java @@ -25,7 +25,6 @@ class ProxyCacheUtils { static final String LOG_TAG = "ProxyCache"; static final int DEFAULT_BUFFER_SIZE = 8 * 1024; static final int MAX_ARRAY_PREVIEW = 16; - static final int MAX_REDIRECTS = 5; static String getSupposablyMime(String url) { MimeTypeMap mimes = MimeTypeMap.getSingleton(); diff --git a/sample/build.gradle b/sample/build.gradle index 8e04185..763c1d0 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -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.0' compile 'com.viewpagerindicator:library:2.4.2-SNAPSHOT@aar' apt 'org.androidannotations:androidannotations:3.3.2' } diff --git a/test/src/test/java/com/danikula/videocache/HttpProxyCacheServerTest.java b/test/src/test/java/com/danikula/videocache/HttpProxyCacheServerTest.java index a27afd6..f58a616 100644 --- a/test/src/test/java/com/danikula/videocache/HttpProxyCacheServerTest.java +++ b/test/src/test/java/com/danikula/videocache/HttpProxyCacheServerTest.java @@ -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 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 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 readProxyData(String url, int offset) throws IOException { File externalCacheDir = RuntimeEnvironment.application.getExternalCacheDir(); FileNameGenerator fileNameGenerator = new Md5FileNameGenerator(externalCacheDir); diff --git a/test/src/test/java/com/danikula/videocache/HttpUrlSourceTest.java b/test/src/test/java/com/danikula/videocache/HttpUrlSourceTest.java index f40a09e..e4e2ea7 100644 --- a/test/src/test/java/com/danikula/videocache/HttpUrlSourceTest.java +++ b/test/src/test/java/com/danikula/videocache/HttpUrlSourceTest.java @@ -1,21 +1,28 @@ package com.danikula.videocache; import com.danikula.videocache.test.BuildConfig; -import java.io.ByteArrayOutputStream; -import java.util.Arrays; + 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.ByteArrayOutputStream; +import java.util.Arrays; + import static com.danikula.videocache.support.ProxyCacheTestUtils.ASSETS_DATA_BIG_NAME; import static com.danikula.videocache.support.ProxyCacheTestUtils.ASSETS_DATA_NAME; import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_BIG_SIZE; import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_BIG_URL; +import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_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). @@ -59,15 +66,68 @@ 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 { assertThat(new HttpUrlSource("http://mysite.by/video.mp4").getMime()).isEqualTo("video/mp4"); assertThat(new HttpUrlSource(HTTP_DATA_URL).getMime()).isEqualTo("image/jpeg"); } - - @Test - public void testHttpUrlSourceRedirect() throws Exception { - assertThat(new HttpUrlSource("http://goo.gl/K0gWQW").getMime()).isEqualTo("video/mp4"); - } } diff --git a/test/src/test/java/com/danikula/videocache/support/ProxyCacheTestUtils.java b/test/src/test/java/com/danikula/videocache/support/ProxyCacheTestUtils.java index 0eac519..8acbee5 100644 --- a/test/src/test/java/com/danikula/videocache/support/ProxyCacheTestUtils.java +++ b/test/src/test/java/com/danikula/videocache/support/ProxyCacheTestUtils.java @@ -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;