summaryrefslogtreecommitdiff
path: root/core/src/main/java/de
diff options
context:
space:
mode:
Diffstat (limited to 'core/src/main/java/de')
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/asynctask/FeedRemover.java7
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/backup/OpmlBackupAgent.java22
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/event/MessageEvent.java21
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/export/CommonSymbols.java24
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/export/ExportWriter.java29
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/export/html/HtmlSymbols.java30
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/export/html/HtmlWriter.java82
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/export/opml/OpmlElement.java (renamed from core/src/main/java/de/danoeh/antennapod/core/opml/OpmlElement.java)2
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/export/opml/OpmlReader.java (renamed from core/src/main/java/de/danoeh/antennapod/core/opml/OpmlReader.java)2
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/export/opml/OpmlSymbols.java21
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/export/opml/OpmlWriter.java (renamed from core/src/main/java/de/danoeh/antennapod/core/opml/OpmlWriter.java)30
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/Feed.java52
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java2
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilter.java6
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/glide/ApOkHttpUrlLoader.java24
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetService.java40
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/opml/OpmlSymbols.java22
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/preferences/SleepTimerPreferences.java80
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java42
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/download/AntennapodHttpClient.java78
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java12
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java41
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java51
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java96
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java58
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java4
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java39
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java11
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/FeedSearcher.java24
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java69
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/syndication/handler/SyndHandler.java19
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSMedia.java34
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSRSS20.java59
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/NSAtom.java40
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndTypeUtils.java53
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java8
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/comparator/SearchResultValueComparator.java15
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java2
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/syndication/HtmlToPlainText.java89
39 files changed, 970 insertions, 370 deletions
diff --git a/core/src/main/java/de/danoeh/antennapod/core/asynctask/FeedRemover.java b/core/src/main/java/de/danoeh/antennapod/core/asynctask/FeedRemover.java
index e475e696c..67c460e78 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/asynctask/FeedRemover.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/asynctask/FeedRemover.java
@@ -38,10 +38,11 @@ public class FeedRemover extends AsyncTask<Void, Void, Void> {
@Override
protected void onPostExecute(Void result) {
- dialog.dismiss();
+ if(dialog != null && dialog.isShowing()) {
+ dialog.dismiss();
+ }
if(skipOnCompletion) {
- context.sendBroadcast(new Intent(
- PlaybackService.ACTION_SKIP_CURRENT_EPISODE));
+ context.sendBroadcast(new Intent(PlaybackService.ACTION_SKIP_CURRENT_EPISODE));
}
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/backup/OpmlBackupAgent.java b/core/src/main/java/de/danoeh/antennapod/core/backup/OpmlBackupAgent.java
index 982015314..80ce6cf56 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/backup/OpmlBackupAgent.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/backup/OpmlBackupAgent.java
@@ -8,6 +8,7 @@ import android.content.Context;
import android.os.ParcelFileDescriptor;
import android.util.Log;
+import org.apache.commons.io.IOUtils;
import org.xmlpull.v1.XmlPullParserException;
import java.io.ByteArrayOutputStream;
@@ -27,10 +28,10 @@ import java.util.ArrayList;
import java.util.Arrays;
import de.danoeh.antennapod.core.BuildConfig;
+import de.danoeh.antennapod.core.export.opml.OpmlElement;
+import de.danoeh.antennapod.core.export.opml.OpmlReader;
+import de.danoeh.antennapod.core.export.opml.OpmlWriter;
import de.danoeh.antennapod.core.feed.Feed;
-import de.danoeh.antennapod.core.opml.OpmlElement;
-import de.danoeh.antennapod.core.opml.OpmlReader;
-import de.danoeh.antennapod.core.opml.OpmlWriter;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DownloadRequestException;
import de.danoeh.antennapod.core.storage.DownloadRequester;
@@ -56,7 +57,9 @@ public class OpmlBackupAgent extends BackupAgentHelper {
}
}
- /** Class for backing up and restoring the OPML file. */
+ /**
+ * Class for backing up and restoring the OPML file.
+ */
private static class OpmlBackupHelper implements BackupHelper {
private static final String TAG = "OpmlBackupHelper";
@@ -64,7 +67,9 @@ public class OpmlBackupAgent extends BackupAgentHelper {
private final Context mContext;
- /** Checksum of restored OPML file */
+ /**
+ * Checksum of restored OPML file
+ */
private byte[] mChecksum;
public OpmlBackupHelper(Context context) {
@@ -170,12 +175,7 @@ public class OpmlBackupAgent extends BackupAgentHelper {
} catch (IOException e) {
Log.e(TAG, "Failed to restore OPML backup", e);
} finally {
- if (reader != null) {
- try {
- reader.close();
- } catch (IOException e) {
- }
- }
+ IOUtils.closeQuietly(reader);
}
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/MessageEvent.java b/core/src/main/java/de/danoeh/antennapod/core/event/MessageEvent.java
new file mode 100644
index 000000000..9fc488fbc
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/event/MessageEvent.java
@@ -0,0 +1,21 @@
+package de.danoeh.antennapod.core.event;
+
+import android.support.annotation.Nullable;
+
+public class MessageEvent {
+
+ public final String message;
+
+ @Nullable
+ public final Runnable action;
+
+ public MessageEvent(String message) {
+ this(message, null);
+ }
+
+ public MessageEvent(String message, Runnable action) {
+ this.message = message;
+ this.action = action;
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/export/CommonSymbols.java b/core/src/main/java/de/danoeh/antennapod/core/export/CommonSymbols.java
new file mode 100644
index 000000000..3ed251047
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/export/CommonSymbols.java
@@ -0,0 +1,24 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package de.danoeh.antennapod.core.export;
+
+public class CommonSymbols {
+
+ public static final String HEAD = "head";
+ public static final String BODY = "body";
+ public static final String TITLE = "title";
+
+ public static final String XML_FEATURE_INDENT_OUTPUT = "http://xmlpull.org/v1/doc/features.html#indent-output";
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/export/ExportWriter.java b/core/src/main/java/de/danoeh/antennapod/core/export/ExportWriter.java
new file mode 100644
index 000000000..d6a187b21
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/export/ExportWriter.java
@@ -0,0 +1,29 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package de.danoeh.antennapod.core.export;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.List;
+
+import de.danoeh.antennapod.core.feed.Feed;
+
+public interface ExportWriter {
+
+ void writeDocument(List<Feed> feeds, Writer writer)
+ throws IllegalArgumentException, IllegalStateException, IOException;
+
+ String fileExtension();
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/export/html/HtmlSymbols.java b/core/src/main/java/de/danoeh/antennapod/core/export/html/HtmlSymbols.java
new file mode 100644
index 000000000..b8807a686
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/export/html/HtmlSymbols.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package de.danoeh.antennapod.core.export.html;
+
+import de.danoeh.antennapod.core.export.CommonSymbols;
+
+class HtmlSymbols extends CommonSymbols {
+
+ static final String HTML = "html";
+
+ static final String ORDERED_LIST = "ol";
+ static final String LIST_ITEM = "li";
+
+ static String HEADING = "h1";
+
+ static final String LINK = "a";
+ static final String LINK_DESTINATION = "href";
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/export/html/HtmlWriter.java b/core/src/main/java/de/danoeh/antennapod/core/export/html/HtmlWriter.java
new file mode 100644
index 000000000..c24b39812
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/export/html/HtmlWriter.java
@@ -0,0 +1,82 @@
+package de.danoeh.antennapod.core.export.html;
+
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Xml;
+
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.List;
+
+import de.danoeh.antennapod.core.export.ExportWriter;
+import de.danoeh.antennapod.core.feed.Feed;
+
+/** Writes HTML documents. */
+public class HtmlWriter implements ExportWriter {
+
+ private static final String TAG = "HtmlWriter";
+ private static final String ENCODING = "UTF-8";
+ private static final String HTML_TITLE = "AntennaPod Subscriptions";
+
+ /**
+ * Takes a list of feeds and a writer and writes those into an HTML
+ * document.
+ *
+ * @throws IOException
+ * @throws IllegalStateException
+ * @throws IllegalArgumentException
+ */
+ @Override
+ public void writeDocument(List<Feed> feeds, Writer writer)
+ throws IllegalArgumentException, IllegalStateException, IOException {
+ Log.d(TAG, "Starting to write document");
+ XmlSerializer xs = Xml.newSerializer();
+ xs.setFeature(HtmlSymbols.XML_FEATURE_INDENT_OUTPUT, true);
+ xs.setOutput(writer);
+
+ xs.startDocument(ENCODING, false);
+ xs.startTag(null, HtmlSymbols.HTML);
+ xs.startTag(null, HtmlSymbols.HEAD);
+ xs.startTag(null, HtmlSymbols.TITLE);
+ xs.text(HTML_TITLE);
+ xs.endTag(null, HtmlSymbols.TITLE);
+ xs.endTag(null, HtmlSymbols.HEAD);
+
+ xs.startTag(null, HtmlSymbols.BODY);
+ xs.startTag(null, HtmlSymbols.HEADING);
+ xs.text(HTML_TITLE);
+ xs.endTag(null, HtmlSymbols.HEADING);
+ xs.startTag(null, HtmlSymbols.ORDERED_LIST);
+ for (Feed feed : feeds) {
+ xs.startTag(null, HtmlSymbols.LIST_ITEM);
+ xs.text(feed.getTitle());
+ if (!TextUtils.isEmpty(feed.getLink())) {
+ xs.text(" [");
+ xs.startTag(null, HtmlSymbols.LINK);
+ xs.attribute(null, HtmlSymbols.LINK_DESTINATION, feed.getLink());
+ xs.text("Website");
+ xs.endTag(null, HtmlSymbols.LINK);
+ xs.text("]");
+ }
+ xs.text(" [");
+ xs.startTag(null, HtmlSymbols.LINK);
+ xs.attribute(null, HtmlSymbols.LINK_DESTINATION, feed.getDownload_url());
+ xs.text("Feed");
+ xs.endTag(null, HtmlSymbols.LINK);
+ xs.text("]");
+ xs.endTag(null, HtmlSymbols.LIST_ITEM);
+ }
+ xs.endTag(null, HtmlSymbols.ORDERED_LIST);
+ xs.endTag(null, HtmlSymbols.BODY);
+ xs.endTag(null, HtmlSymbols.HTML);
+ xs.endDocument();
+ Log.d(TAG, "Finished writing document");
+ }
+
+ public String fileExtension() {
+ return "html";
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlElement.java b/core/src/main/java/de/danoeh/antennapod/core/export/opml/OpmlElement.java
index 8d0a4a842..61eb4d0c9 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlElement.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/export/opml/OpmlElement.java
@@ -1,4 +1,4 @@
-package de.danoeh.antennapod.core.opml;
+package de.danoeh.antennapod.core.export.opml;
/** Represents a single feed in an OPML file. */
public class OpmlElement {
diff --git a/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlReader.java b/core/src/main/java/de/danoeh/antennapod/core/export/opml/OpmlReader.java
index 17afc7904..a17fedd7d 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlReader.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/export/opml/OpmlReader.java
@@ -1,4 +1,4 @@
-package de.danoeh.antennapod.core.opml;
+package de.danoeh.antennapod.core.export.opml;
import android.util.Log;
diff --git a/core/src/main/java/de/danoeh/antennapod/core/export/opml/OpmlSymbols.java b/core/src/main/java/de/danoeh/antennapod/core/export/opml/OpmlSymbols.java
new file mode 100644
index 000000000..40b0e23b8
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/export/opml/OpmlSymbols.java
@@ -0,0 +1,21 @@
+package de.danoeh.antennapod.core.export.opml;
+
+import de.danoeh.antennapod.core.export.CommonSymbols;
+
+/** Contains symbols for reading and writing OPML documents. */
+public final class OpmlSymbols extends CommonSymbols {
+
+ public static final String OPML = "opml";
+ static final String OUTLINE = "outline";
+ static final String TEXT = "text";
+ static final String XMLURL = "xmlUrl";
+ static final String HTMLURL = "htmlUrl";
+ static final String TYPE = "type";
+ static final String VERSION = "version";
+ static final String DATE_CREATED = "dateCreated";
+
+ private OpmlSymbols() {
+
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlWriter.java b/core/src/main/java/de/danoeh/antennapod/core/export/opml/OpmlWriter.java
index 673c602df..fd0922f72 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlWriter.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/export/opml/OpmlWriter.java
@@ -1,4 +1,4 @@
-package de.danoeh.antennapod.core.opml;
+package de.danoeh.antennapod.core.export.opml;
import android.util.Log;
import android.util.Xml;
@@ -10,11 +10,13 @@ import java.io.Writer;
import java.util.Date;
import java.util.List;
+import de.danoeh.antennapod.core.export.ExportWriter;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.util.DateUtils;
/** Writes OPML documents. */
-public class OpmlWriter {
+public class OpmlWriter implements ExportWriter {
+
private static final String TAG = "OpmlWriter";
private static final String ENCODING = "UTF-8";
private static final String OPML_VERSION = "2.0";
@@ -28,40 +30,29 @@ public class OpmlWriter {
* @throws IllegalStateException
* @throws IllegalArgumentException
*/
+ @Override
public void writeDocument(List<Feed> feeds, Writer writer)
throws IllegalArgumentException, IllegalStateException, IOException {
Log.d(TAG, "Starting to write document");
XmlSerializer xs = Xml.newSerializer();
+ xs.setFeature(OpmlSymbols.XML_FEATURE_INDENT_OUTPUT, true);
xs.setOutput(writer);
xs.startDocument(ENCODING, false);
- xs.text("\n");
xs.startTag(null, OpmlSymbols.OPML);
xs.attribute(null, OpmlSymbols.VERSION, OPML_VERSION);
- xs.text("\n");
- xs.text(" ");
xs.startTag(null, OpmlSymbols.HEAD);
- xs.text("\n");
- xs.text(" ");
xs.startTag(null, OpmlSymbols.TITLE);
xs.text(OPML_TITLE);
xs.endTag(null, OpmlSymbols.TITLE);
- xs.text("\n");
- xs.text(" ");
xs.startTag(null, OpmlSymbols.DATE_CREATED);
xs.text(DateUtils.formatRFC822Date(new Date()));
xs.endTag(null, OpmlSymbols.DATE_CREATED);
- xs.text("\n");
- xs.text(" ");
xs.endTag(null, OpmlSymbols.HEAD);
- xs.text("\n");
- xs.text(" ");
xs.startTag(null, OpmlSymbols.BODY);
- xs.text("\n");
for (Feed feed : feeds) {
- xs.text(" ");
xs.startTag(null, OpmlSymbols.OUTLINE);
xs.attribute(null, OpmlSymbols.TEXT, feed.getTitle());
xs.attribute(null, OpmlSymbols.TITLE, feed.getTitle());
@@ -73,14 +64,15 @@ public class OpmlWriter {
xs.attribute(null, OpmlSymbols.HTMLURL, feed.getLink());
}
xs.endTag(null, OpmlSymbols.OUTLINE);
- xs.text("\n");
}
- xs.text(" ");
xs.endTag(null, OpmlSymbols.BODY);
- xs.text("\n");
xs.endTag(null, OpmlSymbols.OPML);
- xs.text("\n");
xs.endDocument();
Log.d(TAG, "Finished writing document");
}
+
+ public String fileExtension() {
+ return "opml";
+ }
+
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/Feed.java b/core/src/main/java/de/danoeh/antennapod/core/feed/Feed.java
index bb0724d66..ac23f3d3d 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/feed/Feed.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/feed/Feed.java
@@ -26,7 +26,11 @@ public class Feed extends FeedFile implements FlattrThing, ImageResource {
public static final String TYPE_RSS091 = "rss";
public static final String TYPE_ATOM1 = "atom";
- private String title;
+ /* title as defined by the feed */
+ private String feedTitle;
+ /* custom title set by the user */
+ private String customTitle;
+
/**
* Contains 'id'-element in Atom feed.
*/
@@ -92,13 +96,14 @@ public class Feed extends FeedFile implements FlattrThing, ImageResource {
/**
* This constructor is used for restoring a feed from the database.
*/
- public Feed(long id, String lastUpdate, String title, String link, String description, String paymentLink,
+ public Feed(long id, String lastUpdate, String title, String customTitle, String link, String description, String paymentLink,
String author, String language, String type, String feedIdentifier, FeedImage image, String fileUrl,
String downloadUrl, boolean downloaded, FlattrStatus status, boolean paged, String nextPageLink,
String filter, boolean lastUpdateFailed) {
super(fileUrl, downloadUrl, downloaded);
this.id = id;
- this.title = title;
+ this.feedTitle = title;
+ this.customTitle = customTitle;
this.lastUpdate = lastUpdate;
this.link = link;
this.description = description;
@@ -126,7 +131,7 @@ public class Feed extends FeedFile implements FlattrThing, ImageResource {
public Feed(long id, String lastUpdate, String title, String link, String description, String paymentLink,
String author, String language, String type, String feedIdentifier, FeedImage image, String fileUrl,
String downloadUrl, boolean downloaded) {
- this(id, lastUpdate, title, link, description, paymentLink, author, language, type, feedIdentifier, image,
+ this(id, lastUpdate, title, null, link, description, paymentLink, author, language, type, feedIdentifier, image,
fileUrl, downloadUrl, downloaded, new FlattrStatus(), false, null, null, false);
}
@@ -154,7 +159,7 @@ public class Feed extends FeedFile implements FlattrThing, ImageResource {
*/
public Feed(String url, String lastUpdate, String title) {
this(url, lastUpdate);
- this.title = title;
+ this.feedTitle = title;
this.flattrStatus = new FlattrStatus();
}
@@ -171,6 +176,7 @@ public class Feed extends FeedFile implements FlattrThing, ImageResource {
int indexId = cursor.getColumnIndex(PodDBAdapter.KEY_ID);
int indexLastUpdate = cursor.getColumnIndex(PodDBAdapter.KEY_LASTUPDATE);
int indexTitle = cursor.getColumnIndex(PodDBAdapter.KEY_TITLE);
+ int indexCustomTitle = cursor.getColumnIndex(PodDBAdapter.KEY_CUSTOM_TITLE);
int indexLink = cursor.getColumnIndex(PodDBAdapter.KEY_LINK);
int indexDescription = cursor.getColumnIndex(PodDBAdapter.KEY_DESCRIPTION);
int indexPaymentLink = cursor.getColumnIndex(PodDBAdapter.KEY_PAYMENT_LINK);
@@ -191,6 +197,7 @@ public class Feed extends FeedFile implements FlattrThing, ImageResource {
cursor.getLong(indexId),
cursor.getString(indexLastUpdate),
cursor.getString(indexTitle),
+ cursor.getString(indexCustomTitle),
cursor.getString(indexLink),
cursor.getString(indexDescription),
cursor.getString(indexPaymentLink),
@@ -268,8 +275,8 @@ public class Feed extends FeedFile implements FlattrThing, ImageResource {
return feedIdentifier;
} else if (download_url != null && !download_url.isEmpty()) {
return download_url;
- } else if (title != null && !title.isEmpty()) {
- return title;
+ } else if (feedTitle != null && !feedTitle.isEmpty()) {
+ return feedTitle;
} else {
return link;
}
@@ -277,8 +284,8 @@ public class Feed extends FeedFile implements FlattrThing, ImageResource {
@Override
public String getHumanReadableIdentifier() {
- if (title != null) {
- return title;
+ if (feedTitle != null) {
+ return feedTitle;
} else {
return download_url;
}
@@ -287,8 +294,8 @@ public class Feed extends FeedFile implements FlattrThing, ImageResource {
public void updateFromOther(Feed other) {
// don't update feed's download_url, we do that manually if redirected
// see AntennapodHttpClient
- if (other.title != null) {
- title = other.title;
+ if (other.feedTitle != null) {
+ feedTitle = other.feedTitle;
}
if (other.feedIdentifier != null) {
feedIdentifier = other.feedIdentifier;
@@ -323,7 +330,7 @@ public class Feed extends FeedFile implements FlattrThing, ImageResource {
if (super.compareWithOther(other)) {
return true;
}
- if (!TextUtils.equals(title, other.title)) {
+ if (!TextUtils.equals(feedTitle, other.feedTitle)) {
return true;
}
if (other.feedIdentifier != null) {
@@ -384,11 +391,28 @@ public class Feed extends FeedFile implements FlattrThing, ImageResource {
}
public String getTitle() {
- return title;
+ return !TextUtils.isEmpty(customTitle) ? customTitle : feedTitle;
}
public void setTitle(String title) {
- this.title = title;
+ this.feedTitle = title;
+ }
+
+ public String getFeedTitle() {
+ return this.feedTitle;
+ }
+
+ @Nullable
+ public String getCustomTitle() {
+ return this.customTitle;
+ }
+
+ public void setCustomTitle(String customTitle) {
+ if(customTitle == null || customTitle.equals(feedTitle)) {
+ this.customTitle = null;
+ } else {
+ this.customTitle = customTitle;
+ }
}
public String getLink() {
diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java
index 2481645e0..ee7a738d0 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java
@@ -1,6 +1,7 @@
package de.danoeh.antennapod.core.feed;
import android.database.Cursor;
+import android.support.annotation.Nullable;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
@@ -269,6 +270,7 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr
}
}
+ @Nullable
public FeedMedia getMedia() {
return media;
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilter.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilter.java
index 9d8f4adf8..200153876 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilter.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilter.java
@@ -18,6 +18,7 @@ public class FeedItemFilter {
private boolean showNotQueued = false;
private boolean showDownloaded = false;
private boolean showNotDownloaded = false;
+ private boolean showHasMedia = false;
public FeedItemFilter(String properties) {
this(TextUtils.split(properties, ","));
@@ -49,6 +50,9 @@ public class FeedItemFilter {
case "not_downloaded":
showNotDownloaded = true;
break;
+ case "has_media":
+ showHasMedia = true;
+ break;
}
}
}
@@ -82,6 +86,8 @@ public class FeedItemFilter {
if (showDownloaded && !downloaded) continue;
if (showNotDownloaded && downloaded) continue;
+ if (showHasMedia && !item.hasMedia()) continue;
+
// If the item reaches here, it meets all criteria
result.add(item);
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/glide/ApOkHttpUrlLoader.java b/core/src/main/java/de/danoeh/antennapod/core/glide/ApOkHttpUrlLoader.java
index 762e92286..8ca9faa0d 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/glide/ApOkHttpUrlLoader.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/glide/ApOkHttpUrlLoader.java
@@ -4,15 +4,12 @@ import android.content.Context;
import android.text.TextUtils;
import android.util.Log;
-import com.bumptech.glide.integration.okhttp.OkHttpStreamFetcher;
+import com.bumptech.glide.integration.okhttp3.OkHttpStreamFetcher;
import com.bumptech.glide.load.data.DataFetcher;
import com.bumptech.glide.load.model.GenericLoaderFactory;
import com.bumptech.glide.load.model.GlideUrl;
import com.bumptech.glide.load.model.ModelLoader;
import com.bumptech.glide.load.model.ModelLoaderFactory;
-import com.squareup.okhttp.Interceptor;
-import com.squareup.okhttp.OkHttpClient;
-import com.squareup.okhttp.Response;
import java.io.IOException;
import java.io.InputStream;
@@ -22,9 +19,13 @@ import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
import de.danoeh.antennapod.core.service.download.HttpDownloader;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.util.NetworkUtils;
+import okhttp3.Interceptor;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
/**
- * @see com.bumptech.glide.integration.okhttp.OkHttpUrlLoader
+ * @see com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
*/
public class ApOkHttpUrlLoader implements ModelLoader<String, InputStream> {
@@ -42,9 +43,10 @@ public class ApOkHttpUrlLoader implements ModelLoader<String, InputStream> {
if (internalClient == null) {
synchronized (Factory.class) {
if (internalClient == null) {
- internalClient = AntennapodHttpClient.newHttpClient();
- internalClient.interceptors().add(new NetworkAllowanceInterceptor());
- internalClient.interceptors().add(new BasicAuthenticationInterceptor());
+ OkHttpClient.Builder builder = AntennapodHttpClient.newBuilder();
+ builder.interceptors().add(new NetworkAllowanceInterceptor());
+ builder.interceptors().add(new BasicAuthenticationInterceptor());
+ internalClient = builder.build();
}
}
}
@@ -113,8 +115,8 @@ public class ApOkHttpUrlLoader implements ModelLoader<String, InputStream> {
@Override
public Response intercept(Chain chain) throws IOException {
- com.squareup.okhttp.Request request = chain.request();
- String url = request.urlString();
+ Request request = chain.request();
+ String url = request.url().toString();
String authentication = DBReader.getImageAuthentication(url);
if(TextUtils.isEmpty(authentication)) {
@@ -125,7 +127,7 @@ public class ApOkHttpUrlLoader implements ModelLoader<String, InputStream> {
// add authentication
String[] auth = authentication.split(":");
String credentials = HttpDownloader.encodeCredentials(auth[0], auth[1], "ISO-8859-1");
- com.squareup.okhttp.Request newRequest = request
+ Request newRequest = request
.newBuilder()
.addHeader("Authorization", credentials)
.build();
diff --git a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetService.java b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetService.java
index 9f716e546..0218b103a 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetService.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetService.java
@@ -1,15 +1,6 @@
package de.danoeh.antennapod.core.gpoddernet;
import android.support.annotation.NonNull;
-
-import com.squareup.okhttp.Credentials;
-import com.squareup.okhttp.MediaType;
-import com.squareup.okhttp.OkHttpClient;
-import com.squareup.okhttp.Request;
-import com.squareup.okhttp.RequestBody;
-import com.squareup.okhttp.Response;
-import com.squareup.okhttp.ResponseBody;
-
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
@@ -38,6 +29,13 @@ import de.danoeh.antennapod.core.gpoddernet.model.GpodnetTag;
import de.danoeh.antennapod.core.gpoddernet.model.GpodnetUploadChangesResponse;
import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
+import okhttp3.Credentials;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import okhttp3.ResponseBody;
/**
* Communicates with the gpodder.net service.
@@ -570,15 +568,7 @@ public class GpodnetService {
e.printStackTrace();
throw new GpodnetServiceException(e);
} finally {
- if (response != null && body != null) {
- try {
- body.close();
- } catch (IOException e) {
- e.printStackTrace();
- throw new GpodnetServiceException(e);
- }
- }
-
+ body.close();
}
return responseString;
}
@@ -605,12 +595,7 @@ public class GpodnetService {
throw new GpodnetServiceException(e);
} finally {
if (body != null) {
- try {
- body.close();
- } catch (IOException e) {
- e.printStackTrace();
- throw new GpodnetServiceException(e);
- }
+ body.close();
}
}
return result;
@@ -619,12 +604,7 @@ public class GpodnetService {
private String getStringFromResponseBody(@NonNull ResponseBody body)
throws GpodnetServiceException {
ByteArrayOutputStream outputStream;
- int contentLength = 0;
- try {
- contentLength = (int) body.contentLength();
- } catch (IOException ignore) {
- // ignore
- }
+ int contentLength = (int) body.contentLength();
if (contentLength > 0) {
outputStream = new ByteArrayOutputStream(contentLength);
} else {
diff --git a/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlSymbols.java b/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlSymbols.java
deleted file mode 100644
index c973713cb..000000000
--- a/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlSymbols.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package de.danoeh.antennapod.core.opml;
-
-/** Contains symbols for reading and writing OPML documents. */
-public final class OpmlSymbols {
-
- public static final String OPML = "opml";
- public static final String BODY = "body";
- public static final String OUTLINE = "outline";
- public static final String TEXT = "text";
- public static final String XMLURL = "xmlUrl";
- public static final String HTMLURL = "htmlUrl";
- public static final String TYPE = "type";
- public static final String VERSION = "version";
- public static final String HEAD = "head";
- public static final String TITLE = "title";
- public static final String DATE_CREATED = "dateCreated";
-
- private OpmlSymbols() {
-
- }
-
-}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/preferences/SleepTimerPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/preferences/SleepTimerPreferences.java
new file mode 100644
index 000000000..b7ed890f5
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/preferences/SleepTimerPreferences.java
@@ -0,0 +1,80 @@
+package de.danoeh.antennapod.core.preferences;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.support.annotation.NonNull;
+import android.util.Log;
+
+import java.util.concurrent.TimeUnit;
+
+public class SleepTimerPreferences {
+
+ private static final String TAG = "SleepTimerPreferences";
+
+ private static final String PREF_NAME = "SleepTimerDialog";
+ private static final String PREF_VALUE = "LastValue";
+ private static final String PREF_TIME_UNIT = "LastTimeUnit";
+ private static final String PREF_VIBRATE = "Vibrate";
+ private static final String PREF_SHAKE_TO_RESET = "ShakeToReset";
+ private static final String PREF_AUTO_ENABLE = "AutoEnable";
+
+ private static final TimeUnit[] UNITS = { TimeUnit.SECONDS, TimeUnit.MINUTES, TimeUnit.HOURS };
+
+ private static final String DEFAULT_VALUE = "15";
+ private static final int DEFAULT_TIME_UNIT = 1;
+
+ private static Context context;
+ private static SharedPreferences prefs;
+
+ /**
+ * Sets up the UserPreferences class.
+ *
+ * @throws IllegalArgumentException if context is null
+ */
+ public static void init(@NonNull Context context) {
+ Log.d(TAG, "Creating new instance of SleepTimerPreferences");
+ SleepTimerPreferences.prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
+ }
+
+ public static void setLastTimer(String value, int timeUnit) {
+ prefs.edit().putString(PREF_VALUE, value).putInt(PREF_TIME_UNIT, timeUnit).apply();
+ }
+
+ public static String lastTimerValue() {
+ return prefs.getString(PREF_VALUE, DEFAULT_VALUE);
+ }
+
+ public static int lastTimerTimeUnit() {
+ return prefs.getInt(PREF_TIME_UNIT, DEFAULT_TIME_UNIT);
+ }
+
+ public static long timerMillis() {
+ long value = Long.parseLong(lastTimerValue());
+ return UNITS[lastTimerTimeUnit()].toMillis(value);
+ }
+
+ public static void setVibrate(boolean vibrate) {
+ prefs.edit().putBoolean(PREF_VIBRATE, vibrate).apply();
+ }
+
+ public static boolean vibrate() {
+ return prefs.getBoolean(PREF_VIBRATE, true);
+ }
+
+ public static void setShakeToReset(boolean shakeToReset) {
+ prefs.edit().putBoolean(PREF_SHAKE_TO_RESET, shakeToReset).apply();
+ }
+
+ public static boolean shakeToReset() {
+ return prefs.getBoolean(PREF_SHAKE_TO_RESET, true);
+ }
+
+ public static void setAutoEnable(boolean autoEnable) {
+ prefs.edit().putBoolean(PREF_AUTO_ENABLE, autoEnable).apply();
+ }
+
+ public static boolean autoEnable() {
+ return prefs.getBoolean(PREF_AUTO_ENABLE, false);
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java
index 04b06d148..016b6c446 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java
@@ -56,7 +56,6 @@ public class UserPreferences {
public static final String PREF_LOCKSCREEN_BACKGROUND = "prefLockscreenBackground";
public static final String PREF_SHOW_DOWNLOAD_REPORT = "prefShowDownloadReport";
-
// Queue
public static final String PREF_QUEUE_ADD_TO_FRONT = "prefQueueAddToFront";
@@ -65,6 +64,7 @@ public class UserPreferences {
public static final String PREF_UNPAUSE_ON_HEADSET_RECONNECT = "prefUnpauseOnHeadsetReconnect";
public static final String PREF_UNPAUSE_ON_BLUETOOTH_RECONNECT = "prefUnpauseOnBluetoothReconnect";
public static final String PREF_HARDWARE_FOWARD_BUTTON_SKIPS = "prefHardwareForwardButtonSkips";
+ public static final String PREF_HARDWARE_PREVIOUS_BUTTON_RESTARTS = "prefHardwarePreviousButtonRestarts";
public static final String PREF_FOLLOW_QUEUE = "prefFollowQueue";
public static final String PREF_SKIP_KEEPS_EPISODE = "prefSkipKeepsEpisode";
public static final String PREF_AUTO_DELETE = "prefAutoDelete";
@@ -74,6 +74,7 @@ public class UserPreferences {
public static final String PREF_RESUME_AFTER_CALL = "prefResumeAfterCall";
// Network
+ public static final String PREF_ENQUEUE_DOWNLOADED = "prefEnqueueDownloaded";
public static final String PREF_UPDATE_INTERVAL = "prefAutoUpdateIntervall";
public static final String PREF_MOBILE_UPDATE = "prefMobileUpdate";
public static final String PREF_EPISODE_CLEANUP = "prefEpisodeCleanup";
@@ -122,13 +123,14 @@ public class UserPreferences {
private static final int NOTIFICATION_BUTTON_FAST_FORWARD = 1;
private static final int NOTIFICATION_BUTTON_SKIP = 2;
private static int EPISODE_CACHE_SIZE_UNLIMITED = -1;
- public static int FEED_ORDER_COUNTER = 0;
- public static int FEED_ORDER_ALPHABETICAL = 1;
- public static int FEED_ORDER_LAST_UPDATE = 2;
- public static int FEED_COUNTER_SHOW_NEW_UNPLAYED_SUM = 0;
- public static int FEED_COUNTER_SHOW_NEW = 1;
- public static int FEED_COUNTER_SHOW_UNPLAYED = 2;
- public static int FEED_COUNTER_SHOW_NONE = 3;
+ public static final int FEED_ORDER_COUNTER = 0;
+ public static final int FEED_ORDER_ALPHABETICAL = 1;
+ public static final int FEED_ORDER_LAST_UPDATE = 2;
+ public static final int FEED_COUNTER_SHOW_NEW_UNPLAYED_SUM = 0;
+ public static final int FEED_COUNTER_SHOW_NEW = 1;
+ public static final int FEED_COUNTER_SHOW_UNPLAYED = 2;
+ public static final int FEED_COUNTER_SHOW_NONE = 3;
+ public static final int FEED_COUNTER_SHOW_DOWNLOADED = 4;
private static Context context;
private static SharedPreferences prefs;
@@ -257,11 +259,10 @@ public class UserPreferences {
return prefs.getBoolean(PREF_SHOW_DOWNLOAD_REPORT, true);
}
- /**
- * Returns {@code true} if new queue elements are added to the front
- *
- * @return {@code true} if new queue elements are added to the front; {@code false} otherwise
- */
+ public static boolean enqueueDownloadedEpisodes() {
+ return prefs.getBoolean(PREF_ENQUEUE_DOWNLOADED, true);
+ }
+
public static boolean enqueueAtFront() {
return prefs.getBoolean(PREF_QUEUE_ADD_TO_FRONT, false);
}
@@ -282,6 +283,10 @@ public class UserPreferences {
return prefs.getBoolean(PREF_HARDWARE_FOWARD_BUTTON_SKIPS, false);
}
+ public static boolean shouldHardwarePreviousButtonRestart() {
+ return prefs.getBoolean(PREF_HARDWARE_PREVIOUS_BUTTON_RESTARTS, false);
+ }
+
public static boolean isFollowQueue() {
return prefs.getBoolean(PREF_FOLLOW_QUEUE, true);
@@ -332,7 +337,10 @@ public class UserPreferences {
}
-
+ /*
+ * Returns update interval in milliseconds; value 0 means that auto update is disabled
+ * or feeds are updated at a certain time of day
+ */
public static long getUpdateInterval() {
String updateInterval = prefs.getString(PREF_UPDATE_INTERVAL, "0");
if(!updateInterval.contains(":")) {
@@ -754,12 +762,12 @@ public class UserPreferences {
if (timeOfDay.length == 2) {
restartUpdateTimeOfDayAlarm(timeOfDay[0], timeOfDay[1]);
} else {
- long hours = getUpdateInterval();
- long startTrigger = hours;
+ long milliseconds = getUpdateInterval();
+ long startTrigger = milliseconds;
if (now) {
startTrigger = TimeUnit.SECONDS.toMillis(10);
}
- restartUpdateIntervalAlarm(startTrigger, hours);
+ restartUpdateIntervalAlarm(startTrigger, milliseconds);
}
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/AntennapodHttpClient.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/AntennapodHttpClient.java
index 5dd1e2dfa..eb28050f0 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/download/AntennapodHttpClient.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/AntennapodHttpClient.java
@@ -5,12 +5,6 @@ import android.support.annotation.NonNull;
import android.text.TextUtils;
import android.util.Log;
-import com.squareup.okhttp.Credentials;
-import com.squareup.okhttp.OkHttpClient;
-import com.squareup.okhttp.Request;
-import com.squareup.okhttp.Response;
-import com.squareup.okhttp.internal.http.StatusLine;
-
import java.io.IOException;
import java.net.CookieManager;
import java.net.CookiePolicy;
@@ -20,16 +14,27 @@ import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.Socket;
import java.net.SocketAddress;
-import java.net.URL;
import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509TrustManager;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.storage.DBWriter;
+import okhttp3.Credentials;
+import okhttp3.HttpUrl;
+import okhttp3.JavaNetCookieJar;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
+import okhttp3.internal.http.StatusLine;
/**
* Provides access to a HttpClient singleton.
@@ -50,13 +55,13 @@ public class AntennapodHttpClient {
*/
public static synchronized OkHttpClient getHttpClient() {
if (httpClient == null) {
- httpClient = newHttpClient();
+ httpClient = newBuilder().build();
}
return httpClient;
}
public static synchronized void reinit() {
- httpClient = newHttpClient();
+ httpClient = newBuilder().build();
}
/**
@@ -67,33 +72,33 @@ public class AntennapodHttpClient {
* @return http client
*/
@NonNull
- public static OkHttpClient newHttpClient() {
+ public static OkHttpClient.Builder newBuilder() {
Log.d(TAG, "Creating new instance of HTTP client");
System.setProperty("http.maxConnections", String.valueOf(MAX_CONNECTIONS));
- OkHttpClient client = new OkHttpClient();
+ OkHttpClient.Builder builder = new OkHttpClient.Builder();
// detect 301 Moved permanently and 308 Permanent Redirect
- client.networkInterceptors().add(chain -> {
+ builder.networkInterceptors().add(chain -> {
Request request = chain.request();
Response response = chain.proceed(request);
if (response.code() == HttpURLConnection.HTTP_MOVED_PERM ||
response.code() == StatusLine.HTTP_PERM_REDIRECT) {
String location = response.header("Location");
if (location.startsWith("/")) { // URL is not absolute, but relative
- URL url = request.url();
- location = url.getProtocol() + "://" + url.getHost() + location;
+ HttpUrl url = request.url();
+ location = url.scheme() + "://" + url.host() + location;
} else if (!location.toLowerCase().startsWith("http://") &&
!location.toLowerCase().startsWith("https://")) {
// Reference is relative to current path
- URL url = request.url();
- String path = url.getPath();
+ HttpUrl url = request.url();
+ String path = url.encodedPath();
String newPath = path.substring(0, path.lastIndexOf("/") + 1) + location;
- location = url.getProtocol() + "://" + url.getHost() + newPath;
+ location = url.scheme() + "://" + url.host() + newPath;
}
try {
- DBWriter.updateFeedDownloadURL(request.urlString(), location).get();
+ DBWriter.updateFeedDownloadURL(request.url().toString(), location).get();
} catch (Exception e) {
Log.e(TAG, Log.getStackTraceString(e));
}
@@ -104,26 +109,26 @@ public class AntennapodHttpClient {
// set cookie handler
CookieManager cm = new CookieManager();
cm.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
- client.setCookieHandler(cm);
+ builder.cookieJar(new JavaNetCookieJar(cm));
// set timeouts
- client.setConnectTimeout(CONNECTION_TIMEOUT, TimeUnit.MILLISECONDS);
- client.setReadTimeout(READ_TIMEOUT, TimeUnit.MILLISECONDS);
- client.setWriteTimeout(READ_TIMEOUT, TimeUnit.MILLISECONDS);
+ builder.connectTimeout(CONNECTION_TIMEOUT, TimeUnit.MILLISECONDS);
+ builder.readTimeout(READ_TIMEOUT, TimeUnit.MILLISECONDS);
+ builder.writeTimeout(READ_TIMEOUT, TimeUnit.MILLISECONDS);
// configure redirects
- client.setFollowRedirects(true);
- client.setFollowSslRedirects(true);
+ builder.followRedirects(true);
+ builder.followSslRedirects(true);
ProxyConfig config = UserPreferences.getProxyConfig();
if (config.type != Proxy.Type.DIRECT) {
int port = config.port > 0 ? config.port : ProxyConfig.DEFAULT_PORT;
SocketAddress address = InetSocketAddress.createUnresolved(config.host, port);
Proxy proxy = new Proxy(config.type, address);
- client.setProxy(proxy);
+ builder.proxy(proxy);
if (!TextUtils.isEmpty(config.username)) {
String credentials = Credentials.basic(config.username, config.password);
- client.interceptors().add(chain -> {
+ builder.interceptors().add(chain -> {
Request request = chain.request().newBuilder()
.header("Proxy-Authorization", credentials).build();
return chain.proceed(request);
@@ -131,9 +136,9 @@ public class AntennapodHttpClient {
}
}
if(16 <= Build.VERSION.SDK_INT && Build.VERSION.SDK_INT < 21) {
- client.setSslSocketFactory(new CustomSslSocketFactory());
+ builder.sslSocketFactory(new CustomSslSocketFactory(), trustManager());
}
- return client;
+ return builder;
}
/**
@@ -146,6 +151,23 @@ public class AntennapodHttpClient {
}
}
+ private static X509TrustManager trustManager() {
+ try {
+ TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
+ TrustManagerFactory.getDefaultAlgorithm());
+ trustManagerFactory.init((KeyStore) null);
+ TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
+ if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
+ throw new IllegalStateException("Unexpected default trust managers:"
+ + Arrays.toString(trustManagers));
+ }
+ return (X509TrustManager) trustManagers[0];
+ } catch (Exception e) {
+ Log.e(TAG, Log.getStackTraceString(e));
+ return null;
+ }
+ }
+
private static class CustomSslSocketFactory extends SSLSocketFactory {
private SSLSocketFactory factory;
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java
index 6bd9fc83c..9ac459394 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java
@@ -842,11 +842,14 @@ public class DownloadService extends Service {
successful = false;
reason = DownloadError.ERROR_PARSER_EXCEPTION;
reasonDetailed = e.getMessage();
+ } finally {
+ File feedFile = new File(request.getDestination());
+ if(feedFile.exists()) {
+ boolean deleted = feedFile.delete();
+ Log.d(TAG, "Deletion of file '" + feedFile.getAbsolutePath() + "' " + (deleted ? "successful" : "FAILED"));
+ }
}
- // cleanup();
-
-
if (successful) {
// we create a 'successful' download log if the feed's last refresh failed
List<DownloadStatus> log = DBReader.getFeedDownloadLog(feed);
@@ -1048,7 +1051,8 @@ public class DownloadService extends Service {
DBWriter.setFeedMedia(media).get();
- if (item != null && !DBTasks.isInQueue(DownloadService.this, item.getId())) {
+ if (item != null && UserPreferences.enqueueDownloadedEpisodes() &&
+ !DBTasks.isInQueue(DownloadService.this, item.getId())) {
DBWriter.addQueueItem(DownloadService.this, item).get();
}
} catch (ExecutionException | InterruptedException e) {
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java
index 99456b756..b409a419a 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java
@@ -2,21 +2,7 @@ package de.danoeh.antennapod.core.service.download;
import android.text.TextUtils;
import android.util.Log;
-import com.squareup.okhttp.Interceptor;
-import com.squareup.okhttp.OkHttpClient;
-import com.squareup.okhttp.Protocol;
-import com.squareup.okhttp.Request;
-import com.squareup.okhttp.Response;
-import com.squareup.okhttp.ResponseBody;
-import de.danoeh.antennapod.core.ClientConfig;
-import de.danoeh.antennapod.core.R;
-import de.danoeh.antennapod.core.feed.FeedImage;
-import de.danoeh.antennapod.core.feed.FeedMedia;
-import de.danoeh.antennapod.core.util.DateUtils;
-import de.danoeh.antennapod.core.util.DownloadError;
-import de.danoeh.antennapod.core.util.StorageUtils;
-import de.danoeh.antennapod.core.util.URIUtil;
-import okio.ByteString;
+
import org.apache.commons.io.IOUtils;
import java.io.BufferedInputStream;
@@ -32,6 +18,22 @@ import java.net.UnknownHostException;
import java.util.Collections;
import java.util.Date;
+import de.danoeh.antennapod.core.ClientConfig;
+import de.danoeh.antennapod.core.R;
+import de.danoeh.antennapod.core.feed.FeedImage;
+import de.danoeh.antennapod.core.feed.FeedMedia;
+import de.danoeh.antennapod.core.util.DateUtils;
+import de.danoeh.antennapod.core.util.DownloadError;
+import de.danoeh.antennapod.core.util.StorageUtils;
+import de.danoeh.antennapod.core.util.URIUtil;
+import okhttp3.Interceptor;
+import okhttp3.OkHttpClient;
+import okhttp3.Protocol;
+import okhttp3.Request;
+import okhttp3.Response;
+import okhttp3.ResponseBody;
+import okio.ByteString;
+
public class HttpDownloader extends Downloader {
private static final String TAG = "HttpDownloader";
@@ -57,8 +59,9 @@ public class HttpDownloader extends Downloader {
}
}
- OkHttpClient httpClient = AntennapodHttpClient.newHttpClient();
- httpClient.interceptors().add(new BasicAuthorizationInterceptor(request));
+ OkHttpClient.Builder httpClientBuilder = AntennapodHttpClient.newBuilder();
+ httpClientBuilder.interceptors().add(new BasicAuthorizationInterceptor(request));
+ OkHttpClient httpClient = httpClientBuilder.build();
RandomAccessFile out = null;
InputStream connection;
ResponseBody responseBody = null;
@@ -103,7 +106,9 @@ public class HttpDownloader extends Downloader {
} catch (IOException e) {
Log.e(TAG, e.toString());
if (e.getMessage().contains("PROTOCOL_ERROR")) {
- httpClient.setProtocols(Collections.singletonList(Protocol.HTTP_1_1));
+ httpClient = httpClient.newBuilder()
+ .protocols(Collections.singletonList(Protocol.HTTP_1_1))
+ .build();
response = httpClient.newCall(httpReq.build()).execute();
} else {
throw e;
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java
index 0871758d0..f395dfb32 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java
@@ -146,7 +146,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
if (!media.getIdentifier().equals(playable.getIdentifier())) {
final Playable oldMedia = media;
- executor.submit(() -> callback.onPostPlayback(oldMedia, false, true));
+ executor.submit(() -> callback.onPostPlayback(oldMedia, false, false, true));
}
setPlayerStatus(PlayerStatus.INDETERMINATE, null);
@@ -383,6 +383,9 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
statusBeforeSeeking = playerStatus;
setPlayerStatus(PlayerStatus.SEEKING, media, getPosition());
mediaPlayer.seekTo(t);
+ if (statusBeforeSeeking == PlayerStatus.PREPARED) {
+ media.setPosition(t);
+ }
try {
seekLatch.await(3, TimeUnit.SECONDS);
} catch (InterruptedException e) {
@@ -755,7 +758,8 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
@Override
- protected Future<?> endPlayback(final boolean wasSkipped, final boolean shouldContinue, final boolean toStoppedState) {
+ protected Future<?> endPlayback(final boolean hasEnded, final boolean wasSkipped,
+ final boolean shouldContinue, final boolean toStoppedState) {
return executor.submit(() -> {
playerLock.lock();
releaseWifiLockIfNecessary();
@@ -813,7 +817,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
}
final boolean hasNext = nextMedia != null;
- executor.submit(() -> callback.onPostPlayback(currentMedia, !wasSkipped, hasNext));
+ executor.submit(() -> callback.onPostPlayback(currentMedia, hasEnded, wasSkipped, hasNext));
} else if (isPlaying) {
callback.onPlaybackPause(currentMedia, currentMedia.getPosition());
}
@@ -848,27 +852,24 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
}
private IPlayer setMediaPlayerListeners(IPlayer mp) {
- if (mp != null && media != null) {
- if (media.getMediaType() == MediaType.AUDIO) {
- ((AudioPlayer) mp)
- .setOnCompletionListener(audioCompletionListener);
- ((AudioPlayer) mp)
- .setOnSeekCompleteListener(audioSeekCompleteListener);
- ((AudioPlayer) mp).setOnErrorListener(audioErrorListener);
- ((AudioPlayer) mp)
- .setOnBufferingUpdateListener(audioBufferingUpdateListener);
- ((AudioPlayer) mp).setOnInfoListener(audioInfoListener);
- ((AudioPlayer) mp).setOnSpeedAdjustmentAvailableChangedListener(audioSetSpeedAbilityListener);
- } else {
- ((VideoPlayer) mp)
- .setOnCompletionListener(videoCompletionListener);
- ((VideoPlayer) mp)
- .setOnSeekCompleteListener(videoSeekCompleteListener);
- ((VideoPlayer) mp).setOnErrorListener(videoErrorListener);
- ((VideoPlayer) mp)
- .setOnBufferingUpdateListener(videoBufferingUpdateListener);
- ((VideoPlayer) mp).setOnInfoListener(videoInfoListener);
- }
+ if (mp == null || media == null) {
+ return mp;
+ }
+ if (media.getMediaType() == MediaType.VIDEO) {
+ VideoPlayer vp = (VideoPlayer) mp;
+ vp.setOnCompletionListener(videoCompletionListener);
+ vp.setOnSeekCompleteListener(videoSeekCompleteListener);
+ vp.setOnErrorListener(videoErrorListener);
+ vp.setOnBufferingUpdateListener(videoBufferingUpdateListener);
+ vp.setOnInfoListener(videoInfoListener);
+ } else {
+ AudioPlayer ap = (AudioPlayer) mp;
+ ap.setOnCompletionListener(audioCompletionListener);
+ ap.setOnSeekCompleteListener(audioSeekCompleteListener);
+ ap.setOnErrorListener(audioErrorListener);
+ ap.setOnBufferingUpdateListener(audioBufferingUpdateListener);
+ ap.setOnInfoListener(audioInfoListener);
+ ap.setOnSpeedAdjustmentAvailableChangedListener(audioSetSpeedAbilityListener);
}
return mp;
}
@@ -880,7 +881,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
mp -> genericOnCompletion();
private void genericOnCompletion() {
- endPlayback(false, true, true);
+ endPlayback(true, false, true, true);
}
private final MediaPlayer.OnBufferingUpdateListener audioBufferingUpdateListener =
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java
index f425d7181..ebab2cbec 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java
@@ -47,12 +47,14 @@ import java.util.List;
import de.danoeh.antennapod.core.ClientConfig;
import de.danoeh.antennapod.core.R;
+import de.danoeh.antennapod.core.event.MessageEvent;
import de.danoeh.antennapod.core.feed.Chapter;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.feed.MediaType;
import de.danoeh.antennapod.core.glide.ApGlideSettings;
import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
+import de.danoeh.antennapod.core.preferences.SleepTimerPreferences;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.receiver.MediaButtonReceiver;
import de.danoeh.antennapod.core.storage.DBReader;
@@ -62,6 +64,7 @@ import de.danoeh.antennapod.core.util.IntList;
import de.danoeh.antennapod.core.util.QueueAccess;
import de.danoeh.antennapod.core.util.playback.ExternalMedia;
import de.danoeh.antennapod.core.util.playback.Playable;
+import de.greenrobot.event.EventBus;
/**
* Controls the MediaPlayer that plays a FeedMedia-file
@@ -298,7 +301,10 @@ public class PlaybackService extends MediaBrowserServiceCompat {
List<MediaSessionCompat.QueueItem> queueItems = new ArrayList<>();
try {
for (FeedItem feedItem : taskManager.getQueue()) {
- queueItems.add(new MediaSessionCompat.QueueItem(feedItem.getMedia().getMediaItem().getDescription(), feedItem.getId()));
+ if(feedItem.getMedia() != null) {
+ MediaDescriptionCompat mediaDescription = feedItem.getMedia().getMediaItem().getDescription();
+ queueItems.add(new MediaSessionCompat.QueueItem(mediaDescription, feedItem.getId()));
+ }
}
mediaSession.setQueue(queueItems);
} catch (InterruptedException e) {
@@ -478,6 +484,14 @@ public class PlaybackService extends MediaBrowserServiceCompat {
mediaPlayer.seekDelta(UserPreferences.getFastForwardSecs() * 1000);
break;
case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
+ if(UserPreferences.shouldHardwarePreviousButtonRestart()) {
+ // user wants to restart current episode
+ mediaPlayer.seekTo(0);
+ } else {
+ // user wants to rewind current episode
+ mediaPlayer.seekDelta(-UserPreferences.getRewindSecs() * 1000);
+ }
+ break;
case KeyEvent.KEYCODE_MEDIA_REWIND:
mediaPlayer.seekDelta(-UserPreferences.getRewindSecs() * 1000);
break;
@@ -597,6 +611,12 @@ public class PlaybackService extends MediaBrowserServiceCompat {
writePlayerStatusPlaybackPreferences();
setupNotification(newInfo);
started = true;
+ // set sleep timer if auto-enabled
+ if(newInfo.oldPlayerStatus != null && newInfo.oldPlayerStatus != PlayerStatus.SEEKING &&
+ SleepTimerPreferences.autoEnable() && !sleepTimerActive()) {
+ setSleepTimer(SleepTimerPreferences.timerMillis(), SleepTimerPreferences.shakeToReset(),
+ SleepTimerPreferences.vibrate());
+ }
break;
case ERROR:
@@ -669,8 +689,9 @@ public class PlaybackService extends MediaBrowserServiceCompat {
}
@Override
- public void onPostPlayback(@NonNull Playable media, boolean ended, boolean playingNext) {
- PlaybackService.this.onPostPlayback(media, ended, playingNext);
+ public void onPostPlayback(@NonNull Playable media, boolean ended, boolean skipped,
+ boolean playingNext) {
+ PlaybackService.this.onPostPlayback(media, ended, skipped, playingNext);
}
@Override
@@ -768,16 +789,20 @@ public class PlaybackService extends MediaBrowserServiceCompat {
* Even though these tasks aren't supposed to be resource intensive, a good practice is to
* usually call this method on a background thread.
*
- * @param playable the media object that was playing. It is assumed that its position property
- * was updated before this method was called.
- * @param ended if true, it signals that {@param playable} was played until its end.
- * In such case, the position property of the media becomes irrelevant for most of
- * the tasks (although it's still a good practice to keep it accurate).
- * @param playingNext if true, it means another media object is being loaded in place of this one.
+ * @param playable the media object that was playing. It is assumed that its position
+ * property was updated before this method was called.
+ * @param ended if true, it signals that {@param playable} was played until its end.
+ * In such case, the position property of the media becomes irrelevant for
+ * most of the tasks (although it's still a good practice to keep it
+ * accurate).
+ * @param skipped if the user pressed a skip >| button.
+ * @param playingNext if true, it means another media object is being loaded in place of this
+ * one.
* Instances when we'd set it to false would be when we're not following the
* queue or when the queue has ended.
*/
- private void onPostPlayback(final Playable playable, boolean ended, boolean playingNext) {
+ private void onPostPlayback(final Playable playable, boolean ended, boolean skipped,
+ boolean playingNext) {
if (playable == null) {
Log.e(TAG, "Cannot do post-playback processing: media was null");
return;
@@ -808,7 +833,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
if (item != null) {
if (ended || smartMarkAsPlayed ||
- !UserPreferences.shouldSkipKeepEpisode()) {
+ (skipped && !UserPreferences.shouldSkipKeepEpisode())) {
// only mark the item as played if we're not keeping it anyways
DBWriter.markItemPlayed(item, FeedItem.PLAYED, ended);
try {
@@ -829,7 +854,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
}
}
- if (ended || playingNext) {
+ if (ended || skipped || playingNext) {
DBWriter.addItemToPlaybackHistory(media);
}
}
@@ -838,11 +863,14 @@ public class PlaybackService extends MediaBrowserServiceCompat {
Log.d(TAG, "Setting sleep timer to " + Long.toString(waitingTime) + " milliseconds");
taskManager.setSleepTimer(waitingTime, shakeToReset, vibrate);
sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0);
+ EventBus.getDefault().post(new MessageEvent(getString(R.string.sleep_timer_enabled_label),
+ () -> disableSleepTimer()));
}
public void disableSleepTimer() {
taskManager.disableSleepTimer();
sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0);
+ EventBus.getDefault().post(new MessageEvent(getString(R.string.sleep_timer_disabled_label)));
}
private void writePlaybackPreferencesNoMediaPlaying() {
@@ -996,13 +1024,37 @@ public class PlaybackService extends MediaBrowserServiceCompat {
state = PlaybackStateCompat.STATE_NONE;
}
sessionState.setState(state, mediaPlayer.getPosition(), mediaPlayer.getPlaybackSpeed());
- sessionState.setActions(PlaybackStateCompat.ACTION_PLAY_PAUSE
+ long capabilities = PlaybackStateCompat.ACTION_PLAY_PAUSE
| PlaybackStateCompat.ACTION_REWIND
| PlaybackStateCompat.ACTION_FAST_FORWARD
- | PlaybackStateCompat.ACTION_SKIP_TO_NEXT);
+ | PlaybackStateCompat.ACTION_SKIP_TO_NEXT;
+
+ if (useSkipToPreviousForRewindInLockscreen()) {
+ // Workaround to fool Android so that Lockscreen will expose a skip-to-previous button,
+ // which will be used for rewind.
+ // The workaround is used for pre Lollipop (Androidv5) devices.
+ // For Androidv5+, lockscreen widges are really notifications (compact),
+ // with an independent codepath
+ //
+ // @see #sessionCallback in the backing callback, skipToPrevious implementation
+ // is actually the same as rewind. So no new inconsistency is created.
+ // @see #setupNotification() for the method to create Androidv5+ lockscreen UI
+ // with notification (compact)
+ capabilities = capabilities | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS;
+ }
+
+ sessionState.setActions(capabilities);
mediaSession.setPlaybackState(sessionState.build());
}
+ private static boolean useSkipToPreviousForRewindInLockscreen() {
+ // showRewindOnCompactNotification() corresponds to the "Set Lockscreen Buttons"
+ // Settings in UI.
+ // Hence, from user perspective, he/she is setting the buttons for Lockscreen
+ return ( UserPreferences.showRewindOnCompactNotification() &&
+ (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) );
+ }
+
/**
* Used by updateMediaSessionMetadata to load notification data in another thread.
*/
@@ -1050,7 +1102,13 @@ public class PlaybackService extends MediaBrowserServiceCompat {
mediaSession.setSessionActivity(PendingIntent.getActivity(this, 0,
PlaybackService.getPlayerActivityIntent(this),
PendingIntent.FLAG_UPDATE_CURRENT));
- mediaSession.setMetadata(builder.build());
+ try {
+ mediaSession.setMetadata(builder.build());
+ } catch (OutOfMemoryError e) {
+ Log.e(TAG, "Setting media session metadata", e);
+ builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, null);
+ mediaSession.setMetadata(builder.build());
+ }
}
};
@@ -1285,7 +1343,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
if (info.playable != null) {
Intent i = new Intent(whatChanged);
- i.putExtra("id", 1);
+ i.putExtra("id", 1L);
i.putExtra("artist", "");
i.putExtra("album", info.playable.getFeedTitle());
i.putExtra("track", info.playable.getEpisodeTitle());
@@ -1294,8 +1352,8 @@ public class PlaybackService extends MediaBrowserServiceCompat {
if (queue != null) {
i.putExtra("ListSize", queue.size());
}
- i.putExtra("duration", info.playable.getDuration());
- i.putExtra("position", info.playable.getPosition());
+ i.putExtra("duration", (long) info.playable.getDuration());
+ i.putExtra("position", (long) info.playable.getPosition());
sendBroadcast(i);
}
}
@@ -1375,7 +1433,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
* Pauses playback if PREF_PAUSE_ON_HEADSET_DISCONNECT was set to true.
*/
private void pauseIfPauseOnDisconnect() {
- if (UserPreferences.isPauseOnHeadsetDisconnect()) {
+ if (UserPreferences.isPauseOnHeadsetDisconnect() && !isCasting()) {
if (mediaPlayer.getPlayerStatus() == PlayerStatus.PLAYING) {
transientPause = true;
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java
index aec059ca0..393019fd1 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java
@@ -29,9 +29,10 @@ public abstract class PlaybackServiceMediaPlayer {
/**
* Return value of some PSMP methods if the method call failed.
*/
- public static final int INVALID_TIME = -1;
+ static final int INVALID_TIME = -1;
- protected volatile PlayerStatus playerStatus;
+ volatile PlayerStatus oldPlayerStatus;
+ volatile PlayerStatus playerStatus;
/**
* A wifi-lock that is acquired if the media file is being streamed.
@@ -41,8 +42,8 @@ public abstract class PlaybackServiceMediaPlayer {
protected final PSMPCallback callback;
protected final Context context;
- public PlaybackServiceMediaPlayer(@NonNull Context context,
- @NonNull PSMPCallback callback){
+ PlaybackServiceMediaPlayer(@NonNull Context context,
+ @NonNull PSMPCallback callback){
this.context = context;
this.callback = callback;
@@ -204,7 +205,7 @@ public abstract class PlaybackServiceMediaPlayer {
* @return The PSMPInfo object.
*/
public final synchronized PSMPInfo getPSMPInfo() {
- return new PSMPInfo(playerStatus, getPlayable());
+ return new PSMPInfo(oldPlayerStatus, playerStatus, getPlayable());
}
/**
@@ -228,32 +229,35 @@ public abstract class PlaybackServiceMediaPlayer {
protected abstract void setPlayable(Playable playable);
public void skip() {
- endPlayback(true, true, true);
+ endPlayback(false, true, true, true);
}
/**
* Ends playback of current media (if any) and moves into INDETERMINATE state, unless
* {@param toStoppedState} is set to true, in which case it moves into STOPPED state.
*
- * @see #endPlayback(boolean, boolean, boolean)
+ * @see #endPlayback(boolean, boolean, boolean, boolean)
*/
public Future<?> stopPlayback(boolean toStoppedState) {
- return endPlayback(true, false, toStoppedState);
+ return endPlayback(false, false, false, toStoppedState);
}
/**
* Internal method that handles end of playback.
*
- * Currently, it has 4 use cases:
+ * Currently, it has 5 use cases:
* <ul>
- * <li>Media playback has completed: call with (false, true, true)</li>
- * <li>User asks to skip to next episode: call with (true, true, true)</li>
- * <li>Stopping the media player: call with (true, false, true)</li>
- * <li>We want to change the media player implementation: call with (true, false, false)</li>
+ * <li>Media playback has completed: call with (true, false, true, true)</li>
+ * <li>User asks to skip to next episode: call with (false, true, true, true)</li>
+ * <li>Skipping to next episode due to playback error: call with (false, false, true, true)</li>
+ * <li>Stopping the media player: call with (false, false, false, true)</li>
+ * <li>We want to change the media player implementation: call with (false, false, false, false)</li>
* </ul>
*
- * @param wasSkipped If true, we assume the current media's playback has ended, for
+ * @param hasEnded If true, we assume the current media's playback has ended, for
* purposes of post playback processing.
+ * @param wasSkipped Whether the user chose to skip the episode (by pressing the skip
+ * button).
* @param shouldContinue If true, the media player should try to load, and possibly play,
* the next item, based on the user preferences and whether such item
* exists.
@@ -264,14 +268,15 @@ public abstract class PlaybackServiceMediaPlayer {
*
* @return a Future, just for the purpose of tracking its execution.
*/
- protected abstract Future<?> endPlayback(boolean wasSkipped, boolean shouldContinue, boolean toStoppedState);
+ protected abstract Future<?> endPlayback(boolean hasEnded, boolean wasSkipped,
+ boolean shouldContinue, boolean toStoppedState);
/**
* @return {@code true} if the WifiLock feature should be used, {@code false} otherwise.
*/
protected abstract boolean shouldLockWifi();
- protected final synchronized void acquireWifiLockIfNecessary() {
+ final synchronized void acquireWifiLockIfNecessary() {
if (shouldLockWifi()) {
if (wifiLock == null) {
wifiLock = ((WifiManager) context.getSystemService(Context.WIFI_SERVICE))
@@ -282,7 +287,7 @@ public abstract class PlaybackServiceMediaPlayer {
}
}
- protected final synchronized void releaseWifiLockIfNecessary() {
+ final synchronized void releaseWifiLockIfNecessary() {
if (wifiLock != null && wifiLock.isHeld()) {
wifiLock.release();
}
@@ -303,29 +308,28 @@ public abstract class PlaybackServiceMediaPlayer {
* @param position The position to be set to the current Playable object in case playback started or paused.
* Will be ignored if given the value of {@link #INVALID_TIME}.
*/
- protected final synchronized void setPlayerStatus(@NonNull PlayerStatus newStatus, Playable newMedia, int position) {
+ final synchronized void setPlayerStatus(@NonNull PlayerStatus newStatus, Playable newMedia, int position) {
Log.d(TAG, this.getClass().getSimpleName() + ": Setting player status to " + newStatus);
- PlayerStatus oldStatus = playerStatus;
-
+ this.oldPlayerStatus = playerStatus;
this.playerStatus = newStatus;
setPlayable(newMedia);
if (newMedia != null && newStatus != PlayerStatus.INDETERMINATE) {
- if (oldStatus == PlayerStatus.PLAYING && newStatus != PlayerStatus.PLAYING) {
+ if (oldPlayerStatus == PlayerStatus.PLAYING && newStatus != PlayerStatus.PLAYING) {
callback.onPlaybackPause(newMedia, position);
- } else if (oldStatus != PlayerStatus.PLAYING && newStatus == PlayerStatus.PLAYING) {
+ } else if (oldPlayerStatus != PlayerStatus.PLAYING && newStatus == PlayerStatus.PLAYING) {
callback.onPlaybackStart(newMedia, position);
}
}
- callback.statusChanged(new PSMPInfo(playerStatus, getPlayable()));
+ callback.statusChanged(new PSMPInfo(oldPlayerStatus, playerStatus, getPlayable()));
}
/**
* @see #setPlayerStatus(PlayerStatus, Playable, int)
*/
- protected final void setPlayerStatus(@NonNull PlayerStatus newStatus, Playable newMedia) {
+ final void setPlayerStatus(@NonNull PlayerStatus newStatus, Playable newMedia) {
setPlayerStatus(newStatus, newMedia, INVALID_TIME);
}
@@ -346,7 +350,7 @@ public abstract class PlaybackServiceMediaPlayer {
boolean onMediaPlayerError(Object inObj, int what, int extra);
- void onPostPlayback(@NonNull Playable media, boolean ended, boolean playingNext);
+ void onPostPlayback(@NonNull Playable media, boolean ended, boolean skipped, boolean playingNext);
void onPlaybackStart(@NonNull Playable playable, int position);
@@ -361,10 +365,12 @@ public abstract class PlaybackServiceMediaPlayer {
* Holds information about a PSMP object.
*/
public static class PSMPInfo {
+ public PlayerStatus oldPlayerStatus;
public PlayerStatus playerStatus;
public Playable playable;
- public PSMPInfo(PlayerStatus playerStatus, Playable playable) {
+ PSMPInfo(PlayerStatus oldPlayerStatus, PlayerStatus playerStatus, Playable playable) {
+ this.oldPlayerStatus = oldPlayerStatus;
this.playerStatus = playerStatus;
this.playable = playable;
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java
index 9a7b2c852..1d57d902c 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java
@@ -1063,7 +1063,7 @@ public final class DBReader {
// reverse natural order: podcast with most unplayed episodes first
return -1;
} else if(counterLhs == counterRhs) {
- return lhs.getTitle().compareTo(rhs.getTitle());
+ return lhs.getTitle().compareToIgnoreCase(rhs.getTitle());
} else {
return 1;
}
@@ -1077,7 +1077,7 @@ public final class DBReader {
} else if(t2 == null) {
return -1;
} else {
- return t1.toLowerCase().compareTo(t2.toLowerCase());
+ return t1.compareToIgnoreCase(t2);
}
};
} else {
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java
index d17b815ff..4d442aac2 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java
@@ -2,6 +2,7 @@ package de.danoeh.antennapod.core.storage;
import android.content.Context;
import android.content.Intent;
+import android.content.SharedPreferences;
import android.database.Cursor;
import android.util.Log;
@@ -16,7 +17,7 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
-import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import de.danoeh.antennapod.core.ClientConfig;
@@ -27,21 +28,29 @@ import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.feed.FeedPreferences;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.GpodnetSyncService;
import de.danoeh.antennapod.core.service.download.DownloadStatus;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
+import de.danoeh.antennapod.core.util.Converter;
import de.danoeh.antennapod.core.util.DownloadError;
import de.danoeh.antennapod.core.util.LongList;
import de.danoeh.antennapod.core.util.comparator.FeedItemPubdateComparator;
import de.danoeh.antennapod.core.util.exception.MediaFileNotFoundException;
import de.danoeh.antennapod.core.util.flattr.FlattrUtils;
+import static android.content.Context.MODE_PRIVATE;
+import static android.provider.Contacts.SettingsColumns.KEY;
+
/**
* Provides methods for doing common tasks that use DBReader and DBWriter.
*/
public final class DBTasks {
private static final String TAG = "DBTasks";
+ public static final String PREF_NAME = "dbtasks";
+ private static final String PREF_LAST_REFRESH = "last_refresh";
+
/**
* Executor service used by the autodownloadUndownloadedEpisodes method.
*/
@@ -162,6 +171,9 @@ public final class DBTasks {
}
isRefreshing.set(false);
+ SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, MODE_PRIVATE);
+ prefs.edit().putLong(PREF_LAST_REFRESH, System.currentTimeMillis()).apply();
+
if (FlattrUtils.hasToken()) {
Log.d(TAG, "Flattring all pending things.");
new FlattrClickWorker(context).executeAsync(); // flattr pending things
@@ -313,6 +325,31 @@ public final class DBTasks {
DownloadRequester.getInstance().downloadFeed(context, f, loadAllPages, force);
}
+ /*
+ * Checks if the app should refresh all feeds, i.e. if the last auto refresh failed.
+ *
+ * The feeds are only refreshed if an update interval or time of day is set and the last
+ * (successful) refresh was before the last interval or more than a day ago, respectively.
+ */
+ public static void checkShouldRefreshFeeds(Context context) {
+ long interval = 0;
+ if(UserPreferences.getUpdateInterval() > 0) {
+ interval = UserPreferences.getUpdateInterval();
+ } else if(UserPreferences.getUpdateTimeOfDay().length > 0){
+ interval = TimeUnit.DAYS.toMillis(1);
+ }
+ if(interval == 0) { // auto refresh is disabled
+ return;
+ }
+ SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, MODE_PRIVATE);
+ long lastRefresh = prefs.getLong(PREF_LAST_REFRESH, 0);
+ Log.d(TAG, "last refresh: " + Converter.getDurationStringLocalized(context,
+ System.currentTimeMillis() - lastRefresh) + " ago");
+ if(lastRefresh <= System.currentTimeMillis() - interval) {
+ DBTasks.refreshAllFeeds(context, null);
+ }
+ }
+
/**
* Notifies the database about a missing FeedMedia file. This method will correct the FeedMedia object's values in the
* DB and send a FeedUpdateBroadcast.
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java
index 8e007019f..563d80da0 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java
@@ -884,6 +884,17 @@ public class DBWriter {
});
}
+ public static Future<?> setFeedCustomTitle(Feed feed) {
+ return dbExec.submit(() -> {
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.setFeedCustomTitle(feed.getId(), feed.getCustomTitle());
+ adapter.close();
+ EventDistributor.getInstance().sendFeedUpdateBroadcast();
+ });
+ }
+
+
/**
* format an url for querying the database
* (postfix a / and apply percent-encoding)
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/FeedSearcher.java b/core/src/main/java/de/danoeh/antennapod/core/storage/FeedSearcher.java
index 9d136273c..48e574069 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/storage/FeedSearcher.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/FeedSearcher.java
@@ -4,7 +4,9 @@ import android.content.Context;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
@@ -51,15 +53,17 @@ public class FeedSearcher {
task.run();
}
try {
+ Set<Long> set = new HashSet<>();
+
for (int i = 0; i < tasks.size(); i++) {
FutureTask<List<FeedItem>> task = tasks.get(i);
List<FeedItem> items = task.get();
for (FeedItem item : items) {
- if (result.isEmpty() || !isDuplicate(result, item)) {
+ if (!set.contains(item.getId())) { // to prevent duplicate results
result.add(new SearchResult(item, values[i], subtitles[i]));
+ set.add(item.getId());
}
}
-
}
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
@@ -67,20 +71,4 @@ public class FeedSearcher {
Collections.sort(result, new SearchResultValueComparator());
return result;
}
-
- /**
- * Determines if the feed item is already in the search result list.
- *
- * @param result list of search results
- * @param item feed item to validate
- * @return true if the feed item is already in the results
- */
- private static boolean isDuplicate(List<SearchResult> result, FeedItem item) {
- for (SearchResult resultItem : result) {
- if (resultItem.getComponent().getId() == item.getId()) {
- return true;
- }
- }
- return false;
- }
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java b/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java
index 7631d26d5..ff003550c 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java
@@ -42,12 +42,12 @@ import de.greenrobot.event.EventBus;
public class PodDBAdapter {
private static final String TAG = "PodDBAdapter";
- public static final String DATABASE_NAME = "Antennapod.db";
+ private static final String DATABASE_NAME = "Antennapod.db";
/**
* Maximum number of arguments for IN-operator.
*/
- public static final int IN_OPERATOR_MAXIMUM = 800;
+ private static final int IN_OPERATOR_MAXIMUM = 800;
/**
* Maximum number of entries per search request.
@@ -57,6 +57,7 @@ public class PodDBAdapter {
// Key-constants
public static final String KEY_ID = "id";
public static final String KEY_TITLE = "title";
+ public static final String KEY_CUSTOM_TITLE = "custom_title";
public static final String KEY_NAME = "name";
public static final String KEY_LINK = "link";
public static final String KEY_DESCRIPTION = "description";
@@ -109,14 +110,14 @@ public class PodDBAdapter {
public static final String KEY_EXCLUDE_FILTER = "exclude_filter";
// Table names
- public static final String TABLE_NAME_FEEDS = "Feeds";
- public static final String TABLE_NAME_FEED_ITEMS = "FeedItems";
- public static final String TABLE_NAME_FEED_IMAGES = "FeedImages";
- public static final String TABLE_NAME_FEED_MEDIA = "FeedMedia";
- public static final String TABLE_NAME_DOWNLOAD_LOG = "DownloadLog";
- public static final String TABLE_NAME_QUEUE = "Queue";
- public static final String TABLE_NAME_SIMPLECHAPTERS = "SimpleChapters";
- public static final String TABLE_NAME_FAVORITES = "Favorites";
+ private static final String TABLE_NAME_FEEDS = "Feeds";
+ private static final String TABLE_NAME_FEED_ITEMS = "FeedItems";
+ private static final String TABLE_NAME_FEED_IMAGES = "FeedImages";
+ private static final String TABLE_NAME_FEED_MEDIA = "FeedMedia";
+ private static final String TABLE_NAME_DOWNLOAD_LOG = "DownloadLog";
+ private static final String TABLE_NAME_QUEUE = "Queue";
+ private static final String TABLE_NAME_SIMPLECHAPTERS = "SimpleChapters";
+ private static final String TABLE_NAME_FAVORITES = "Favorites";
// SQL Statements for creating new tables
private static final String TABLE_PRIMARY_KEY = KEY_ID
@@ -124,7 +125,7 @@ public class PodDBAdapter {
public static final String CREATE_TABLE_FEEDS = "CREATE TABLE "
+ TABLE_NAME_FEEDS + " (" + TABLE_PRIMARY_KEY + KEY_TITLE
- + " TEXT," + KEY_FILE_URL + " TEXT," + KEY_DOWNLOAD_URL + " TEXT,"
+ + " TEXT," + KEY_CUSTOM_TITLE + " TEXT," + KEY_FILE_URL + " TEXT," + KEY_DOWNLOAD_URL + " TEXT,"
+ KEY_DOWNLOADED + " INTEGER," + KEY_LINK + " TEXT,"
+ KEY_DESCRIPTION + " TEXT," + KEY_PAYMENT_LINK + " TEXT,"
+ KEY_LASTUPDATE + " TEXT," + KEY_LANGUAGE + " TEXT," + KEY_AUTHOR
@@ -225,6 +226,7 @@ public class PodDBAdapter {
private static final String[] FEED_SEL_STD = {
TABLE_NAME_FEEDS + "." + KEY_ID,
TABLE_NAME_FEEDS + "." + KEY_TITLE,
+ TABLE_NAME_FEEDS + "." + KEY_CUSTOM_TITLE,
TABLE_NAME_FEEDS + "." + KEY_FILE_URL,
TABLE_NAME_FEEDS + "." + KEY_DOWNLOAD_URL,
TABLE_NAME_FEEDS + "." + KEY_DOWNLOADED,
@@ -363,7 +365,7 @@ public class PodDBAdapter {
*/
public long setFeed(Feed feed) {
ContentValues values = new ContentValues();
- values.put(KEY_TITLE, feed.getTitle());
+ values.put(KEY_TITLE, feed.getFeedTitle());
values.put(KEY_LINK, feed.getLink());
values.put(KEY_DESCRIPTION, feed.getDescription());
values.put(KEY_PAYMENT_LINK, feed.getPaymentLink());
@@ -854,6 +856,12 @@ public class PodDBAdapter {
db.execSQL(sql);
}
+ void setFeedCustomTitle(long feedId, String customTitle) {
+ ContentValues values = new ContentValues();
+ values.put(KEY_CUSTOM_TITLE, customTitle);
+ db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?", new String[]{String.valueOf(feedId)});
+ }
+
/**
* Inserts or updates a download status.
*/
@@ -1436,15 +1444,24 @@ public class PodDBAdapter {
public final LongIntMap getFeedCounters(long... feedIds) {
int setting = UserPreferences.getFeedCounterSetting();
String whereRead;
- if(setting == UserPreferences.FEED_COUNTER_SHOW_NEW_UNPLAYED_SUM) {
- whereRead = "(" + KEY_READ + "=" + FeedItem.NEW
- + " OR " + KEY_READ + "=" + FeedItem.UNPLAYED + ")";
- } else if(setting == UserPreferences.FEED_COUNTER_SHOW_NEW) {
- whereRead = KEY_READ + "=" + FeedItem.NEW;
- } else if(setting == UserPreferences.FEED_COUNTER_SHOW_UNPLAYED) {
- whereRead = KEY_READ + "=" + FeedItem.UNPLAYED;
- } else { // NONE
- return new LongIntMap(0);
+ switch(setting) {
+ case UserPreferences.FEED_COUNTER_SHOW_NEW_UNPLAYED_SUM:
+ whereRead = "(" + KEY_READ + "=" + FeedItem.NEW +
+ " OR " + KEY_READ + "=" + FeedItem.UNPLAYED + ")";
+ break;
+ case UserPreferences.FEED_COUNTER_SHOW_NEW:
+ whereRead = KEY_READ + "=" + FeedItem.NEW;
+ break;
+ case UserPreferences.FEED_COUNTER_SHOW_UNPLAYED:
+ whereRead = KEY_READ + "=" + FeedItem.UNPLAYED;
+ break;
+ case UserPreferences.FEED_COUNTER_SHOW_DOWNLOADED:
+ whereRead = KEY_DOWNLOADED + "=1";
+ break;
+ case UserPreferences.FEED_COUNTER_SHOW_NONE:
+ // deliberate fall-through
+ default: // NONE
+ return new LongIntMap(0);
}
// work around TextUtils.join wanting only boxed items
@@ -1459,8 +1476,10 @@ public class PodDBAdapter {
builder.deleteCharAt(builder.length() - 1);
}
- final String query = "SELECT " + KEY_FEED + ", COUNT(" + KEY_ID + ") AS count "
+ final String query = "SELECT " + KEY_FEED + ", COUNT(" + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + ") AS count "
+ " FROM " + TABLE_NAME_FEED_ITEMS
+ + " LEFT JOIN " + TABLE_NAME_FEED_MEDIA + " ON "
+ + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + "=" + TABLE_NAME_FEED_MEDIA + "." + KEY_FEEDITEM
+ " WHERE " + KEY_FEED + " IN (" + builder.toString() + ") "
+ " AND " + whereRead + " GROUP BY " + KEY_FEED;
@@ -1617,7 +1636,7 @@ public class PodDBAdapter {
*/
private static class PodDBHelper extends SQLiteOpenHelper {
- private static final int VERSION = 1050004;
+ private static final int VERSION = 1060200;
private Context context;
@@ -1913,6 +1932,10 @@ public class PodDBAdapter {
db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEEDS
+" SET " + PodDBAdapter.KEY_LASTUPDATE + "=NULL");
}
+ if(oldVersion < 1060200) {
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS
+ + " ADD COLUMN " + PodDBAdapter.KEY_CUSTOM_TITLE + " TEXT");
+ }
EventBus.getDefault().post(ProgressEvent.end());
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/SyndHandler.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/SyndHandler.java
index 47503dee4..ae91c0743 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/SyndHandler.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/SyndHandler.java
@@ -6,7 +6,6 @@ import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
-import de.danoeh.antennapod.core.BuildConfig;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.syndication.namespace.NSContent;
import de.danoeh.antennapod.core.syndication.namespace.NSDublinCore;
@@ -86,34 +85,28 @@ public class SyndHandler extends DefaultHandler {
state.defaultNamespaces.push(new NSAtom());
} else if (prefix.equals(NSAtom.NSTAG)) {
state.namespaces.put(uri, new NSAtom());
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Recognized Atom namespace");
+ Log.d(TAG, "Recognized Atom namespace");
}
} else if (uri.equals(NSContent.NSURI)
&& prefix.equals(NSContent.NSTAG)) {
state.namespaces.put(uri, new NSContent());
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Recognized Content namespace");
+ Log.d(TAG, "Recognized Content namespace");
} else if (uri.equals(NSITunes.NSURI)
&& prefix.equals(NSITunes.NSTAG)) {
state.namespaces.put(uri, new NSITunes());
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Recognized ITunes namespace");
+ Log.d(TAG, "Recognized ITunes namespace");
} else if (uri.equals(NSSimpleChapters.NSURI)
&& prefix.matches(NSSimpleChapters.NSTAG)) {
state.namespaces.put(uri, new NSSimpleChapters());
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Recognized SimpleChapters namespace");
+ Log.d(TAG, "Recognized SimpleChapters namespace");
} else if (uri.equals(NSMedia.NSURI)
&& prefix.equals(NSMedia.NSTAG)) {
state.namespaces.put(uri, new NSMedia());
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Recognized media namespace");
+ Log.d(TAG, "Recognized media namespace");
} else if (uri.equals(NSDublinCore.NSURI)
&& prefix.equals(NSDublinCore.NSTAG)) {
state.namespaces.put(uri, new NSDublinCore());
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Recognized DublinCore namespace");
+ Log.d(TAG, "Recognized DublinCore namespace");
}
}
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSMedia.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSMedia.java
index 839e2ae0c..f2cfc2e57 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSMedia.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSMedia.java
@@ -26,6 +26,11 @@ public class NSMedia extends Namespace {
private static final String MIME_TYPE = "type";
private static final String DURATION = "duration";
private static final String DEFAULT = "isDefault";
+ private static final String MEDIUM = "medium";
+
+ private static final String MEDIUM_IMAGE = "image";
+ private static final String MEDIUM_AUDIO = "audio";
+ private static final String MEDIUM_VIDEO = "video";
private static final String IMAGE = "thumbnail";
private static final String IMAGE_URL = "url";
@@ -40,20 +45,31 @@ public class NSMedia extends Namespace {
String url = attributes.getValue(DOWNLOAD_URL);
String type = attributes.getValue(MIME_TYPE);
String defaultStr = attributes.getValue(DEFAULT);
- boolean validType;
+ String medium = attributes.getValue(MEDIUM);
+ boolean validTypeMedia = false;
+ boolean validTypeImage = false;
boolean isDefault = "true".equals(defaultStr);
- if (SyndTypeUtils.enclosureTypeValid(type)) {
- validType = true;
+ if (MEDIUM_AUDIO.equals(medium) || MEDIUM_VIDEO.equals(medium)) {
+ validTypeMedia = true;
+ } else if (MEDIUM_IMAGE.equals(medium)) {
+ validTypeImage = true;
} else {
- type = SyndTypeUtils.getValidMimeTypeFromUrl(url);
- validType = type != null;
+ if (type == null) {
+ type = SyndTypeUtils.getMimeTypeFromUrl(url);
+ }
+
+ if (SyndTypeUtils.enclosureTypeValid(type)) {
+ validTypeMedia = true;
+ } else if (SyndTypeUtils.imageTypeValid(type)) {
+ validTypeImage = true;
+ }
}
if (state.getCurrentItem() != null &&
(state.getCurrentItem().getMedia() == null || isDefault) &&
- url != null && validType) {
+ url != null && validTypeMedia) {
long size = 0;
String sizeStr = attributes.getValue(SIZE);
try {
@@ -77,6 +93,12 @@ public class NSMedia extends Namespace {
media.setDuration(durationMs);
}
state.getCurrentItem().setMedia(media);
+ } else if (state.getCurrentItem() != null && url != null && validTypeImage) {
+ FeedImage image = new FeedImage();
+ image.setDownload_url(url);
+ image.setOwner(state.getCurrentItem());
+
+ state.getCurrentItem().setImage(image);
}
} else if (IMAGE.equals(localName)) {
String url = attributes.getValue(IMAGE_URL);
diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSRSS20.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSRSS20.java
index a5ca9d6f4..3d752df76 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSRSS20.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSRSS20.java
@@ -20,25 +20,27 @@ import de.danoeh.antennapod.core.util.DateUtils;
*
*/
public class NSRSS20 extends Namespace {
- private static final String TAG = "NSRSS20";
- public static final String NSTAG = "rss";
- public static final String NSURI = "";
- public static final String CHANNEL = "channel";
+ private static final String TAG = "NSRSS20";
+
+ private static final String NSTAG = "rss";
+ private static final String NSURI = "";
+
+ public static final String CHANNEL = "channel";
public static final String ITEM = "item";
- public static final String GUID = "guid";
- public static final String TITLE = "title";
- public static final String LINK = "link";
- public static final String DESCR = "description";
- public static final String PUBDATE = "pubDate";
- public static final String ENCLOSURE = "enclosure";
- public static final String IMAGE = "image";
- public static final String URL = "url";
- public static final String LANGUAGE = "language";
-
- public static final String ENC_URL = "url";
- public static final String ENC_LEN = "length";
- public static final String ENC_TYPE = "type";
+ private static final String GUID = "guid";
+ private static final String TITLE = "title";
+ private static final String LINK = "link";
+ private static final String DESCR = "description";
+ private static final String PUBDATE = "pubDate";
+ private static final String ENCLOSURE = "enclosure";
+ private static final String IMAGE = "image";
+ private static final String URL = "url";
+ private static final String LANGUAGE = "language";
+
+ private static final String ENC_URL = "url";
+ private static final String ENC_LEN = "length";
+ private static final String ENC_TYPE = "type";
@Override
public SyndElement handleElementStart(String localName, HandlerState state,
@@ -51,15 +53,16 @@ public class NSRSS20 extends Namespace {
} else if (ENCLOSURE.equals(localName)) {
String type = attributes.getValue(ENC_TYPE);
String url = attributes.getValue(ENC_URL);
- boolean validType;
- if(SyndTypeUtils.enclosureTypeValid(type)) {
- validType = true;
- } else {
- type = type = SyndTypeUtils.getValidMimeTypeFromUrl(url);
- validType = type != null;
- }
- if (state.getCurrentItem() != null && state.getCurrentItem().getMedia() == null &&
- validType) {
+
+ boolean validType = SyndTypeUtils.enclosureTypeValid(type);
+ if(!validType) {
+ type = SyndTypeUtils.getMimeTypeFromUrl(url);
+ validType = SyndTypeUtils.enclosureTypeValid(type);
+ }
+
+ boolean validUrl = !TextUtils.isEmpty(url);
+ if (state.getCurrentItem() != null && state.getCurrentItem().getMedia() == null &&
+ validType && validUrl) {
long size = 0;
try {
size = Long.parseLong(attributes.getValue(ENC_LEN));
@@ -70,8 +73,8 @@ public class NSRSS20 extends Namespace {
} catch (NumberFormatException e) {
Log.d(TAG, "Length attribute could not be parsed.");
}
- state.getCurrentItem().setMedia(
- new FeedMedia(state.getCurrentItem(), url, size, type));
+ FeedMedia media = new FeedMedia(state.getCurrentItem(), url, size, type);
+ state.getCurrentItem().setMedia(media);
}
} else if (IMAGE.equals(localName)) {
diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/NSAtom.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/NSAtom.java
index 52b05aa98..cfb20d578 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/NSAtom.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/NSAtom.java
@@ -30,6 +30,7 @@ public class NSAtom extends Namespace {
private static final String AUTHOR = "author";
private static final String AUTHOR_NAME = "name";
private static final String CONTENT = "content";
+ private static final String SUMMARY = "summary";
private static final String IMAGE_LOGO = "logo";
private static final String IMAGE_ICON = "icon";
private static final String SUBTITLE = "subtitle";
@@ -44,6 +45,7 @@ public class NSAtom extends Namespace {
private static final String LINK_LENGTH = "length";
// rel-values
private static final String LINK_REL_ALTERNATE = "alternate";
+ private static final String LINK_REL_ARCHIVES = "archives";
private static final String LINK_REL_ENCLOSURE = "enclosure";
private static final String LINK_REL_PAYMENT = "payment";
private static final String LINK_REL_RELATED = "related";
@@ -59,8 +61,8 @@ public class NSAtom extends Namespace {
/**
* Regexp to test whether an Element is a Text Element.
*/
- private static final String isText = TITLE + "|" + CONTENT + "|" + "|"
- + SUBTITLE;
+ private static final String isText = TITLE + "|" + CONTENT + "|"
+ + SUBTITLE + "|" + SUMMARY;
public static final String isFeed = FEED + "|" + NSRSS20.CHANNEL;
public static final String isFeedItem = ENTRY + "|" + NSRSS20.ITEM;
@@ -93,14 +95,12 @@ public class NSAtom extends Namespace {
Log.d(TAG, "Length attribute could not be parsed.");
}
String type = attributes.getValue(LINK_TYPE);
- boolean validType;
- if(SyndTypeUtils.enclosureTypeValid(type)) {
- validType = true;
- } else {
- type = SyndTypeUtils.getValidMimeTypeFromUrl(href);
- validType = type != null;
+
+ if (type == null) {
+ type = SyndTypeUtils.getMimeTypeFromUrl(href);
}
- if (validType) {
+
+ if(SyndTypeUtils.enclosureTypeValid(type)) {
FeedItem currItem = state.getCurrentItem();
if(currItem != null && !currItem.hasMedia()) {
currItem.setMedia(new FeedMedia(currItem, href, size, type));
@@ -129,6 +129,17 @@ public class NSAtom extends Namespace {
}
state.addAlternateFeedUrl(title, href);
}
+ } else if (LINK_REL_ARCHIVES.equals(rel) && state.getFeed() != null) {
+ String type = attributes.getValue(LINK_TYPE);
+ if (LINK_TYPE_ATOM.equals(type) || LINK_TYPE_RSS.equals(type)) {
+ String title = attributes.getValue(LINK_TITLE);
+ if (TextUtils.isEmpty(title)) {
+ title = href;
+ }
+ state.addAlternateFeedUrl(title, href);
+ } else if (LINK_TYPE_HTML.equals(type) || LINK_TYPE_XHTML.equals(type)) {
+ //A Link such as to a directory such as iTunes
+ }
} else if (LINK_REL_PAYMENT.equals(rel) && state.getFeed() != null) {
state.getFeed().setPaymentLink(href);
} else if (LINK_REL_NEXT.equals(rel) && state.getFeed() != null) {
@@ -191,6 +202,9 @@ public class NSAtom extends Namespace {
} else if (CONTENT.equals(top) && ENTRY.equals(second) && textElement != null &&
state.getCurrentItem() != null) {
state.getCurrentItem().setDescription(textElement.getProcessedContent());
+ } else if (SUMMARY.equals(top) && ENTRY.equals(second) && textElement != null &&
+ state.getCurrentItem() != null && state.getCurrentItem().getDescription() == null) {
+ state.getCurrentItem().setDescription(textElement.getProcessedContent());
} else if (UPDATED.equals(top) && ENTRY.equals(second) && state.getCurrentItem() != null &&
state.getCurrentItem().getPubDate() == null) {
state.getCurrentItem().setPubDate(DateUtils.parse(content));
@@ -200,9 +214,13 @@ public class NSAtom extends Namespace {
state.getFeed().setImage(new FeedImage(state.getFeed(), content, null));
} else if (IMAGE_ICON.equals(top) && state.getFeed() != null) {
state.getFeed().setImage(new FeedImage(state.getFeed(), content, null));
- } else if (AUTHOR.equals(second) && state.getFeed() != null) {
- if (AUTHOR_NAME.equals(top)) {
+ } else if (AUTHOR_NAME.equals(top) && AUTHOR.equals(second) &&
+ state.getFeed() != null && state.getCurrentItem() == null) {
+ String currentName = state.getFeed().getAuthor();
+ if (currentName == null) {
state.getFeed().setAuthor(content);
+ } else {
+ state.getFeed().setAuthor(currentName + ", " + content);
}
}
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndTypeUtils.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndTypeUtils.java
index d228b9ef7..1d564ab0e 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndTypeUtils.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndTypeUtils.java
@@ -1,13 +1,22 @@
package de.danoeh.antennapod.core.syndication.util;
+import android.text.TextUtils;
import android.webkit.MimeTypeMap;
+
import org.apache.commons.io.FilenameUtils;
+import java.util.Arrays;
+
/** Utility class for handling MIME-Types of enclosures */
public class SyndTypeUtils {
- private static final String VALID_MIMETYPE = "audio/.*" + "|" + "video/.*"
- + "|" + "application/ogg";
+ private static final String VALID_MEDIA_MIMETYPE = TextUtils.join("|", Arrays.asList(
+ "audio/.*",
+ "video/.*",
+ "application/ogg",
+ "application/octet-stream"));
+
+ private static final String VALID_IMAGE_MIMETYPE = "image/.*";
private SyndTypeUtils() {
@@ -17,9 +26,17 @@ public class SyndTypeUtils {
if (type == null) {
return false;
} else {
- return type.matches(VALID_MIMETYPE);
+ return type.matches(VALID_MEDIA_MIMETYPE);
}
}
+ public static boolean imageTypeValid(String type) {
+ if (type == null) {
+ return false;
+ } else {
+ return type.matches(VALID_IMAGE_MIMETYPE);
+ }
+ }
+
/**
* Should be used if mime-type of enclosure tag is not supported. This
@@ -27,15 +44,27 @@ public class SyndTypeUtils {
* the type is not supported, this method will return null.
*/
public static String getValidMimeTypeFromUrl(String url) {
- if (url != null) {
- String extension = FilenameUtils.getExtension(url);
- if (extension != null) {
- String type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
- if (type != null && enclosureTypeValid(type)) {
- return type;
- }
- }
+ String type = getMimeTypeFromUrl(url);
+ if (enclosureTypeValid(type)) {
+ return type;
+ } else {
+ return null;
}
- return null;
+ }
+
+ /**
+ * Should be used if mime-type of enclosure tag is not supported. This
+ * method will return the mime-type of the file extension.
+ */
+ public static String getMimeTypeFromUrl(String url) {
+ if (url == null) {
+ return null;
+ }
+ String extension = FilenameUtils.getExtension(url);
+ if (extension == null) {
+ return null;
+ }
+
+ return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
}
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java
index 43158c471..1717bde0e 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java
@@ -8,11 +8,6 @@ import android.net.wifi.WifiManager;
import android.support.v4.net.ConnectivityManagerCompat;
import android.text.TextUtils;
import android.util.Log;
-
-import com.squareup.okhttp.OkHttpClient;
-import com.squareup.okhttp.Request;
-import com.squareup.okhttp.Response;
-
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
@@ -22,6 +17,9 @@ import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
import de.danoeh.antennapod.core.storage.DBWriter;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
import rx.Observable;
import rx.Subscriber;
import rx.android.schedulers.AndroidSchedulers;
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/SearchResultValueComparator.java b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/SearchResultValueComparator.java
index b16e0949d..d23901a45 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/SearchResultValueComparator.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/SearchResultValueComparator.java
@@ -1,14 +1,27 @@
package de.danoeh.antennapod.core.util.comparator;
+import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.SearchResult;
import java.util.Comparator;
public class SearchResultValueComparator implements Comparator<SearchResult> {
+ /**
+ * Compare items based, first, on where they were found (ie. title, chapters, or show notes).
+ * If they were found in the same section, then compare based on the title, in lexicographic
+ * order. This is still not ideal since, for example, "#12 Example A" would be considered
+ * before "#8 Example B" due to the fact that "8" has a larger unicode value than "1"
+ */
@Override
public int compare(SearchResult lhs, SearchResult rhs) {
- return rhs.getValue() - lhs.getValue();
+ int value = rhs.getValue() - lhs.getValue();
+ if (value == 0 && lhs.getComponent() instanceof FeedItem && rhs.getComponent() instanceof FeedItem) {
+ String lhsTitle = ((FeedItem) lhs.getComponent()).getTitle();
+ String rhsTitle = ((FeedItem) rhs.getComponent()).getTitle();
+ return lhsTitle.compareTo(rhsTitle);
+ }
+ return value;
}
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java
index 13aadf027..6251cc4a0 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java
@@ -216,7 +216,7 @@ public abstract class PlaybackController {
Intent serviceIntent = new Intent(activity, PlaybackService.class);
serviceIntent.putExtra(PlaybackService.EXTRA_PLAYABLE, media);
serviceIntent.putExtra(PlaybackService.EXTRA_START_WHEN_PREPARED, false);
- serviceIntent.putExtra(PlaybackService.EXTRA_PREPARE_IMMEDIATELY, false);
+ serviceIntent.putExtra(PlaybackService.EXTRA_PREPARE_IMMEDIATELY, true);
boolean fileExists = media.localFileAvailable();
boolean lastIsStream = PlaybackPreferences.getCurrentEpisodeIsStream();
if (!fileExists && !lastIsStream && media instanceof FeedMedia) {
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/syndication/HtmlToPlainText.java b/core/src/main/java/de/danoeh/antennapod/core/util/syndication/HtmlToPlainText.java
new file mode 100644
index 000000000..bd40f398d
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/syndication/HtmlToPlainText.java
@@ -0,0 +1,89 @@
+package de.danoeh.antennapod.core.util.syndication;
+
+import org.jsoup.helper.StringUtil;
+import org.jsoup.nodes.Element;
+import org.jsoup.nodes.Node;
+import org.jsoup.nodes.TextNode;
+import org.jsoup.select.NodeTraversor;
+import org.jsoup.select.NodeVisitor;
+
+/**
+ * This class is based on <code>HtmlToPlainText</code> from jsoup's examples package.
+ *
+ * HTML to plain-text. This example program demonstrates the use of jsoup to convert HTML input to lightly-formatted
+ * plain-text. That is divergent from the general goal of jsoup's .text() methods, which is to get clean data from a
+ * scrape.
+ * <p>
+ * Note that this is a fairly simplistic formatter -- for real world use you'll want to embrace and extend.
+ * </p>
+ * <p>
+ * To invoke from the command line, assuming you've downloaded the jsoup jar to your current directory:</p>
+ * <p><code>java -cp jsoup.jar org.jsoup.examples.HtmlToPlainText url [selector]</code></p>
+ * where <i>url</i> is the URL to fetch, and <i>selector</i> is an optional CSS selector.
+ *
+ * @author Jonathan Hedley, jonathan@hedley.net
+ * @author AntennaPod open source community
+ */
+public class HtmlToPlainText {
+
+ /**
+ * Format an Element to plain-text
+ * @param element the root element to format
+ * @return formatted text
+ */
+ public String getPlainText(Element element) {
+ FormattingVisitor formatter = new FormattingVisitor();
+ NodeTraversor traversor = new NodeTraversor(formatter);
+ traversor.traverse(element); // walk the DOM, and call .head() and .tail() for each node
+
+ return formatter.toString();
+ }
+
+ // the formatting rules, implemented in a breadth-first DOM traverse
+ private class FormattingVisitor implements NodeVisitor {
+
+ private StringBuilder accum = new StringBuilder(); // holds the accumulated text
+
+ // hit when the node is first seen
+ public void head(Node node, int depth) {
+ String name = node.nodeName();
+ if (node instanceof TextNode) {
+ append(((TextNode) node).text()); // TextNodes carry all user-readable text in the DOM.
+ }
+ else if (name.equals("li")) {
+ append("\n * ");
+ }
+ else if (name.equals("dt")) {
+ append(" ");
+ }
+ else if (StringUtil.in(name, "p", "h1", "h2", "h3", "h4", "h5", "tr")) {
+ append("\n");
+ }
+ }
+
+ // hit when all of the node's children (if any) have been visited
+ public void tail(Node node, int depth) {
+ String name = node.nodeName();
+ if (StringUtil.in(name, "br", "dd", "dt", "p", "h1", "h2", "h3", "h4", "h5")) {
+ append("\n");
+ } else if (name.equals("a")) {
+ append(String.format(" <%s>", node.absUrl("href")));
+ }
+ }
+
+ // appends text to the string builder with a simple word wrap method
+ private void append(String text) {
+ if (text.equals(" ") &&
+ (accum.length() == 0 || StringUtil.in(accum.substring(accum.length() - 1), " ", "\n"))) {
+ return; // don't accumulate long runs of empty spaces
+ }
+
+ accum.append(text);
+ }
+
+ @Override
+ public String toString() {
+ return accum.toString();
+ }
+ }
+}