diff --git a/app/src/androidTest/java/de/test/antennapod/feed/FeedFilterTest.java b/app/src/androidTest/java/de/test/antennapod/feed/FeedFilterTest.java new file mode 100644 index 000000000..0308ff9a8 --- /dev/null +++ b/app/src/androidTest/java/de/test/antennapod/feed/FeedFilterTest.java @@ -0,0 +1,119 @@ +package de.test.antennapod.feed; + +import android.test.AndroidTestCase; + +import de.danoeh.antennapod.core.feed.FeedFilter; +import de.danoeh.antennapod.core.feed.FeedItem; + +public class FeedFilterTest extends AndroidTestCase { + + public void testNullFilter() throws Exception { + FeedFilter filter = new FeedFilter(); + FeedItem item = new FeedItem(); + item.setTitle("Hello world"); + + assertTrue(!filter.excludeOnly()); + assertTrue(!filter.includeOnly()); + assertEquals("", filter.getExcludeFilter()); + assertEquals("", filter.getIncludeFilter()); + assertTrue(filter.shouldAutoDownload(item)); + } + + public void testBasicIncludeFilter() throws Exception { + String includeFilter = "Hello"; + FeedFilter filter = new FeedFilter(includeFilter, ""); + FeedItem item = new FeedItem(); + item.setTitle("Hello world"); + + FeedItem item2 = new FeedItem(); + item2.setTitle("Don't include me"); + + assertTrue(!filter.excludeOnly()); + assertTrue(filter.includeOnly()); + assertEquals("", filter.getExcludeFilter()); + assertEquals(includeFilter, filter.getIncludeFilter()); + assertTrue(filter.shouldAutoDownload(item)); + assertTrue(!filter.shouldAutoDownload(item2)); + } + + public void testBasicExcludeFilter() throws Exception { + String excludeFilter = "Hello"; + FeedFilter filter = new FeedFilter("", excludeFilter); + FeedItem item = new FeedItem(); + item.setTitle("Hello world"); + + FeedItem item2 = new FeedItem(); + item2.setTitle("Item2"); + + assertTrue(filter.excludeOnly()); + assertTrue(!filter.includeOnly()); + assertEquals(excludeFilter, filter.getExcludeFilter()); + assertEquals("", filter.getIncludeFilter()); + assertTrue(!filter.shouldAutoDownload(item)); + assertTrue(filter.shouldAutoDownload(item2)); + } + + public void testComplexIncludeFilter() throws Exception { + String includeFilter = "Hello \"Two words\""; + FeedFilter filter = new FeedFilter(includeFilter, ""); + FeedItem item = new FeedItem(); + item.setTitle("hello world"); + + FeedItem item2 = new FeedItem(); + item2.setTitle("Two three words"); + + FeedItem item3 = new FeedItem(); + item3.setTitle("One two words"); + + assertTrue(!filter.excludeOnly()); + assertTrue(filter.includeOnly()); + assertEquals("", filter.getExcludeFilter()); + assertEquals(includeFilter, filter.getIncludeFilter()); + assertTrue(filter.shouldAutoDownload(item)); + assertTrue(!filter.shouldAutoDownload(item2)); + assertTrue(filter.shouldAutoDownload(item3)); + } + + public void testComplexExcludeFilter() throws Exception { + String excludeFilter = "Hello \"Two words\""; + FeedFilter filter = new FeedFilter("", excludeFilter); + FeedItem item = new FeedItem(); + item.setTitle("hello world"); + + FeedItem item2 = new FeedItem(); + item2.setTitle("One three words"); + + FeedItem item3 = new FeedItem(); + item3.setTitle("One two words"); + + assertTrue(filter.excludeOnly()); + assertTrue(!filter.includeOnly()); + assertEquals(excludeFilter, filter.getExcludeFilter()); + assertEquals("", filter.getIncludeFilter()); + assertTrue(!filter.shouldAutoDownload(item)); + assertTrue(filter.shouldAutoDownload(item2)); + assertTrue(!filter.shouldAutoDownload(item3)); + } + + public void testComboFilter() throws Exception { + String includeFilter = "Hello world"; + String excludeFilter = "dislike"; + FeedFilter filter = new FeedFilter(includeFilter, excludeFilter); + + FeedItem download = new FeedItem(); + download.setTitle("Hello everyone!"); + // because, while it has words from the include filter it also has exclude words + FeedItem doNotDownload = new FeedItem(); + doNotDownload.setTitle("I dislike the world"); + // because it has no words from the include filter + FeedItem doNotDownload2 = new FeedItem(); + doNotDownload2.setTitle("no words to include"); + + assertTrue(filter.hasExcludeFilter()); + assertTrue(filter.hasIncludeFilter()); + assertTrue(filter.shouldAutoDownload(download)); + assertTrue(!filter.shouldAutoDownload(doNotDownload)); + assertTrue(!filter.shouldAutoDownload(doNotDownload2)); + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/activity/FeedInfoActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/FeedInfoActivity.java index 80883e4ae..8b46c934d 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/FeedInfoActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/FeedInfoActivity.java @@ -21,6 +21,7 @@ import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.EditText; import android.widget.ImageView; +import android.widget.RadioButton; import android.widget.Spinner; import android.widget.TextView; import android.widget.Toast; @@ -32,6 +33,7 @@ import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.dialog.ConfirmationDialog; import de.danoeh.antennapod.core.dialog.DownloadRequestErrorDialogCreator; import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedFilter; import de.danoeh.antennapod.core.feed.FeedPreferences; import de.danoeh.antennapod.core.glide.ApGlideSettings; import de.danoeh.antennapod.core.preferences.UserPreferences; @@ -61,8 +63,12 @@ public class FeedInfoActivity extends ActionBarActivity { private TextView txtvUrl; private EditText etxtUsername; private EditText etxtPassword; + private EditText etxtFilterText; + private RadioButton rdoFilterInclude; + private RadioButton rdoFilterExclude; private CheckBox cbxAutoDownload; private Spinner spnAutoDelete; + private boolean filterInclude = true; private final View.OnClickListener copyUrlToClipboard = new View.OnClickListener() { @Override @@ -103,6 +109,17 @@ public class FeedInfoActivity extends ActionBarActivity { spnAutoDelete = (Spinner) findViewById(R.id.spnAutoDelete); etxtUsername = (EditText) findViewById(R.id.etxtUsername); etxtPassword = (EditText) findViewById(R.id.etxtPassword); + etxtFilterText = (EditText) findViewById(R.id.etxtEpisodeFilterText); + rdoFilterInclude = (RadioButton) findViewById(R.id.radio_filter_include); + rdoFilterInclude.setOnClickListener(v -> { + filterInclude = true; + filterTextChanged = true; + }); + rdoFilterExclude = (RadioButton) findViewById(R.id.radio_filter_exclude); + rdoFilterExclude.setOnClickListener(v -> { + filterInclude = false; + filterTextChanged = true; + }); txtvUrl.setOnClickListener(copyUrlToClipboard); @@ -184,6 +201,7 @@ public class FeedInfoActivity extends ActionBarActivity { feed.getPreferences().setAutoDeleteAction(auto_delete_action);// p autoDeleteChanged = true; } + @Override public void onNothingSelected(AdapterView parent) { // Another interface callback @@ -197,6 +215,23 @@ public class FeedInfoActivity extends ActionBarActivity { etxtUsername.addTextChangedListener(authTextWatcher); etxtPassword.addTextChangedListener(authTextWatcher); + FeedFilter filter = prefs.getFilter(); + if (filter.includeOnly()) { + etxtFilterText.setText(filter.getIncludeFilter()); + rdoFilterInclude.setChecked(true); + rdoFilterExclude.setChecked(false); + } else if (filter.excludeOnly()) { + etxtFilterText.setText(filter.getExcludeFilter()); + rdoFilterInclude.setChecked(false); + rdoFilterExclude.setChecked(true); + } else { + Log.d(TAG, "No filter set"); + rdoFilterInclude.setChecked(false); + rdoFilterExclude.setChecked(false); + etxtFilterText.setText(""); + } + etxtFilterText.addTextChangedListener(filterTextWatcher); + supportInvalidateOptionsMenu(); } else { @@ -227,6 +262,25 @@ public class FeedInfoActivity extends ActionBarActivity { } }; + private boolean filterTextChanged = false; + + private TextWatcher filterTextWatcher = new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + + } + + @Override + public void afterTextChanged(Editable s) { + filterTextChanged = true; + } + }; + @Override protected void onPause() { super.onPause(); @@ -237,11 +291,24 @@ public class FeedInfoActivity extends ActionBarActivity { prefs.setUsername(etxtUsername.getText().toString()); prefs.setPassword(etxtPassword.getText().toString()); } - if (authInfoChanged || autoDeleteChanged) { + if (filterTextChanged) { + Log.d(TAG, "Filter info changed, saving..."); + String filterText = etxtFilterText.getText().toString(); + String includeString = ""; + String excludeString = ""; + if (filterInclude) { + includeString = filterText; + } else { + excludeString = filterText; + } + prefs.setFilter(new FeedFilter(includeString, excludeString)); + } + if (authInfoChanged || autoDeleteChanged || filterTextChanged) { DBWriter.setFeedPreferences(prefs); } authInfoChanged = false; autoDeleteChanged = false; + filterTextChanged = false; } } diff --git a/app/src/main/res/layout/feedinfo.xml b/app/src/main/res/layout/feedinfo.xml index 2b49b4b35..2e36bf495 100644 --- a/app/src/main/res/layout/feedinfo.xml +++ b/app/src/main/res/layout/feedinfo.xml @@ -265,6 +265,53 @@ + + + + + + + + + + + + parseTerms(String filter) { + // from http://stackoverflow.com/questions/7804335/split-string-on-spaces-in-java-except-if-between-quotes-i-e-treat-hello-wor + List list = new ArrayList<>(); + Matcher m = Pattern.compile("([^\"]\\S*|\".+?\")\\s*").matcher(filter); + while (m.find()) + list.add(m.group(1).replace("\"", "")); + return list; + } + + /** + * @param item + * @return true if the item should be downloaded + */ + public boolean shouldAutoDownload(FeedItem item) { + + List includeTerms = parseTerms(includeFilter); + List excludeTerms = parseTerms(excludeFilter); + + if (includeTerms.size() == 0 && excludeTerms.size() == 0) { + // nothing has been specified, so include everything + return true; + } + + // check using lowercase so the users don't have to worry about case. + String title = item.getTitle().toLowerCase(); + + // if it's explicitly excluded, it shouldn't be autodownloaded + // even if it has include terms + for (String term : excludeTerms) { + if (title.contains(term.trim().toLowerCase())) { + return false; + } + } + + for (String term : includeTerms) { + if (title.contains(term.trim().toLowerCase())) { + return true; + } + } + + // now's the tricky bit + // if they haven't set an include filter, but they have set an exclude filter + // default to including, but if they've set both, then exclude + if (!hasIncludeFilter() && hasExcludeFilter()) { + return true; + } + + return false; + } + + public String getIncludeFilter() { + return includeFilter; + } + + public String getExcludeFilter() { return excludeFilter; } + + /** + * @return true if only include is set + */ + public boolean includeOnly() { + return hasIncludeFilter() && !hasExcludeFilter(); + } + + /** + * @return true if only exclude is set + */ + public boolean excludeOnly() { + return hasExcludeFilter() && !hasIncludeFilter(); + } + + public boolean hasIncludeFilter() { + return includeFilter.length() > 0; + } + + public boolean hasExcludeFilter() { + return excludeFilter.length() > 0; + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedPreferences.java index ed568a6e5..9e95d5276 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedPreferences.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedPreferences.java @@ -2,6 +2,7 @@ package de.danoeh.antennapod.core.feed; import android.content.Context; import android.database.Cursor; +import android.support.annotation.NonNull; import android.text.TextUtils; import de.danoeh.antennapod.core.preferences.UserPreferences; @@ -13,8 +14,11 @@ import de.danoeh.antennapod.core.storage.PodDBAdapter; */ public class FeedPreferences { + @NonNull + private FeedFilter filter; private long feedID; private boolean autoDownload; + public enum AutoDeleteAction { GLOBAL, YES, @@ -25,11 +29,16 @@ public class FeedPreferences { private String password; public FeedPreferences(long feedID, boolean autoDownload, AutoDeleteAction auto_delete_action, String username, String password) { + this(feedID, autoDownload, auto_delete_action, username, password, new FeedFilter()); + } + + public FeedPreferences(long feedID, boolean autoDownload, AutoDeleteAction auto_delete_action, String username, String password, @NonNull FeedFilter filter) { this.feedID = feedID; this.autoDownload = autoDownload; this.auto_delete_action = auto_delete_action; this.username = username; this.password = password; + this.filter = filter; } public static FeedPreferences fromCursor(Cursor cursor) { @@ -38,6 +47,8 @@ public class FeedPreferences { int indexAutoDeleteAction = cursor.getColumnIndex(PodDBAdapter.KEY_AUTO_DELETE_ACTION); int indexUsername = cursor.getColumnIndex(PodDBAdapter.KEY_USERNAME); int indexPassword = cursor.getColumnIndex(PodDBAdapter.KEY_PASSWORD); + int indexIncludeFilter = cursor.getColumnIndex(PodDBAdapter.KEY_INCLUDE_FILTER); + int indexExcludeFilter = cursor.getColumnIndex(PodDBAdapter.KEY_EXCLUDE_FILTER); long feedId = cursor.getLong(indexId); boolean autoDownload = cursor.getInt(indexAutoDownload) > 0; @@ -45,10 +56,21 @@ public class FeedPreferences { AutoDeleteAction autoDeleteAction = AutoDeleteAction.values()[autoDeleteActionIndex]; String username = cursor.getString(indexUsername); String password = cursor.getString(indexPassword); - return new FeedPreferences(feedId, autoDownload, autoDeleteAction, username, password); + String includeFilter = cursor.getString(indexIncludeFilter); + String excludeFilter = cursor.getString(indexExcludeFilter); + return new FeedPreferences(feedId, autoDownload, autoDeleteAction, username, password, new FeedFilter(includeFilter, excludeFilter)); } + /** + * @return the filter for this feed + */ + public FeedFilter getFilter() { + return filter; + } + public void setFilter(@NonNull FeedFilter filter) { + this.filter = filter; + } /** * Compare another FeedPreferences with this one. The feedID, autoDownload and AutoDeleteAction attribute are excluded from the diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/APDownloadAlgorithm.java b/core/src/main/java/de/danoeh/antennapod/core/storage/APDownloadAlgorithm.java index 9e21a55f2..26dc027bf 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/APDownloadAlgorithm.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/APDownloadAlgorithm.java @@ -7,7 +7,9 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import de.danoeh.antennapod.core.feed.FeedFilter; import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedPreferences; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.util.NetworkUtils; import de.danoeh.antennapod.core.util.PowerUtils; @@ -54,7 +56,9 @@ public class APDownloadAlgorithm implements AutomaticDownloadAlgorithm { candidates = new ArrayList(queue.size() + newItems.size()); candidates.addAll(queue); for(FeedItem newItem : newItems) { - if(candidates.contains(newItem) == false) { + FeedPreferences feedPrefs = newItem.getFeed().getPreferences(); + FeedFilter feedFilter = feedPrefs.getFilter(); + if(candidates.contains(newItem) == false && feedFilter.shouldAutoDownload(newItem)) { candidates.add(newItem); } } 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 63b563525..a2f65c7e6 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 @@ -102,6 +102,8 @@ public class PodDBAdapter { public static final String KEY_LAST_UPDATE_FAILED = "last_update_failed"; public static final String KEY_HAS_EMBEDDED_PICTURE = "has_embedded_picture"; public static final String KEY_LAST_PLAYED_TIME = "last_played_time"; + public static final String KEY_INCLUDE_FILTER = "include_filter"; + public static final String KEY_EXCLUDE_FILTER = "exclude_filter"; // Table names public static final String TABLE_NAME_FEEDS = "Feeds"; @@ -128,6 +130,8 @@ public class PodDBAdapter { + KEY_FLATTR_STATUS + " INTEGER," + KEY_USERNAME + " TEXT," + KEY_PASSWORD + " TEXT," + + KEY_INCLUDE_FILTER + " TEXT," + + KEY_EXCLUDE_FILTER + " TEXT," + KEY_IS_PAGED + " INTEGER DEFAULT 0," + KEY_NEXT_PAGE_LINK + " TEXT," + KEY_HIDE + " TEXT," @@ -238,6 +242,8 @@ public class PodDBAdapter { TABLE_NAME_FEEDS + "." + KEY_HIDE, TABLE_NAME_FEEDS + "." + KEY_LAST_UPDATE_FAILED, TABLE_NAME_FEEDS + "." + KEY_AUTO_DELETE_ACTION, + TABLE_NAME_FEEDS + "." + KEY_INCLUDE_FILTER, + TABLE_NAME_FEEDS + "." + KEY_EXCLUDE_FILTER }; /** @@ -395,6 +401,8 @@ public class PodDBAdapter { values.put(KEY_AUTO_DELETE_ACTION,prefs.getAutoDeleteAction().ordinal()); values.put(KEY_USERNAME, prefs.getUsername()); values.put(KEY_PASSWORD, prefs.getPassword()); + values.put(KEY_INCLUDE_FILTER, prefs.getFilter().getIncludeFilter()); + values.put(KEY_EXCLUDE_FILTER, prefs.getFilter().getExcludeFilter()); db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?", new String[]{String.valueOf(prefs.getFeedID())}); } @@ -1745,6 +1753,7 @@ public class PodDBAdapter { db.execSQL(PodDBAdapter.CREATE_INDEX_FEEDITEMS_PUBDATE); db.execSQL(PodDBAdapter.CREATE_INDEX_FEEDITEMS_READ); } + if (oldVersion < 1050003) { // Migrates feed list filter data @@ -1780,6 +1789,13 @@ public class PodDBAdapter { db.setTransactionSuccessful(); db.endTransaction(); + + // and now get ready for autodownload filters + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + + " ADD COLUMN " + PodDBAdapter.KEY_INCLUDE_FILTER + " TEXT DEFAULT ''"); + + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + + " ADD COLUMN " + PodDBAdapter.KEY_EXCLUDE_FILTER + " TEXT DEFAULT ''"); } EventBus.getDefault().post(ProgressEvent.end()); diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index e4097feed..3e0aa6538 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -530,7 +530,11 @@ Authentication Change your username and password for this podcast and its episodes. - + Episode Filter + List of terms used to decide if an episode should be included or excluded from being autodownloaded + Include + Exclude + Filter text Upgrading the database