diff options
Diffstat (limited to 'net')
13 files changed, 854 insertions, 0 deletions
diff --git a/net/common/README.md b/net/common/README.md new file mode 100644 index 000000000..3bd8b232e --- /dev/null +++ b/net/common/README.md @@ -0,0 +1,3 @@ +# :net:common + +This module contains general network related utilities. diff --git a/net/common/build.gradle b/net/common/build.gradle new file mode 100644 index 000000000..c519aa653 --- /dev/null +++ b/net/common/build.gradle @@ -0,0 +1,12 @@ +plugins { + id("com.android.library") +} +apply from: "../../common.gradle" + +dependencies { + annotationProcessor "androidx.annotation:annotation:$annotationVersion" + implementation "com.squareup.okhttp3:okhttp:$okhttpVersion" + + testImplementation "junit:junit:$junitVersion" + testImplementation "org.robolectric:robolectric:$robolectricVersion" +} diff --git a/net/common/src/main/AndroidManifest.xml b/net/common/src/main/AndroidManifest.xml new file mode 100644 index 000000000..702944daa --- /dev/null +++ b/net/common/src/main/AndroidManifest.xml @@ -0,0 +1 @@ +<manifest package="de.danoeh.antennapod.net.common" /> diff --git a/net/common/src/main/java/de/danoeh/antennapod/net/common/UrlChecker.java b/net/common/src/main/java/de/danoeh/antennapod/net/common/UrlChecker.java new file mode 100644 index 000000000..4eb1fd6a5 --- /dev/null +++ b/net/common/src/main/java/de/danoeh/antennapod/net/common/UrlChecker.java @@ -0,0 +1,139 @@ +package de.danoeh.antennapod.net.common; + +import android.net.Uri; +import android.text.TextUtils; +import androidx.annotation.NonNull; +import android.util.Log; + +import okhttp3.HttpUrl; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +/** + * Provides methods for checking and editing a URL. + */ +public final class UrlChecker { + + /** + * Class shall not be instantiated. + */ + private UrlChecker() { + } + + /** + * Logging tag. + */ + private static final String TAG = "UrlChecker"; + + private static final String AP_SUBSCRIBE = "antennapod-subscribe://"; + private static final String AP_SUBSCRIBE_DEEPLINK = "antennapod.org/deeplink/subscribe"; + + /** + * Checks if URL is valid and modifies it if necessary. + * + * @param url The url which is going to be prepared + * @return The prepared url + */ + public static String prepareUrl(@NonNull String url) { + url = url.trim(); + String lowerCaseUrl = url.toLowerCase(Locale.ROOT); // protocol names are case insensitive + if (lowerCaseUrl.startsWith("feed://")) { + Log.d(TAG, "Replacing feed:// with http://"); + return prepareUrl(url.substring("feed://".length())); + } else if (lowerCaseUrl.startsWith("pcast://")) { + Log.d(TAG, "Removing pcast://"); + return prepareUrl(url.substring("pcast://".length())); + } else if (lowerCaseUrl.startsWith("pcast:")) { + Log.d(TAG, "Removing pcast:"); + return prepareUrl(url.substring("pcast:".length())); + } else if (lowerCaseUrl.startsWith("itpc")) { + Log.d(TAG, "Replacing itpc:// with http://"); + return prepareUrl(url.substring("itpc://".length())); + } else if (lowerCaseUrl.startsWith(AP_SUBSCRIBE)) { + Log.d(TAG, "Removing antennapod-subscribe://"); + return prepareUrl(url.substring(AP_SUBSCRIBE.length())); + } else if (lowerCaseUrl.contains(AP_SUBSCRIBE_DEEPLINK)) { + Log.d(TAG, "Removing " + AP_SUBSCRIBE_DEEPLINK); + String removedWebsite = url.substring(url.indexOf("?url=") + "?url=".length()); + try { + return prepareUrl(URLDecoder.decode(removedWebsite, "UTF-8")); + } catch (UnsupportedEncodingException e) { + return prepareUrl(removedWebsite); + } + } else if (!(lowerCaseUrl.startsWith("http://") || lowerCaseUrl.startsWith("https://"))) { + Log.d(TAG, "Adding http:// at the beginning of the URL"); + return "http://" + url; + } else { + return url; + } + } + + /** + * Checks if URL is valid and modifies it if necessary. + * This method also handles protocol relative URLs. + * + * @param url The url which is going to be prepared + * @param base The url against which the (possibly relative) url is applied. If this is null, + * the result of prepareURL(url) is returned instead. + * @return The prepared url + */ + public static String prepareUrl(String url, String base) { + if (base == null) { + return prepareUrl(url); + } + url = url.trim(); + base = prepareUrl(base); + Uri urlUri = Uri.parse(url); + Uri baseUri = Uri.parse(base); + if (urlUri.isRelative() && baseUri.isAbsolute()) { + return urlUri.buildUpon().scheme(baseUri.getScheme()).build().toString(); + } else { + return prepareUrl(url); + } + } + + public static boolean containsUrl(List<String> list, String url) { + for (String item : list) { + if (urlEquals(item, url)) { + return true; + } + } + return false; + } + + public static boolean urlEquals(String string1, String string2) { + HttpUrl url1 = HttpUrl.parse(string1); + HttpUrl url2 = HttpUrl.parse(string2); + if (!url1.host().equals(url2.host())) { + return false; + } + List<String> pathSegments1 = normalizePathSegments(url1.pathSegments()); + List<String> pathSegments2 = normalizePathSegments(url2.pathSegments()); + if (!pathSegments1.equals(pathSegments2)) { + return false; + } + if (TextUtils.isEmpty(url1.query())) { + return TextUtils.isEmpty(url2.query()); + } + return url1.query().equals(url2.query()); + } + + /** + * Removes empty segments and converts all to lower case. + * @param input List of path segments + * @return Normalized list of path segments + */ + private static List<String> normalizePathSegments(List<String> input) { + List<String> result = new ArrayList<>(); + for (String string : input) { + if (!TextUtils.isEmpty(string)) { + result.add(string.toLowerCase(Locale.ROOT)); + } + } + return result; + } +} diff --git a/net/common/src/test/java/de/danoeh/antennapod/net/common/UrlCheckerTest.java b/net/common/src/test/java/de/danoeh/antennapod/net/common/UrlCheckerTest.java new file mode 100644 index 000000000..ba9f1dcbb --- /dev/null +++ b/net/common/src/test/java/de/danoeh/antennapod/net/common/UrlCheckerTest.java @@ -0,0 +1,175 @@ +package de.danoeh.antennapod.net.common; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * Test class for {@link UrlChecker} + */ +@RunWith(RobolectricTestRunner.class) +public class UrlCheckerTest { + + @Test + public void testCorrectURLHttp() { + final String in = "http://example.com"; + final String out = UrlChecker.prepareUrl(in); + assertEquals(in, out); + } + + @Test + public void testCorrectURLHttps() { + final String in = "https://example.com"; + final String out = UrlChecker.prepareUrl(in); + assertEquals(in, out); + } + + @Test + public void testMissingProtocol() { + final String in = "example.com"; + final String out = UrlChecker.prepareUrl(in); + assertEquals("http://example.com", out); + } + + @Test + public void testFeedProtocol() { + final String in = "feed://example.com"; + final String out = UrlChecker.prepareUrl(in); + assertEquals("http://example.com", out); + } + + @Test + public void testPcastProtocolNoScheme() { + final String in = "pcast://example.com"; + final String out = UrlChecker.prepareUrl(in); + assertEquals("http://example.com", out); + } + + @Test + public void testItpcProtocol() { + final String in = "itpc://example.com"; + final String out = UrlChecker.prepareUrl(in); + assertEquals("http://example.com", out); + } + + @Test + public void testItpcProtocolWithScheme() { + final String in = "itpc://https://example.com"; + final String out = UrlChecker.prepareUrl(in); + assertEquals("https://example.com", out); + } + + @Test + public void testWhiteSpaceUrlShouldNotAppend() { + final String in = "\n http://example.com \t"; + final String out = UrlChecker.prepareUrl(in); + assertEquals("http://example.com", out); + } + + @Test + public void testWhiteSpaceShouldAppend() { + final String in = "\n example.com \t"; + final String out = UrlChecker.prepareUrl(in); + assertEquals("http://example.com", out); + } + + @Test + public void testAntennaPodSubscribeProtocolNoScheme() { + final String in = "antennapod-subscribe://example.com"; + final String out = UrlChecker.prepareUrl(in); + assertEquals("http://example.com", out); + } + + @Test + public void testPcastProtocolWithScheme() { + final String in = "pcast://https://example.com"; + final String out = UrlChecker.prepareUrl(in); + assertEquals("https://example.com", out); + } + + @Test + public void testAntennaPodSubscribeProtocolWithScheme() { + final String in = "antennapod-subscribe://https://example.com"; + final String out = UrlChecker.prepareUrl(in); + assertEquals("https://example.com", out); + } + + @Test + public void testAntennaPodSubscribeDeeplink() throws UnsupportedEncodingException { + final String feed = "http://example.org/podcast.rss"; + assertEquals(feed, UrlChecker.prepareUrl("https://antennapod.org/deeplink/subscribe?url=" + feed)); + assertEquals(feed, UrlChecker.prepareUrl("http://antennapod.org/deeplink/subscribe?url=" + feed)); + assertEquals(feed, UrlChecker.prepareUrl("http://antennapod.org/deeplink/subscribe/?url=" + feed)); + assertEquals(feed, UrlChecker.prepareUrl("https://www.antennapod.org/deeplink/subscribe?url=" + feed)); + assertEquals(feed, UrlChecker.prepareUrl("http://www.antennapod.org/deeplink/subscribe?url=" + feed)); + assertEquals(feed, UrlChecker.prepareUrl("http://www.antennapod.org/deeplink/subscribe/?url=" + feed)); + assertEquals(feed, UrlChecker.prepareUrl("http://www.antennapod.org/deeplink/subscribe?url=" + + URLEncoder.encode(feed, "UTF-8"))); + assertEquals(feed, UrlChecker.prepareUrl("http://www.antennapod.org/deeplink/subscribe?url=" + + "example.org/podcast.rss")); + } + + @Test + public void testProtocolRelativeUrlIsAbsolute() { + final String in = "https://example.com"; + final String inBase = "http://examplebase.com"; + final String out = UrlChecker.prepareUrl(in, inBase); + assertEquals(in, out); + } + + @Test + public void testProtocolRelativeUrlIsRelativeHttps() { + final String in = "//example.com"; + final String inBase = "https://examplebase.com"; + final String out = UrlChecker.prepareUrl(in, inBase); + assertEquals("https://example.com", out); + } + + @Test + public void testProtocolRelativeUrlIsHttpsWithApSubscribeProtocol() { + final String in = "//example.com"; + final String inBase = "antennapod-subscribe://https://examplebase.com"; + final String out = UrlChecker.prepareUrl(in, inBase); + assertEquals("https://example.com", out); + } + + @Test + public void testProtocolRelativeUrlBaseUrlNull() { + final String in = "example.com"; + final String out = UrlChecker.prepareUrl(in, null); + assertEquals("http://example.com", out); + } + + @Test + public void testUrlEqualsSame() { + assertTrue(UrlChecker.urlEquals("https://www.example.com/test", "https://www.example.com/test")); + assertTrue(UrlChecker.urlEquals("https://www.example.com/test", "https://www.example.com/test/")); + assertTrue(UrlChecker.urlEquals("https://www.example.com/test", "https://www.example.com//test")); + assertTrue(UrlChecker.urlEquals("https://www.example.com", "https://www.example.com/")); + assertTrue(UrlChecker.urlEquals("https://www.example.com", "http://www.example.com")); + assertTrue(UrlChecker.urlEquals("http://www.example.com/", "https://www.example.com/")); + assertTrue(UrlChecker.urlEquals("https://www.example.com/?id=42", "https://www.example.com/?id=42")); + assertTrue(UrlChecker.urlEquals("https://example.com/podcast%20test", "https://example.com/podcast test")); + assertTrue(UrlChecker.urlEquals("https://example.com/?a=podcast%20test", "https://example.com/?a=podcast test")); + assertTrue(UrlChecker.urlEquals("https://example.com/?", "https://example.com/")); + assertTrue(UrlChecker.urlEquals("https://example.com/?", "https://example.com")); + assertTrue(UrlChecker.urlEquals("https://Example.com", "https://example.com")); + assertTrue(UrlChecker.urlEquals("https://example.com/test", "https://example.com/Test")); + } + + @Test + public void testUrlEqualsDifferent() { + assertFalse(UrlChecker.urlEquals("https://www.example.com/test", "https://www.example2.com/test")); + assertFalse(UrlChecker.urlEquals("https://www.example.com/test", "https://www.example.de/test")); + assertFalse(UrlChecker.urlEquals("https://example.com/", "https://otherpodcast.example.com/")); + assertFalse(UrlChecker.urlEquals("https://www.example.com/?id=42&a=b", "https://www.example.com/?id=43&a=b")); + assertFalse(UrlChecker.urlEquals("https://example.com/podcast%25test", "https://example.com/podcast test")); + } +} diff --git a/net/download/README.md b/net/download/README.md new file mode 100644 index 000000000..57b2d2f31 --- /dev/null +++ b/net/download/README.md @@ -0,0 +1,3 @@ +# :net:download + +This folder contains the download service and its interface. diff --git a/net/download/service-interface/README.md b/net/download/service-interface/README.md new file mode 100644 index 000000000..fafe03230 --- /dev/null +++ b/net/download/service-interface/README.md @@ -0,0 +1,3 @@ +# :net:download:service-interface + +Interface of the download service. Enables other modules to call the download service without actually depending on the implementation. diff --git a/net/download/service-interface/build.gradle b/net/download/service-interface/build.gradle new file mode 100644 index 000000000..785326bab --- /dev/null +++ b/net/download/service-interface/build.gradle @@ -0,0 +1,21 @@ +plugins { + id("com.android.library") + id("java-test-fixtures") +} +apply from: "../../../common.gradle" + +android { + lintOptions { + disable 'ParcelClassLoader' + } +} + +dependencies { + implementation project(':model') + implementation project(':net:common') + + annotationProcessor "androidx.annotation:annotation:$annotationVersion" + + testImplementation "junit:junit:$junitVersion" + testImplementation "org.robolectric:robolectric:$robolectricVersion" +} diff --git a/net/download/service-interface/src/main/AndroidManifest.xml b/net/download/service-interface/src/main/AndroidManifest.xml new file mode 100644 index 000000000..df6365325 --- /dev/null +++ b/net/download/service-interface/src/main/AndroidManifest.xml @@ -0,0 +1 @@ +<manifest package="de.danoeh.antennapod.net.download.serviceinterface" /> diff --git a/net/download/service-interface/src/main/java/de/danoeh/antennapod/net/download/serviceinterface/DownloadRequest.java b/net/download/service-interface/src/main/java/de/danoeh/antennapod/net/download/serviceinterface/DownloadRequest.java new file mode 100644 index 000000000..e5c6662eb --- /dev/null +++ b/net/download/service-interface/src/main/java/de/danoeh/antennapod/net/download/serviceinterface/DownloadRequest.java @@ -0,0 +1,331 @@ +package de.danoeh.antennapod.net.download.serviceinterface; + +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.net.common.UrlChecker; +import de.danoeh.antennapod.model.feed.FeedMedia; + +public class DownloadRequest implements Parcelable { + public static final String REQUEST_ARG_PAGE_NR = "page"; + public static final String REQUEST_ARG_LOAD_ALL_PAGES = "loadAllPages"; + + private final String destination; + private final String source; + private final String title; + private String username; + private String password; + private String lastModified; + private final boolean deleteOnFailure; + private final long feedfileId; + private final int feedfileType; + private final Bundle arguments; + + private int progressPercent; + private long soFar; + private long size; + private int statusMsg; + private boolean mediaEnqueued; + private boolean initiatedByUser; + + public DownloadRequest(@NonNull String destination, @NonNull String source, @NonNull String title, long feedfileId, + int feedfileType, String username, String password, boolean deleteOnFailure, + Bundle arguments, boolean initiatedByUser) { + this(destination, source, title, feedfileId, feedfileType, null, deleteOnFailure, username, password, false, + arguments, initiatedByUser); + } + + private DownloadRequest(Builder builder) { + this(builder.destination, builder.source, builder.title, builder.feedfileId, builder.feedfileType, + builder.lastModified, builder.deleteOnFailure, builder.username, builder.password, false, + builder.arguments != null ? builder.arguments : new Bundle(), builder.initiatedByUser); + } + + private DownloadRequest(Parcel in) { + this(in.readString(), in.readString(), in.readString(), in.readLong(), in.readInt(), in.readString(), + in.readByte() > 0, nullIfEmpty(in.readString()), nullIfEmpty(in.readString()), in.readByte() > 0, + in.readBundle(), in.readByte() > 0); + } + + private DownloadRequest(String destination, String source, String title, long feedfileId, int feedfileType, + String lastModified, boolean deleteOnFailure, String username, String password, + boolean mediaEnqueued, Bundle arguments, boolean initiatedByUser) { + this.destination = destination; + this.source = source; + this.title = title; + this.feedfileId = feedfileId; + this.feedfileType = feedfileType; + this.lastModified = lastModified; + this.deleteOnFailure = deleteOnFailure; + this.username = username; + this.password = password; + this.mediaEnqueued = mediaEnqueued; + this.arguments = arguments; + this.initiatedByUser = initiatedByUser; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(destination); + dest.writeString(source); + dest.writeString(title); + dest.writeLong(feedfileId); + dest.writeInt(feedfileType); + dest.writeString(lastModified); + dest.writeByte((deleteOnFailure) ? (byte) 1 : 0); + // in case of null username/password, still write an empty string + // (rather than skipping it). Otherwise, unmarshalling a collection + // of them from a Parcel (from an Intent extra to submit a request to DownloadService) will fail. + // + // see: https://stackoverflow.com/a/22926342 + dest.writeString(nonNullString(username)); + dest.writeString(nonNullString(password)); + dest.writeByte((mediaEnqueued) ? (byte) 1 : 0); + dest.writeBundle(arguments); + dest.writeByte(initiatedByUser ? (byte) 1 : 0); + } + + private static String nonNullString(String str) { + return str != null ? str : ""; + } + + private static String nullIfEmpty(String str) { + return TextUtils.isEmpty(str) ? null : str; + } + + public static final Parcelable.Creator<DownloadRequest> CREATOR = new Parcelable.Creator<DownloadRequest>() { + public DownloadRequest createFromParcel(Parcel in) { + return new DownloadRequest(in); + } + + public DownloadRequest[] newArray(int size) { + return new DownloadRequest[size]; + } + }; + + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof DownloadRequest)) return false; + + DownloadRequest that = (DownloadRequest) o; + + if (lastModified != null ? !lastModified.equals(that.lastModified) : that.lastModified != null) + return false; + if (deleteOnFailure != that.deleteOnFailure) return false; + if (feedfileId != that.feedfileId) return false; + if (feedfileType != that.feedfileType) return false; + if (progressPercent != that.progressPercent) return false; + if (size != that.size) return false; + if (soFar != that.soFar) return false; + if (statusMsg != that.statusMsg) return false; + if (!destination.equals(that.destination)) return false; + if (password != null ? !password.equals(that.password) : that.password != null) + return false; + if (!source.equals(that.source)) return false; + if (title != null ? !title.equals(that.title) : that.title != null) return false; + if (username != null ? !username.equals(that.username) : that.username != null) + return false; + if (mediaEnqueued != that.mediaEnqueued) return false; + if (initiatedByUser != that.initiatedByUser) return false; + return true; + } + + @Override + public int hashCode() { + int result = destination.hashCode(); + result = 31 * result + source.hashCode(); + result = 31 * result + (title != null ? title.hashCode() : 0); + result = 31 * result + (username != null ? username.hashCode() : 0); + result = 31 * result + (password != null ? password.hashCode() : 0); + result = 31 * result + (lastModified != null ? lastModified.hashCode() : 0); + result = 31 * result + (deleteOnFailure ? 1 : 0); + result = 31 * result + (int) (feedfileId ^ (feedfileId >>> 32)); + result = 31 * result + feedfileType; + result = 31 * result + arguments.hashCode(); + result = 31 * result + progressPercent; + result = 31 * result + (int) (soFar ^ (soFar >>> 32)); + result = 31 * result + (int) (size ^ (size >>> 32)); + result = 31 * result + statusMsg; + result = 31 * result + (mediaEnqueued ? 1 : 0); + return result; + } + + public String getDestination() { + return destination; + } + + public String getSource() { + return source; + } + + public String getTitle() { + return title; + } + + public long getFeedfileId() { + return feedfileId; + } + + public int getFeedfileType() { + return feedfileType; + } + + public int getProgressPercent() { + return progressPercent; + } + + public void setProgressPercent(int progressPercent) { + this.progressPercent = progressPercent; + } + + public long getSoFar() { + return soFar; + } + + public void setSoFar(long soFar) { + this.soFar = soFar; + } + + public long getSize() { + return size; + } + + public void setSize(long size) { + this.size = size; + } + + public void setStatusMsg(int statusMsg) { + this.statusMsg = statusMsg; + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } + + public void setUsername(String username) { + this.username = username; + } + + public void setPassword(String password) { + this.password = password; + } + + public DownloadRequest setLastModified(@Nullable String lastModified) { + this.lastModified = lastModified; + return this; + } + + @Nullable + public String getLastModified() { + return lastModified; + } + + public boolean isDeleteOnFailure() { + return deleteOnFailure; + } + + public boolean isMediaEnqueued() { + return mediaEnqueued; + } + + public boolean isInitiatedByUser() { + return initiatedByUser; + } + + /** + * Set to true if the media is enqueued because of this download. + * The state is helpful if the download is cancelled, and undoing the enqueue is needed. + */ + public void setMediaEnqueued(boolean mediaEnqueued) { + this.mediaEnqueued = mediaEnqueued; + } + + public Bundle getArguments() { + return arguments; + } + + public static class Builder { + private final String destination; + private final String source; + private final String title; + private String username; + private String password; + private String lastModified; + private boolean deleteOnFailure = false; + private final long feedfileId; + private final int feedfileType; + private final Bundle arguments = new Bundle(); + private boolean initiatedByUser = true; + + public Builder(@NonNull String destination, @NonNull FeedMedia media) { + this.destination = destination; + this.source = UrlChecker.prepareUrl(media.getDownload_url()); + this.title = media.getHumanReadableIdentifier(); + this.feedfileId = media.getId(); + this.feedfileType = media.getTypeAsInt(); + } + + public Builder(@NonNull String destination, @NonNull Feed feed) { + this.destination = destination; + this.source = feed.isLocalFeed() ? feed.getDownload_url() : UrlChecker.prepareUrl(feed.getDownload_url()); + this.title = feed.getHumanReadableIdentifier(); + this.feedfileId = feed.getId(); + this.feedfileType = feed.getTypeAsInt(); + arguments.putInt(REQUEST_ARG_PAGE_NR, feed.getPageNr()); + } + + public Builder withInitiatedByUser(boolean initiatedByUser) { + this.initiatedByUser = initiatedByUser; + return this; + } + + public void setForce(boolean force) { + if (force) { + lastModified = null; + } + } + + public Builder deleteOnFailure(boolean deleteOnFailure) { + this.deleteOnFailure = deleteOnFailure; + return this; + } + + public Builder lastModified(String lastModified) { + this.lastModified = lastModified; + return this; + } + + public Builder withAuthentication(String username, String password) { + this.username = username; + this.password = password; + return this; + } + + public void loadAllPages(boolean loadAllPages) { + if (loadAllPages) { + arguments.putBoolean(REQUEST_ARG_LOAD_ALL_PAGES, true); + } + } + + public DownloadRequest build() { + return new DownloadRequest(this); + } + } +} diff --git a/net/download/service-interface/src/main/java/de/danoeh/antennapod/net/download/serviceinterface/DownloadServiceInterface.java b/net/download/service-interface/src/main/java/de/danoeh/antennapod/net/download/serviceinterface/DownloadServiceInterface.java new file mode 100644 index 000000000..54987a83e --- /dev/null +++ b/net/download/service-interface/src/main/java/de/danoeh/antennapod/net/download/serviceinterface/DownloadServiceInterface.java @@ -0,0 +1,23 @@ +package de.danoeh.antennapod.net.download.serviceinterface; + +import android.content.Context; + +public abstract class DownloadServiceInterface { + private static DownloadServiceInterface impl; + + public static DownloadServiceInterface get() { + return impl; + } + + public static void setImpl(DownloadServiceInterface impl) { + DownloadServiceInterface.impl = impl; + } + + public abstract void download(Context context, boolean cleanupMedia, DownloadRequest... requests); + + public abstract void refreshAllFeeds(Context context, boolean initiatedByUser); + + public abstract void cancel(Context context, String url); + + public abstract void cancelAll(Context context); +} diff --git a/net/download/service-interface/src/main/java/de/danoeh/antennapod/net/download/serviceinterface/DownloadServiceInterfaceStub.java b/net/download/service-interface/src/main/java/de/danoeh/antennapod/net/download/serviceinterface/DownloadServiceInterfaceStub.java new file mode 100644 index 000000000..251c59c61 --- /dev/null +++ b/net/download/service-interface/src/main/java/de/danoeh/antennapod/net/download/serviceinterface/DownloadServiceInterfaceStub.java @@ -0,0 +1,18 @@ +package de.danoeh.antennapod.net.download.serviceinterface; + +import android.content.Context; + +public class DownloadServiceInterfaceStub extends DownloadServiceInterface { + + public void download(Context context, boolean cleanupMedia, DownloadRequest... requests) { + } + + public void refreshAllFeeds(Context context, boolean initiatedByUser) { + } + + public void cancel(Context context, String url) { + } + + public void cancelAll(Context context) { + } +} diff --git a/net/download/service-interface/src/test/java/de/danoeh/antennapod/net/download/serviceinterface/DownloadRequestTest.java b/net/download/service-interface/src/test/java/de/danoeh/antennapod/net/download/serviceinterface/DownloadRequestTest.java new file mode 100644 index 000000000..2709744f7 --- /dev/null +++ b/net/download/service-interface/src/test/java/de/danoeh/antennapod/net/download/serviceinterface/DownloadRequestTest.java @@ -0,0 +1,124 @@ +package de.danoeh.antennapod.net.download.serviceinterface; + +import android.os.Bundle; +import android.os.Parcel; + +import de.danoeh.antennapod.model.feed.FeedMedia; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import java.util.ArrayList; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; + +@RunWith(RobolectricTestRunner.class) +public class DownloadRequestTest { + + @Test + public void parcelInArrayListTest_WithAuth() { + doTestParcelInArrayList("case has authentication", + "usr1", "pass1", "usr2", "pass2"); + } + + @Test + public void parcelInArrayListTest_NoAuth() { + doTestParcelInArrayList("case no authentication", + null, null, null, null); + } + + @Test + public void parcelInArrayListTest_MixAuth() { + doTestParcelInArrayList("case mixed authentication", + null, null, "usr2", "pass2"); + } + + @Test + public void downloadRequestTestEquals() { + String destStr = "file://location/media.mp3"; + String username = "testUser"; + String password = "testPassword"; + FeedMedia item = createFeedItem(1); + DownloadRequest request1 = new DownloadRequest.Builder(destStr, item) + .deleteOnFailure(true) + .withAuthentication(username, password) + .build(); + + DownloadRequest request2 = new DownloadRequest.Builder(destStr, item) + .deleteOnFailure(true) + .withAuthentication(username, password) + .build(); + + DownloadRequest request3 = new DownloadRequest.Builder(destStr, item) + .deleteOnFailure(true) + .withAuthentication("diffUsername", "diffPassword") + .build(); + + assertEquals(request1, request2); + assertNotEquals(request1, request3); + } + + // Test to ensure parcel using put/getParcelableArrayList() API work + // based on: https://stackoverflow.com/a/13507191 + private void doTestParcelInArrayList(String message, + String username1, String password1, + String username2, String password2) { + ArrayList<DownloadRequest> toParcel; + { // test DownloadRequests to parcel + String destStr = "file://location/media.mp3"; + FeedMedia item1 = createFeedItem(1); + DownloadRequest request1 = new DownloadRequest.Builder(destStr, item1) + .withAuthentication(username1, password1) + .build(); + + FeedMedia item2 = createFeedItem(2); + DownloadRequest request2 = new DownloadRequest.Builder(destStr, item2) + .withAuthentication(username2, password2) + .build(); + + toParcel = new ArrayList<>(); + toParcel.add(request1); + toParcel.add(request2); + } + + // parcel the download requests + Bundle bundleIn = new Bundle(); + bundleIn.putParcelableArrayList("r", toParcel); + + Parcel parcel = Parcel.obtain(); + bundleIn.writeToParcel(parcel, 0); + + Bundle bundleOut = new Bundle(); + bundleOut.setClassLoader(DownloadRequest.class.getClassLoader()); + parcel.setDataPosition(0); // to read the parcel from the beginning. + bundleOut.readFromParcel(parcel); + + ArrayList<DownloadRequest> fromParcel = bundleOut.getParcelableArrayList("r"); + + // spot-check contents to ensure they are the same + // DownloadRequest.equals() implementation doesn't quite work + // for DownloadRequest.argument (a Bundle) + assertEquals(message + " - size", toParcel.size(), fromParcel.size()); + assertEquals(message + " - source", toParcel.get(1).getSource(), fromParcel.get(1).getSource()); + assertEquals(message + " - password", toParcel.get(0).getPassword(), fromParcel.get(0).getPassword()); + assertEquals(message + " - argument", toString(toParcel.get(0).getArguments()), + toString(fromParcel.get(0).getArguments())); + } + + private static String toString(Bundle b) { + StringBuilder sb = new StringBuilder(); + sb.append("{"); + for (String key: b.keySet()) { + Object val = b.get(key); + sb.append("(").append(key).append(":").append(val).append(") "); + } + sb.append("}"); + return sb.toString(); + } + + private FeedMedia createFeedItem(final int id) { + // Use mockito would be less verbose, but it'll take extra 1 second for this tiny test + return new FeedMedia(id, null, 0, 0, 0, "", "", "http://example.com/episode" + id, false, null, 0, 0); + } +} |