summaryrefslogtreecommitdiff
path: root/net/download/service-interface
diff options
context:
space:
mode:
Diffstat (limited to 'net/download/service-interface')
-rw-r--r--net/download/service-interface/README.md3
-rw-r--r--net/download/service-interface/build.gradle21
-rw-r--r--net/download/service-interface/src/main/AndroidManifest.xml1
-rw-r--r--net/download/service-interface/src/main/java/de/danoeh/antennapod/net/download/serviceinterface/DownloadRequest.java331
-rw-r--r--net/download/service-interface/src/main/java/de/danoeh/antennapod/net/download/serviceinterface/DownloadServiceInterface.java23
-rw-r--r--net/download/service-interface/src/main/java/de/danoeh/antennapod/net/download/serviceinterface/DownloadServiceInterfaceStub.java18
-rw-r--r--net/download/service-interface/src/test/java/de/danoeh/antennapod/net/download/serviceinterface/DownloadRequestTest.java124
7 files changed, 521 insertions, 0 deletions
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);
+ }
+}