diff options
182 files changed, 5629 insertions, 2381 deletions
diff --git a/.gitignore b/.gitignore index 482ba1839..5a3070e8f 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ gen-external-apklibs out #transifex downloads changelog +description # other *.odg# diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 9e6042bf4..65d29f75a 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -1,5 +1,6 @@ Code contributions: +danieloeh patheticpat toggles ligi diff --git a/app/build.gradle b/app/build.gradle index f9c768d3e..40451e8ea 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,27 +1,46 @@ +import org.apache.tools.ant.filters.ReplaceTokens + apply plugin: 'com.android.application' repositories { mavenCentral() } + dependencies { compile 'com.android.support:support-v4:21.0.3' compile 'com.android.support:appcompat-v7:21.0.3' + compile 'com.android.support:gridlayout-v7:21.0.3' compile 'org.apache.commons:commons-lang3:3.3.2' compile('org.shredzone.flattr4j:flattr4j-core:2.12') { exclude group: 'org.json', module: 'json' } compile 'commons-io:commons-io:2.4' - compile 'com.jayway.android.robotium:robotium-solo:5.2.1' compile 'org.jsoup:jsoup:1.7.3' - compile 'com.squareup.picasso:picasso:2.4.0' - compile 'com.squareup.okhttp:okhttp:2.2.0' - compile 'com.squareup.okhttp:okhttp-urlconnection:2.2.0' + compile 'com.squareup.picasso:picasso:2.5.2' + compile 'com.squareup.okhttp:okhttp:2.3.0' + compile 'com.squareup.okhttp:okhttp-urlconnection:2.3.0' compile 'com.squareup.okio:okio:1.2.0' compile 'de.greenrobot:eventbus:2.4.0' + compile 'com.joanzapata.android:android-iconify:1.0.9' + compile project(':core') compile project(':library:drag-sort-listview') } +def getMyVersionName() { + def parsedManifestXml = (new XmlSlurper()) + .parse('app/src/main/AndroidManifest.xml') + .declareNamespace(android:"http://schemas.android.com/apk/res/android") + return parsedManifestXml.'@android:versionName' +} + +def getMyVersionCode() { + def parsedManifestXml = (new XmlSlurper()) + .parse('app/src/main/AndroidManifest.xml') + .declareNamespace(android:"http://schemas.android.com/apk/res/android") + return parsedManifestXml.'@android:versionCode'.toInteger() +} + android { compileSdkVersion 21 buildToolsVersion "21.1.2" @@ -29,6 +48,8 @@ android { defaultConfig { minSdkVersion 10 targetSdkVersion 21 + versionCode getMyVersionCode() + versionName "${getMyVersionName()}" testApplicationId "de.test.antennapod" testInstrumentationRunner "de.test.antennapod.AntennaPodTestRunner" } @@ -87,4 +108,21 @@ android { lintOptions { abortOnError false } -}
\ No newline at end of file +} + +// about.html is templatized so that we can automatically insert +// our version string in to it at build time. +task filterAbout { + inputs.files files(['src/main/templates/about.html', + 'src/main/AndroidManifest.xml']) + outputs.file 'src/main/assets/about.html' +} << { + copy { + from 'src/main/templates/about.html' + into 'src/main/assets' + filter(ReplaceTokens, tokens: [versionname: android.defaultConfig.versionName, + versioncode: android.defaultConfig.versionCode.toString()]) + } +} + +preBuild.dependsOn filterAbout
\ No newline at end of file diff --git a/app/proguard.cfg b/app/proguard.cfg index c0f1796ea..536f54eca 100644 --- a/app/proguard.cfg +++ b/app/proguard.cfg @@ -1,5 +1,7 @@ +-dontobfuscate -renamesourcefileattribute SourceFile -keepattributes SourceFile,LineNumberTable +-optimizations !code/allocation/variable -dontpreverify -repackageclasses '' @@ -84,3 +86,6 @@ -keepclassmembers class ** { public void onEvent*(**); } + +# android-iconify +-keep class com.joanzapata.** { *; } diff --git a/app/src/androidTest/java/de/test/antennapod/storage/DBReaderTest.java b/app/src/androidTest/java/de/test/antennapod/storage/DBReaderTest.java index dfb5fd381..95d2ce58a 100644 --- a/app/src/androidTest/java/de/test/antennapod/storage/DBReaderTest.java +++ b/app/src/androidTest/java/de/test/antennapod/storage/DBReaderTest.java @@ -45,7 +45,7 @@ public class DBReaderTest extends InstrumentationTestCase { private void expiredFeedListTestHelper(long lastUpdate, long expirationTime, boolean shouldReturn) { final Context context = getInstrumentation().getTargetContext(); Feed feed = new Feed(0, new Date(lastUpdate), "feed", "link", "descr", null, - null, null, null, "feed", null, null, "url", false, new FlattrStatus(), false, null); + null, null, null, "feed", null, null, "url", false, new FlattrStatus(), false, null, null, false); feed.setItems(new ArrayList<FeedItem>()); PodDBAdapter adapter = new PodDBAdapter(context); adapter.open(); @@ -301,7 +301,7 @@ public class DBReaderTest extends InstrumentationTestCase { } } - public void testGetUnreadItemIds() { + public void testGetNewItemIds() { final Context context = getInstrumentation().getTargetContext(); final int numItems = 10; @@ -310,10 +310,11 @@ public class DBReaderTest extends InstrumentationTestCase { for (int i = 0; i < unread.size(); i++) { unreadIds[i] = unread.get(i).getId(); } - long[] unreadSaved = DBReader.getUnreadItemIds(context); + LongList unreadSaved = DBReader.getNewItemIds(context); assertNotNull(unreadSaved); - assertTrue(unread.size() == unreadSaved.length); - for (long savedId : unreadSaved) { + assertTrue(unread.size() == unreadSaved.size()); + for(int i=0; i < unreadSaved.size(); i++) { + long savedId = unreadSaved.get(i); boolean found = false; for (long id : unreadIds) { if (id == savedId) { @@ -375,7 +376,7 @@ public class DBReaderTest extends InstrumentationTestCase { List<Feed> feeds = DBTestUtils.saveFeedlist(context, NUM_FEEDS, NUM_ITEMS, true); DBReader.NavDrawerData navDrawerData = DBReader.getNavDrawerData(context); assertEquals(NUM_FEEDS, navDrawerData.feeds.size()); - assertEquals(0, navDrawerData.numUnreadItems); + assertEquals(0, navDrawerData.numNewItems); assertEquals(0, navDrawerData.queueSize); } @@ -404,7 +405,7 @@ public class DBReaderTest extends InstrumentationTestCase { DBReader.NavDrawerData navDrawerData = DBReader.getNavDrawerData(context); assertEquals(NUM_FEEDS, navDrawerData.feeds.size()); - assertEquals(NUM_UNREAD, navDrawerData.numUnreadItems); + assertEquals(NUM_UNREAD, navDrawerData.numNewItems); assertEquals(NUM_QUEUE, navDrawerData.queueSize); } diff --git a/app/src/androidTest/java/de/test/antennapod/storage/DBTasksTest.java b/app/src/androidTest/java/de/test/antennapod/storage/DBTasksTest.java index e28a7918f..16f50cf28 100644 --- a/app/src/androidTest/java/de/test/antennapod/storage/DBTasksTest.java +++ b/app/src/androidTest/java/de/test/antennapod/storage/DBTasksTest.java @@ -3,7 +3,16 @@ package de.test.antennapod.storage; import android.content.Context; import android.content.SharedPreferences; import android.preference.PreferenceManager; +import android.test.FlakyTest; import android.test.InstrumentationTestCase; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; + import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; @@ -13,28 +22,23 @@ import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.storage.PodDBAdapter; import de.danoeh.antennapod.core.util.flattr.FlattrStatus; -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.List; - -import static de.test.antennapod.storage.DBTestUtils.*; +import static de.test.antennapod.storage.DBTestUtils.saveFeedlist; /** * Test class for DBTasks */ public class DBTasksTest extends InstrumentationTestCase { - private static final String TEST_FOLDER = "testDBTasks"; + + private static final String TAG = "DBTasksTest"; private static final int EPISODE_CACHE_SIZE = 5; + private Context context; + private File destFolder; @Override protected void tearDown() throws Exception { super.tearDown(); - final Context context = getInstrumentation().getTargetContext(); assertTrue(PodDBAdapter.deleteDatabase(context)); for (File f : destFolder.listFiles()) { @@ -47,23 +51,24 @@ public class DBTasksTest extends InstrumentationTestCase { @Override protected void setUp() throws Exception { super.setUp(); - destFolder = getInstrumentation().getTargetContext().getExternalFilesDir(TEST_FOLDER); + context = getInstrumentation().getTargetContext(); + destFolder = context.getExternalCacheDir(); assertNotNull(destFolder); assertTrue(destFolder.exists()); assertTrue(destFolder.canWrite()); - final Context context = getInstrumentation().getTargetContext(); context.deleteDatabase(PodDBAdapter.DATABASE_NAME); // make sure database is created PodDBAdapter adapter = new PodDBAdapter(context); adapter.open(); adapter.close(); - SharedPreferences.Editor prefEdit = PreferenceManager.getDefaultSharedPreferences(getInstrumentation().getTargetContext().getApplicationContext()).edit(); + SharedPreferences.Editor prefEdit = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()).edit(); prefEdit.putString(UserPreferences.PREF_EPISODE_CACHE_SIZE, Integer.toString(EPISODE_CACHE_SIZE)); prefEdit.commit(); } + @FlakyTest(tolerance = 3) public void testPerformAutoCleanupShouldDelete() throws IOException { final int NUM_ITEMS = EPISODE_CACHE_SIZE * 2; @@ -81,7 +86,7 @@ public class DBTasksTest extends InstrumentationTestCase { items.add(item); } - PodDBAdapter adapter = new PodDBAdapter(getInstrumentation().getTargetContext()); + PodDBAdapter adapter = new PodDBAdapter(context); adapter.open(); adapter.setCompleteFeed(feed); adapter.close(); @@ -91,7 +96,7 @@ public class DBTasksTest extends InstrumentationTestCase { assertTrue(item.getId() != 0); assertTrue(item.getMedia().getId() != 0); } - DBTasks.performAutoCleanup(getInstrumentation().getTargetContext()); + DBTasks.performAutoCleanup(context); for (int i = 0; i < files.size(); i++) { if (i < EPISODE_CACHE_SIZE) { assertTrue(files.get(i).exists()); @@ -101,6 +106,7 @@ public class DBTasksTest extends InstrumentationTestCase { } } + @FlakyTest(tolerance = 3) public void testPerformAutoCleanupShouldNotDeleteBecauseUnread() throws IOException { final int NUM_ITEMS = EPISODE_CACHE_SIZE * 2; @@ -119,7 +125,7 @@ public class DBTasksTest extends InstrumentationTestCase { items.add(item); } - PodDBAdapter adapter = new PodDBAdapter(getInstrumentation().getTargetContext()); + PodDBAdapter adapter = new PodDBAdapter(context); adapter.open(); adapter.setCompleteFeed(feed); adapter.close(); @@ -129,12 +135,13 @@ public class DBTasksTest extends InstrumentationTestCase { assertTrue(item.getId() != 0); assertTrue(item.getMedia().getId() != 0); } - DBTasks.performAutoCleanup(getInstrumentation().getTargetContext()); + DBTasks.performAutoCleanup(context); for (File file : files) { assertTrue(file.exists()); } } + @FlakyTest(tolerance = 3) public void testPerformAutoCleanupShouldNotDeleteBecauseInQueue() throws IOException { final int NUM_ITEMS = EPISODE_CACHE_SIZE * 2; @@ -153,7 +160,7 @@ public class DBTasksTest extends InstrumentationTestCase { items.add(item); } - PodDBAdapter adapter = new PodDBAdapter(getInstrumentation().getTargetContext()); + PodDBAdapter adapter = new PodDBAdapter(context); adapter.open(); adapter.setCompleteFeed(feed); adapter.setQueue(items); @@ -164,7 +171,7 @@ public class DBTasksTest extends InstrumentationTestCase { assertTrue(item.getId() != 0); assertTrue(item.getMedia().getId() != 0); } - DBTasks.performAutoCleanup(getInstrumentation().getTargetContext()); + DBTasks.performAutoCleanup(context); for (File file : files) { assertTrue(file.exists()); } @@ -175,13 +182,13 @@ public class DBTasksTest extends InstrumentationTestCase { * call to DBWriter.deleteFeedMediaOfItem instead of the ID of the FeedMedia. This would cause the wrong item to be deleted. * @throws IOException */ + @FlakyTest(tolerance = 3) public void testPerformAutoCleanupShouldNotDeleteBecauseInQueue_withFeedsWithNoMedia() throws IOException { - final Context context = getInstrumentation().getTargetContext(); // add feed with no enclosures so that item ID != media ID saveFeedlist(context, 1, 10, false); // add candidate for performAutoCleanup - List<Feed> feeds = saveFeedlist(getInstrumentation().getTargetContext(), 1, 1, true); + List<Feed> feeds = saveFeedlist(context, 1, 1, true); FeedMedia m = feeds.get(0).getItems().get(0).getMedia(); m.setDownloaded(true); m.setFile_url("file"); @@ -193,8 +200,8 @@ public class DBTasksTest extends InstrumentationTestCase { testPerformAutoCleanupShouldNotDeleteBecauseInQueue(); } + @FlakyTest(tolerance = 3) public void testUpdateFeedNewFeed() { - final Context context = getInstrumentation().getTargetContext(); final int NUM_ITEMS = 10; Feed feed = new Feed("url", new Date(), "title"); @@ -214,7 +221,6 @@ public class DBTasksTest extends InstrumentationTestCase { /** Two feeds with the same title, but different download URLs should be treated as different feeds. */ public void testUpdateFeedSameTitle() { - final Context context = getInstrumentation().getTargetContext(); Feed feed1 = new Feed("url1", new Date(), "title"); Feed feed2 = new Feed("url2", new Date(), "title"); @@ -229,7 +235,6 @@ public class DBTasksTest extends InstrumentationTestCase { } public void testUpdateFeedUpdatedFeed() { - final Context context = getInstrumentation().getTargetContext(); final int NUM_ITEMS_OLD = 10; final int NUM_ITEMS_NEW = 10; @@ -269,6 +274,7 @@ public class DBTasksTest extends InstrumentationTestCase { updatedFeedTest(feedFromDB, feedID, itemIDs, NUM_ITEMS_OLD, NUM_ITEMS_NEW); } + @FlakyTest(tolerance = 3) private void updatedFeedTest(final Feed newFeed, long feedID, List<Long> itemIDs, final int NUM_ITEMS_OLD, final int NUM_ITEMS_NEW) { assertTrue(newFeed.getId() == feedID); assertTrue(newFeed.getItems().size() == NUM_ITEMS_NEW + NUM_ITEMS_OLD); @@ -292,11 +298,11 @@ public class DBTasksTest extends InstrumentationTestCase { } } + @FlakyTest(tolerance = 3) private void expiredFeedListTestHelper(long lastUpdate, long expirationTime, boolean shouldReturn) { - final Context context = getInstrumentation().getTargetContext(); UserPreferences.setUpdateInterval(context, expirationTime); Feed feed = new Feed(0, new Date(lastUpdate), "feed", "link", "descr", null, - null, null, null, "feed", null, null, "url", false, new FlattrStatus(), false, null); + null, null, null, "feed", null, null, "url", false, new FlattrStatus(), false, null, null, false); feed.setItems(new ArrayList<FeedItem>()); PodDBAdapter adapter = new PodDBAdapter(context); adapter.open(); @@ -314,11 +320,13 @@ public class DBTasksTest extends InstrumentationTestCase { } } + @FlakyTest(tolerance = 3) public void testGetExpiredFeedsTestShouldReturn() { final long expirationTime = 1000 * 60 * 60; expiredFeedListTestHelper(System.currentTimeMillis() - expirationTime - 1, expirationTime, true); } + @FlakyTest(tolerance = 3) public void testGetExpiredFeedsTestShouldNotReturn() { final long expirationTime = 1000 * 60 * 60; expiredFeedListTestHelper(System.currentTimeMillis() - expirationTime / 2, expirationTime, false); diff --git a/app/src/androidTest/java/de/test/antennapod/storage/DBTestUtils.java b/app/src/androidTest/java/de/test/antennapod/storage/DBTestUtils.java index 17c926cc2..4f33cd798 100644 --- a/app/src/androidTest/java/de/test/antennapod/storage/DBTestUtils.java +++ b/app/src/androidTest/java/de/test/antennapod/storage/DBTestUtils.java @@ -47,7 +47,7 @@ public class DBTestUtils { adapter.open(); for (int i = 0; i < numFeeds; i++) { Feed f = new Feed(0, new Date(), "feed " + i, "link" + i, "descr", null, null, - null, null, "id" + i, null, null, "url" + i, false, new FlattrStatus(), false, null); + null, null, "id" + i, null, null, "url" + i, false, new FlattrStatus(), false, null, null, false); f.setItems(new ArrayList<FeedItem>()); for (int j = 0; j < numItems; j++) { FeedItem item = new FeedItem(0, "item " + j, "id" + j, "link" + j, new Date(), diff --git a/app/src/androidTest/java/de/test/antennapod/ui/MainActivityTest.java b/app/src/androidTest/java/de/test/antennapod/ui/MainActivityTest.java index b092264cd..0326174e3 100644 --- a/app/src/androidTest/java/de/test/antennapod/ui/MainActivityTest.java +++ b/app/src/androidTest/java/de/test/antennapod/ui/MainActivityTest.java @@ -3,15 +3,26 @@ package de.test.antennapod.ui; import android.content.Context; import android.content.SharedPreferences; import android.test.ActivityInstrumentationTestCase2; +import android.test.FlakyTest; import android.widget.ListView; import com.robotium.solo.Solo; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.DefaultOnlineFeedViewActivity; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.storage.PodDBAdapter; +import de.danoeh.antennapod.fragment.AllEpisodesFragment; +import de.danoeh.antennapod.fragment.DownloadsFragment; +import de.danoeh.antennapod.fragment.NewEpisodesFragment; +import de.danoeh.antennapod.fragment.PlaybackHistoryFragment; +import de.danoeh.antennapod.fragment.QueueFragment; import de.danoeh.antennapod.preferences.PreferenceController; /** @@ -22,6 +33,8 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2<MainActiv private Solo solo; private UITestUtils uiTestUtils; + private SharedPreferences prefs; + public MainActivityTest() { super(MainActivity.class); } @@ -38,7 +51,7 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2<MainActiv adapter.close(); // override first launch preference - SharedPreferences prefs = getInstrumentation().getTargetContext().getSharedPreferences(MainActivity.PREF_NAME, Context.MODE_PRIVATE); + prefs = getInstrumentation().getTargetContext().getSharedPreferences(MainActivity.PREF_NAME, Context.MODE_PRIVATE); prefs.edit().putBoolean(MainActivity.PREF_IS_FIRST_LAUNCH, false).commit(); } @@ -46,7 +59,12 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2<MainActiv protected void tearDown() throws Exception { uiTestUtils.tearDown(); solo.finishOpenedActivities(); + PodDBAdapter.deleteDatabase(getInstrumentation().getTargetContext()); + + // reset preferences + prefs.edit().clear().commit(); + super.tearDown(); } @@ -68,20 +86,29 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2<MainActiv solo.waitForText(solo.getString(R.string.subscribed_label)); } + @FlakyTest(tolerance = 3) public void testClickNavDrawer() throws Exception { uiTestUtils.addLocalFeedData(false); - // all episodes + UserPreferences.setHiddenDrawerItems(getInstrumentation().getTargetContext(), new ArrayList<String>()); + + // queue + openNavDrawer(); + solo.clickOnText(solo.getString(R.string.queue_label)); + solo.waitForView(android.R.id.list); + assertEquals(solo.getString(R.string.queue_label), getActionbarTitle()); + + // new episodes openNavDrawer(); solo.clickOnText(solo.getString(R.string.new_episodes_label)); solo.waitForView(android.R.id.list); assertEquals(solo.getString(R.string.new_episodes_label), getActionbarTitle()); - // queue + // all episodes openNavDrawer(); - solo.clickOnText(solo.getString(R.string.queue_label)); + solo.clickOnText(solo.getString(R.string.all_episodes_label)); solo.waitForView(android.R.id.list); - assertEquals(solo.getString(R.string.queue_label), getActionbarTitle()); + assertEquals(solo.getString(R.string.all_episodes_label), getActionbarTitle()); // downloads openNavDrawer(); @@ -117,9 +144,78 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2<MainActiv return ((MainActivity)solo.getCurrentActivity()).getMainActivtyActionBar().getTitle().toString(); } + @FlakyTest(tolerance = 3) public void testGoToPreferences() { openNavDrawer(); - solo.clickOnMenuItem(solo.getString(R.string.settings_label)); + solo.clickOnText(solo.getString(R.string.settings_label)); solo.waitForActivity(PreferenceController.getPreferenceActivity()); } + + public void testDrawerPreferencesHideSomeElements() { + UserPreferences.setHiddenDrawerItems(getInstrumentation().getTargetContext(), new ArrayList<String>()); + openNavDrawer(); + solo.clickLongOnText(solo.getString(R.string.queue_label)); + solo.waitForDialogToOpen(); + solo.clickOnText(solo.getString(R.string.all_episodes_label)); + solo.clickOnText(solo.getString(R.string.playback_history_label)); + solo.clickOnText(solo.getString(R.string.confirm_label)); + solo.waitForDialogToClose(); + List<String> hidden = UserPreferences.getHiddenDrawerItems(); + assertEquals(2, hidden.size()); + assertTrue(hidden.contains(AllEpisodesFragment.TAG)); + assertTrue(hidden.contains(PlaybackHistoryFragment.TAG)); + } + + public void testDrawerPreferencesUnhideSomeElements() { + List<String> hidden = Arrays.asList(NewEpisodesFragment.TAG, DownloadsFragment.TAG); + UserPreferences.setHiddenDrawerItems(getInstrumentation().getTargetContext(), hidden); + openNavDrawer(); + solo.clickLongOnText(solo.getString(R.string.queue_label)); + solo.waitForDialogToOpen(); + solo.clickOnText(solo.getString(R.string.downloads_label)); + solo.clickOnText(solo.getString(R.string.queue_label)); + solo.clickOnText(solo.getString(R.string.confirm_label)); + solo.waitForDialogToClose(); + hidden = UserPreferences.getHiddenDrawerItems(); + assertEquals(2, hidden.size()); + assertTrue(hidden.contains(QueueFragment.TAG)); + assertTrue(hidden.contains(NewEpisodesFragment.TAG)); + } + + public void testDrawerPreferencesHideAllElements() { + UserPreferences.setHiddenDrawerItems(getInstrumentation().getTargetContext(), new ArrayList<String>()); + String[] titles = getInstrumentation().getTargetContext().getResources().getStringArray(R.array.nav_drawer_titles); + + openNavDrawer(); + solo.clickLongOnText(solo.getString(R.string.queue_label)); + solo.waitForDialogToOpen(); + for(String title : titles) { + solo.clickOnText(title); + } + solo.clickOnText(solo.getString(R.string.confirm_label)); + solo.waitForDialogToClose(); + List<String> hidden = UserPreferences.getHiddenDrawerItems(); + assertEquals(6, hidden.size()); + for(String tag : MainActivity.NAV_DRAWER_TAGS) { + assertTrue(hidden.contains(tag)); + } + } + + public void testDrawerPreferencesHideCurrentElement() { + UserPreferences.setHiddenDrawerItems(getInstrumentation().getTargetContext(), new ArrayList<String>()); + + openNavDrawer(); + String downloads = solo.getString(R.string.downloads_label); + solo.clickOnText(downloads); + solo.waitForView(android.R.id.list); + openNavDrawer(); + solo.clickLongOnText(downloads); + solo.waitForDialogToOpen(); + solo.clickOnText(downloads); + solo.clickOnText(solo.getString(R.string.confirm_label)); + solo.waitForDialogToClose(); + List<String> hidden = UserPreferences.getHiddenDrawerItems(); + assertEquals(1, hidden.size()); + assertTrue(hidden.contains(DownloadsFragment.TAG)); + } } diff --git a/app/src/androidTest/java/de/test/antennapod/ui/PlaybackTest.java b/app/src/androidTest/java/de/test/antennapod/ui/PlaybackTest.java index 346ef6c18..775bc0ecd 100644 --- a/app/src/androidTest/java/de/test/antennapod/ui/PlaybackTest.java +++ b/app/src/androidTest/java/de/test/antennapod/ui/PlaybackTest.java @@ -1,17 +1,18 @@ package de.test.antennapod.ui; +import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.preference.PreferenceManager; import android.test.ActivityInstrumentationTestCase2; -import android.widget.TextView; +import com.robotium.solo.Condition; import com.robotium.solo.Solo; +import com.robotium.solo.Timeout; import java.util.List; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.AudioplayerActivity; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.preferences.UserPreferences; @@ -28,6 +29,8 @@ public class PlaybackTest extends ActivityInstrumentationTestCase2<MainActivity> private Solo solo; private UITestUtils uiTestUtils; + private Context context; + public PlaybackTest() { super(MainActivity.class); } @@ -36,28 +39,33 @@ public class PlaybackTest extends ActivityInstrumentationTestCase2<MainActivity> public void setUp() throws Exception { super.setUp(); solo = new Solo(getInstrumentation(), getActivity()); - uiTestUtils = new UITestUtils(getInstrumentation().getTargetContext()); + context = getInstrumentation().getContext(); + + uiTestUtils = new UITestUtils(context); uiTestUtils.setup(); + // create database - PodDBAdapter adapter = new PodDBAdapter(getInstrumentation().getTargetContext()); + PodDBAdapter adapter = new PodDBAdapter(context); adapter.open(); adapter.close(); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getInstrumentation().getTargetContext()); - prefs.edit().putBoolean(UserPreferences.PREF_UNPAUSE_ON_HEADSET_RECONNECT, false).commit(); - prefs.edit().putBoolean(UserPreferences.PREF_PAUSE_ON_HEADSET_DISCONNECT, false).commit(); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + prefs.edit() + .putBoolean(UserPreferences.PREF_UNPAUSE_ON_HEADSET_RECONNECT, false) + .putBoolean(UserPreferences.PREF_PAUSE_ON_HEADSET_DISCONNECT, false) + .putString(UserPreferences.PREF_HIDDEN_DRAWER_ITEMS, "") + .commit(); } @Override public void tearDown() throws Exception { uiTestUtils.tearDown(); solo.finishOpenedActivities(); - PodDBAdapter.deleteDatabase(getInstrumentation().getTargetContext()); + PodDBAdapter.deleteDatabase(context); // shut down playback service skipEpisode(); - getInstrumentation().getTargetContext().sendBroadcast( - new Intent(PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE)); + context.sendBroadcast(new Intent(PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE)); super.tearDown(); } @@ -67,70 +75,97 @@ public class PlaybackTest extends ActivityInstrumentationTestCase2<MainActivity> } private void setContinuousPlaybackPreference(boolean value) { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getInstrumentation().getTargetContext()); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); prefs.edit().putBoolean(UserPreferences.PREF_FOLLOW_QUEUE, value).commit(); } private void skipEpisode() { Intent skipIntent = new Intent(PlaybackService.ACTION_SKIP_CURRENT_EPISODE); - getInstrumentation().getTargetContext().sendBroadcast(skipIntent); + context.sendBroadcast(skipIntent); } private void startLocalPlayback() { - assertTrue(solo.waitForActivity(MainActivity.class)); openNavDrawer(); - solo.clickOnText(solo.getString(R.string.new_episodes_label)); - solo.waitForView(android.R.id.list); + + solo.clickOnText(solo.getString(R.string.all_episodes_label)); + final List<FeedItem> episodes = DBReader.getRecentlyPublishedEpisodes(context, 10); + assertTrue(solo.waitForView(solo.getView(R.id.butSecondaryAction))); + solo.clickOnView(solo.getView(R.id.butSecondaryAction)); - assertTrue(solo.waitForActivity(AudioplayerActivity.class)); assertTrue(solo.waitForView(solo.getView(R.id.butPlay))); + solo.waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + return episodes.get(0).getMedia().isCurrentlyPlaying(); + } + }, Timeout.getLargeTimeout()); } private void startLocalPlaybackFromQueue() { - assertTrue(solo.waitForActivity(MainActivity.class)); - openNavDrawer(); - solo.clickOnText(solo.getString(R.string.queue_label)); assertTrue(solo.waitForView(solo.getView(R.id.butSecondaryAction))); + final List<FeedItem> queue = DBReader.getQueue(context); solo.clickOnImageButton(1); - assertTrue(solo.waitForActivity(AudioplayerActivity.class)); assertTrue(solo.waitForView(solo.getView(R.id.butPlay))); + solo.waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + return queue.get(0).getMedia().isCurrentlyPlaying(); + } + }, Timeout.getLargeTimeout()); } public void testStartLocal() throws Exception { uiTestUtils.addLocalFeedData(true); - DBWriter.clearQueue(getInstrumentation().getTargetContext()).get(); + DBWriter.clearQueue(context).get(); startLocalPlayback(); - - solo.clickOnView(solo.getView(R.id.butPlay)); } public void testContinousPlaybackOffSingleEpisode() throws Exception { setContinuousPlaybackPreference(false); uiTestUtils.addLocalFeedData(true); - DBWriter.clearQueue(getInstrumentation().getTargetContext()).get(); + DBWriter.clearQueue(context).get(); startLocalPlayback(); - assertTrue(solo.waitForActivity(MainActivity.class)); } public void testContinousPlaybackOffMultipleEpisodes() throws Exception { setContinuousPlaybackPreference(false); uiTestUtils.addLocalFeedData(true); - List<FeedItem> queue = DBReader.getQueue(getInstrumentation().getTargetContext()); - FeedItem second = queue.get(0); + List<FeedItem> queue = DBReader.getQueue(context); + final FeedItem first = queue.get(0); + final FeedItem second = queue.get(1); startLocalPlaybackFromQueue(); - assertTrue(solo.waitForText(second.getTitle())); + solo.waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + return first.getMedia().isCurrentlyPlaying() == false; + } + }, 10000); + Thread.sleep(1000); + assertTrue(second.getMedia().isCurrentlyPlaying() == false); } public void testContinuousPlaybackOnMultipleEpisodes() throws Exception { setContinuousPlaybackPreference(true); uiTestUtils.addLocalFeedData(true); - List<FeedItem> queue = DBReader.getQueue(getInstrumentation().getTargetContext()); - FeedItem second = queue.get(1); + List<FeedItem> queue = DBReader.getQueue(context); + final FeedItem first = queue.get(0); + final FeedItem second = queue.get(1); startLocalPlaybackFromQueue(); - assertTrue(solo.waitForText(second.getTitle())); + solo.waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + return first.getMedia().isCurrentlyPlaying() == false; + } + }, 10000); + solo.waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + return second.getMedia().isCurrentlyPlaying() == true; + } + }, 10000); } /** @@ -139,14 +174,24 @@ public class PlaybackTest extends ActivityInstrumentationTestCase2<MainActivity> private void replayEpisodeCheck(boolean followQueue) throws Exception { setContinuousPlaybackPreference(followQueue); uiTestUtils.addLocalFeedData(true); - DBWriter.clearQueue(getInstrumentation().getTargetContext()).get(); - String title = ((TextView) solo.getView(R.id.txtvTitle)).getText().toString(); + DBWriter.clearQueue(context).get(); + final List<FeedItem> episodes = DBReader.getRecentlyPublishedEpisodes(context, 10); + startLocalPlayback(); - assertTrue(solo.waitForText(title)); - assertTrue(solo.waitForActivity(MainActivity.class)); + solo.waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + return false == episodes.get(0).getMedia().isCurrentlyPlaying(); + } + }, Timeout.getLargeTimeout()); + startLocalPlayback(); - assertTrue(solo.waitForText(title)); - assertTrue(solo.waitForActivity(MainActivity.class)); + solo.waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + return false == episodes.get(0).getMedia().isCurrentlyPlaying(); + } + }, Timeout.getLargeTimeout()); } public void testReplayEpisodeContinuousPlaybackOn() throws Exception { diff --git a/app/src/androidTest/java/de/test/antennapod/ui/PreferencesTest.java b/app/src/androidTest/java/de/test/antennapod/ui/PreferencesTest.java new file mode 100644 index 000000000..eb1cb9c71 --- /dev/null +++ b/app/src/androidTest/java/de/test/antennapod/ui/PreferencesTest.java @@ -0,0 +1,453 @@ +package de.test.antennapod.ui; + +import android.content.Context; +import android.content.res.Resources; +import android.test.ActivityInstrumentationTestCase2; +import android.test.FlakyTest; + +import com.robotium.solo.Condition; +import com.robotium.solo.Solo; +import com.robotium.solo.Timeout; + +import org.apache.commons.io.IOUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.PreferenceActivity; +import de.danoeh.antennapod.core.preferences.UserPreferences; + +public class PreferencesTest extends ActivityInstrumentationTestCase2<PreferenceActivity> { + + private static final String TAG = "PreferencesTest"; + + private Solo solo; + private Context context; + private Resources res; + + public PreferencesTest() { + super(PreferenceActivity.class); + } + + @Override + public void setUp() throws Exception { + super.setUp(); + solo = new Solo(getInstrumentation(), getActivity()); + Timeout.setSmallTimeout(500); + Timeout.setLargeTimeout(1000); + context = getInstrumentation().getTargetContext(); + res = getActivity().getResources(); + UserPreferences.createInstance(context); + } + + @Override + public void tearDown() throws Exception { + solo.finishOpenedActivities(); + super.tearDown(); + } + + public void testSwitchTheme() { + final int theme = UserPreferences.getTheme(); + int otherTheme; + if(theme == de.danoeh.antennapod.core.R.style.Theme_AntennaPod_Light) { + otherTheme = R.string.pref_theme_title_dark; + } else { + otherTheme = R.string.pref_theme_title_light; + } + solo.clickOnText(solo.getString(R.string.pref_set_theme_title)); + solo.waitForDialogToOpen(); + solo.clickOnText(solo.getString(otherTheme)); + solo.waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + return UserPreferences.getTheme() != theme; + } + }, Timeout.getLargeTimeout()); + } + + public void testSwitchThemeBack() { + final int theme = UserPreferences.getTheme(); + int otherTheme; + if(theme == de.danoeh.antennapod.core.R.style.Theme_AntennaPod_Light) { + otherTheme = R.string.pref_theme_title_dark; + } else { + otherTheme = R.string.pref_theme_title_light; + } + solo.clickOnText(solo.getString(R.string.pref_set_theme_title)); + solo.waitForDialogToOpen(1000); + solo.clickOnText(solo.getString(otherTheme)); + solo.waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + return UserPreferences.getTheme() != theme; + } + }, Timeout.getLargeTimeout()); + } + + public void testExpandNotification() { + final int priority = UserPreferences.getNotifyPriority(); + solo.clickOnText(solo.getString(R.string.pref_expandNotify_title)); + solo.waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + return priority != UserPreferences.getNotifyPriority(); + } + }, Timeout.getLargeTimeout()); + solo.clickOnText(solo.getString(R.string.pref_expandNotify_title)); + solo.waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + return priority == UserPreferences.getNotifyPriority(); + } + }, Timeout.getLargeTimeout()); + } + + public void testEnablePersistentPlaybackControls() { + final boolean persistNotify = UserPreferences.isPersistNotify(); + solo.clickOnText(solo.getString(R.string.pref_persistNotify_title)); + solo.waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + return persistNotify != UserPreferences.isPersistNotify(); + } + }, Timeout.getLargeTimeout()); + solo.clickOnText(solo.getString(R.string.pref_persistNotify_title)); + solo.waitForCondition(new Condition() { + @Override public boolean isSatisfied() { + return persistNotify == UserPreferences.isPersistNotify(); + } + }, Timeout.getLargeTimeout()); + } + + public void testEnqueueAtFront() { + final boolean enqueueAtFront = UserPreferences.enqueueAtFront(); + solo.clickOnText(solo.getString(R.string.pref_queueAddToFront_title)); + solo.waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + return enqueueAtFront != UserPreferences.enqueueAtFront(); + } + }, Timeout.getLargeTimeout()); + solo.clickOnText(solo.getString(R.string.pref_queueAddToFront_title)); + solo.waitForCondition(new Condition() { + @Override public boolean isSatisfied() { + return enqueueAtFront == UserPreferences.enqueueAtFront(); + } + }, Timeout.getLargeTimeout()); + } + + public void testHeadPhonesDisconnect() { + final boolean pauseOnHeadsetDisconnect = UserPreferences.isPauseOnHeadsetDisconnect(); + solo.clickOnText(solo.getString(R.string.pref_pauseOnHeadsetDisconnect_title)); + solo.waitForCondition(new Condition() { + @Override public boolean isSatisfied() { + return pauseOnHeadsetDisconnect != UserPreferences.isPauseOnHeadsetDisconnect(); + } + }, Timeout.getLargeTimeout()); + solo.clickOnText(solo.getString(R.string.pref_pauseOnHeadsetDisconnect_title)); + solo.waitForCondition(new Condition() { + @Override public boolean isSatisfied() { + return pauseOnHeadsetDisconnect == UserPreferences.isPauseOnHeadsetDisconnect(); + } + }, Timeout.getLargeTimeout()); + } + + public void testHeadPhonesReconnect() { + if(UserPreferences.isPauseOnHeadsetDisconnect() == false) { + solo.clickOnText(solo.getString(R.string.pref_pauseOnHeadsetDisconnect_title)); + solo.waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + return UserPreferences.isPauseOnHeadsetDisconnect(); + } + }, Timeout.getLargeTimeout()); + } + final boolean unpauseOnHeadsetReconnect = UserPreferences.isUnpauseOnHeadsetReconnect(); + solo.clickOnText(solo.getString(R.string.pref_unpauseOnHeadsetReconnect_title)); + solo.waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + return unpauseOnHeadsetReconnect != UserPreferences.isUnpauseOnHeadsetReconnect(); + } + }, Timeout.getLargeTimeout()); + solo.clickOnText(solo.getString(R.string.pref_unpauseOnHeadsetReconnect_title)); + solo.waitForCondition(new Condition() { + @Override public boolean isSatisfied() { + return unpauseOnHeadsetReconnect == UserPreferences.isUnpauseOnHeadsetReconnect(); + } + }, Timeout.getLargeTimeout()); + } + + public void testContinuousPlayback() { + final boolean continuousPlayback = UserPreferences.isFollowQueue(); + solo.clickOnText(solo.getString(R.string.pref_followQueue_title)); + solo.waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + return continuousPlayback != UserPreferences.isFollowQueue(); + } + }, Timeout.getLargeTimeout()); + solo.clickOnText(solo.getString(R.string.pref_followQueue_title)); + solo.waitForCondition(new Condition() { + @Override public boolean isSatisfied() { + return continuousPlayback == UserPreferences.isFollowQueue(); + } + }, Timeout.getLargeTimeout()); + } + + public void testAutoDelete() { + final boolean autoDelete = UserPreferences.isAutoDelete(); + solo.clickOnText(solo.getString(R.string.pref_auto_delete_title)); + solo.waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + return autoDelete != UserPreferences.isAutoDelete(); + } + }, Timeout.getLargeTimeout()); + solo.clickOnText(solo.getString(R.string.pref_auto_delete_title)); + solo.waitForCondition(new Condition() { + @Override public boolean isSatisfied() { + return autoDelete == UserPreferences.isAutoDelete(); + } + }, Timeout.getLargeTimeout()); + } + + public void testPlaybackSpeeds() { + solo.clickOnText(solo.getString(R.string.pref_playback_speed_title)); + solo.waitForDialogToOpen(1000); + assertTrue(solo.searchText(solo.getString(R.string.no_playback_plugin_title))); + solo.clickOnText(solo.getString(R.string.close_label)); + solo.waitForDialogToClose(1000); + } + + public void testPauseForInterruptions() { + final boolean pauseForFocusLoss = UserPreferences.shouldPauseForFocusLoss(); + solo.clickOnText(solo.getString(R.string.pref_pausePlaybackForFocusLoss_title)); + solo.waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + return pauseForFocusLoss != UserPreferences.shouldPauseForFocusLoss(); + } + }, Timeout.getLargeTimeout()); + solo.clickOnText(solo.getString(R.string.pref_auto_delete_title)); + solo.waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + return pauseForFocusLoss == UserPreferences.shouldPauseForFocusLoss(); + } + }, Timeout.getLargeTimeout()); + } + + public void testDisableUpdateInterval() { + solo.clickOnText(solo.getString(R.string.pref_autoUpdateIntervall_title)); + solo.waitForDialogToOpen(); + solo.clickOnText(solo.getString(R.string.pref_update_interval_hours_manual)); + solo.waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + return UserPreferences.getUpdateInterval() == 0; + } + }, 1000); + } + + public void testSetUpdateInterval() { + solo.clickOnText(solo.getString(R.string.pref_autoUpdateIntervall_title)); + solo.waitForDialogToOpen(); + String search = "12 " + solo.getString(R.string.pref_update_interval_hours_plural); + solo.clickOnText(search); + solo.waitForDialogToClose(); + solo.waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + return UserPreferences.getUpdateInterval() == 12; + } + }, Timeout.getLargeTimeout()); + } + + public void testMobileUpdates() { + final boolean mobileUpdates = UserPreferences.isAllowMobileUpdate(); + solo.clickOnText(solo.getString(R.string.pref_mobileUpdate_title)); + solo.waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + return mobileUpdates != UserPreferences.isAllowMobileUpdate(); + } + }, Timeout.getLargeTimeout()); + solo.clickOnText(solo.getString(R.string.pref_mobileUpdate_title)); + solo.waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + return mobileUpdates == UserPreferences.isAllowMobileUpdate(); + } + }, Timeout.getLargeTimeout()); + } + + public void testSetSequentialDownload() { + solo.clickOnText(solo.getString(R.string.pref_parallel_downloads_title)); + solo.waitForDialogToOpen(); + solo.clearEditText(0); + solo.enterText(0, "1"); + solo.clickOnText(solo.getString(android.R.string.ok)); + solo.waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + return UserPreferences.getParallelDownloads() == 1; + } + }, Timeout.getLargeTimeout()); + } + + public void testSetParallelDownloads() { + solo.clickOnText(solo.getString(R.string.pref_parallel_downloads_title)); + solo.waitForDialogToOpen(); + solo.clearEditText(0); + solo.enterText(0, "10"); + solo.clickOnText(solo.getString(android.R.string.ok)); + solo.waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + return UserPreferences.getParallelDownloads() == 10; + } + }, Timeout.getLargeTimeout()); + } + + public void testSetParallelDownloadsInvalidInput() { + solo.clickOnText(solo.getString(R.string.pref_parallel_downloads_title)); + solo.waitForDialogToOpen(); + solo.clearEditText(0); + solo.enterText(0, "0"); + assertEquals("1", solo.getEditText(0).getText().toString()); + solo.clearEditText(0); + solo.enterText(0, "100"); + assertEquals("50", solo.getEditText(0).getText().toString()); + } + + public void testSetEpisodeCache() { + String[] entries = res.getStringArray(R.array.episode_cache_size_entries); + String[] values = res.getStringArray(R.array.episode_cache_size_values); + String entry = entries[entries.length/2]; + final int value = Integer.valueOf(values[values.length/2]); + solo.clickOnText(solo.getString(R.string.pref_episode_cache_title)); + solo.waitForDialogToOpen(); + solo.clickOnText(entry); + solo.waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + return UserPreferences.getEpisodeCacheSize() == value; + } + }, Timeout.getLargeTimeout()); + } + + public void testSetEpisodeCacheMin() { + String[] entries = res.getStringArray(R.array.episode_cache_size_entries); + String[] values = res.getStringArray(R.array.episode_cache_size_values); + String minEntry = entries[0]; + final int minValue = Integer.valueOf(values[0]); + solo.clickOnText(solo.getString(R.string.pref_episode_cache_title)); + solo.waitForDialogToOpen(1000); + solo.scrollUp(); + solo.clickOnText(minEntry); + solo.waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + return UserPreferences.getEpisodeCacheSize() == minValue; + } + }, Timeout.getLargeTimeout()); + } + + + public void testSetEpisodeCacheMax() { + String[] entries = res.getStringArray(R.array.episode_cache_size_entries); + String[] values = res.getStringArray(R.array.episode_cache_size_values); + String maxEntry = entries[entries.length-1]; + final int maxValue = Integer.valueOf(values[values.length-1]); + solo.clickOnText(solo.getString(R.string.pref_episode_cache_title)); + solo.waitForDialogToOpen(); + solo.clickOnText(maxEntry); + solo.waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + return UserPreferences.getEpisodeCacheSize() == maxValue; + } + }, Timeout.getLargeTimeout()); + } + + public void testAutomaticDownload() { + final boolean automaticDownload = UserPreferences.isEnableAutodownload(); + solo.clickOnText(solo.getString(R.string.pref_automatic_download_title)); + solo.waitForText(solo.getString(R.string.pref_automatic_download_title)); + solo.clickOnText(solo.getString(R.string.pref_automatic_download_title)); + solo.waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + return automaticDownload != UserPreferences.isEnableAutodownload(); + } + }, Timeout.getLargeTimeout()); + if(UserPreferences.isEnableAutodownload() == false) { + solo.clickOnText(solo.getString(R.string.pref_automatic_download_title)); + } + solo.waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + return UserPreferences.isEnableAutodownload() == true; + } + }, Timeout.getLargeTimeout()); + final boolean enableAutodownloadOnBattery = UserPreferences.isEnableAutodownloadOnBattery(); + solo.clickOnText(solo.getString(R.string.pref_automatic_download_on_battery_title)); + solo.waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + return enableAutodownloadOnBattery != UserPreferences.isEnableAutodownloadOnBattery(); + } + }, Timeout.getLargeTimeout()); + solo.clickOnText(solo.getString(R.string.pref_automatic_download_on_battery_title)); + solo.waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + return enableAutodownloadOnBattery == UserPreferences.isEnableAutodownloadOnBattery(); + } + }, Timeout.getLargeTimeout()); + final boolean enableWifiFilter = UserPreferences.isEnableAutodownloadWifiFilter(); + solo.clickOnText(solo.getString(R.string.pref_autodl_wifi_filter_title)); + solo.waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + return enableWifiFilter != UserPreferences.isEnableAutodownloadWifiFilter(); + } + }, Timeout.getLargeTimeout()); + solo.clickOnText(solo.getString(R.string.pref_automatic_download_on_battery_title)); + solo.waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + return enableWifiFilter == UserPreferences.isEnableAutodownloadWifiFilter(); + } + }, Timeout.getLargeTimeout()); + } + + @FlakyTest(tolerance = 3) + public void testAbout() throws IOException { + int numViews = 0, numLinks = 0; + InputStream input = getActivity().getResources().getAssets().open("about.html"); + List<String> lines = IOUtils.readLines(input); + input.close(); + for(String line : lines) { + if(line.contains("(View)")) { + numViews++; + } else if(line.contains("(Link)")) { + numLinks++; + } + } + for(int i=0; i < numViews; i++) { + solo.clickOnText(solo.getString(R.string.about_pref)); + solo.clickOnText("(View)", i); + solo.goBack(); + } + for(int i=0; i < numLinks; i++) { + solo.clickOnText(solo.getString(R.string.about_pref)); + solo.clickOnText("(Link)", i); + solo.goBack(); + } + } + +} diff --git a/app/src/androidTest/java/de/test/antennapod/ui/UITestUtils.java b/app/src/androidTest/java/de/test/antennapod/ui/UITestUtils.java index 85923d40f..613826932 100644 --- a/app/src/androidTest/java/de/test/antennapod/ui/UITestUtils.java +++ b/app/src/androidTest/java/de/test/antennapod/ui/UITestUtils.java @@ -141,11 +141,12 @@ public class UITestUtils { // create items List<FeedItem> items = new ArrayList<FeedItem>(); for (int j = 0; j < NUM_ITEMS_PER_FEED; j++) { - FeedItem item = new FeedItem(0, "item" + j, "item" + j, "http://example.com/feed" + i + "/item/" + j, new Date(), true, feed); + FeedItem item = new FeedItem(j, "Feed " + (i+1) + ": Item " + (j+1), "item" + j, + "http://example.com/feed" + i + "/item/" + j, new Date(), false, feed); items.add(item); File mediaFile = newMediaFile("feed-" + i + "-episode-" + j + ".mp3"); - item.setMedia(new FeedMedia(0, item, 0, 0, mediaFile.length(), "audio/mp3", null, hostFile(mediaFile), false, null, 0)); + item.setMedia(new FeedMedia(j, item, 0, 0, mediaFile.length(), "audio/mp3", null, hostFile(mediaFile), false, null, 0)); } feed.setItems(items); @@ -169,7 +170,9 @@ public class UITestUtils { * @param downloadEpisodes true if episodes should also be marked as downloaded. */ public void addLocalFeedData(boolean downloadEpisodes) throws Exception { - if (localFeedDataAdded) throw new IllegalStateException("addLocalFeedData was called twice on the same instance"); + if (localFeedDataAdded) { + throw new IllegalStateException("addLocalFeedData was called twice on the same instance"); + } if (!feedDataHosted) { addHostedFeedData(); } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 23136cd37..97f8bbdad 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,8 +1,8 @@ <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="de.danoeh.antennapod" - android:versionCode="48" - android:versionName="1.1"> + android:versionCode="51" + android:versionName="1.2"> <uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> diff --git a/app/src/main/assets/.gitignore b/app/src/main/assets/.gitignore new file mode 100644 index 000000000..328840cfc --- /dev/null +++ b/app/src/main/assets/.gitignore @@ -0,0 +1,2 @@ +# this file is generated automatically +about.html diff --git a/app/src/main/assets/LICENSE_ANDROID_ICONIFY.txt b/app/src/main/assets/LICENSE_ANDROID_ICONIFY.txt new file mode 100644 index 000000000..85a6c0cf2 --- /dev/null +++ b/app/src/main/assets/LICENSE_ANDROID_ICONIFY.txt @@ -0,0 +1,18 @@ +Copyright 2013 Joan Zapata + +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. + +It uses FontAwesome font, licensed under OFL 1.1, which is compatible +with this library's license. + + http://scripts.sil.org/cms/scripts/render_download.php?format=file&media_id=OFL_plaintext&filename=OFL.txt diff --git a/app/src/main/java/de/danoeh/antennapod/activity/AudioplayerActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/AudioplayerActivity.java index f056b107b..b59f6ba35 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/AudioplayerActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/AudioplayerActivity.java @@ -13,7 +13,6 @@ import android.support.v4.widget.DrawerLayout; import android.support.v7.app.ActionBarDrawerToggle; import android.support.v7.widget.Toolbar; import android.util.Log; -import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; @@ -29,7 +28,6 @@ import com.squareup.picasso.Picasso; import org.apache.commons.lang3.StringUtils; -import de.danoeh.antennapod.BuildConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.adapter.ChapterListAdapter; import de.danoeh.antennapod.adapter.NavListAdapter; @@ -47,7 +45,6 @@ import de.danoeh.antennapod.core.util.playback.PlaybackController; import de.danoeh.antennapod.dialog.VariableSpeedDialog; import de.danoeh.antennapod.fragment.CoverFragment; import de.danoeh.antennapod.fragment.ItemDescriptionFragment; -import de.danoeh.antennapod.menuhandler.MenuItemUtils; import de.danoeh.antennapod.menuhandler.NavDrawerActivity; import de.danoeh.antennapod.preferences.PreferenceController; @@ -96,30 +93,25 @@ public class AudioplayerActivity extends MediaplayerActivity implements ItemDesc FragmentTransaction fT = getSupportFragmentManager().beginTransaction(); if (coverFragment != null) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Removing cover fragment"); + Log.d(TAG, "Removing cover fragment"); fT.remove(coverFragment); } if (descriptionFragment != null) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Removing description fragment"); + Log.d(TAG, "Removing description fragment"); fT.remove(descriptionFragment); } if (chapterFragment != null) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Removing chapter fragment"); + Log.d(TAG, "Removing chapter fragment"); fT.remove(chapterFragment); } if (currentlyShownFragment != null) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Removing currently shown fragment"); + Log.d(TAG, "Removing currently shown fragment"); fT.remove(currentlyShownFragment); } for (int i = 0; i < detachedFragments.length; i++) { Fragment f = detachedFragments[i]; if (f != null) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Removing detached fragment"); + Log.d(TAG, "Removing detached fragment"); fT.remove(f); } } @@ -152,8 +144,7 @@ public class AudioplayerActivity extends MediaplayerActivity implements ItemDesc } private void savePreferences() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Saving preferences"); + Log.d(TAG, "Saving preferences"); SharedPreferences prefs = getSharedPreferences(PREFS, MODE_PRIVATE); SharedPreferences.Editor editor = prefs.edit(); if (currentlyShownPosition >= 0 && controller != null @@ -180,9 +171,7 @@ public class AudioplayerActivity extends MediaplayerActivity implements ItemDesc @Override protected void onSaveInstanceState(Bundle outState) { // super.onSaveInstanceState(outState); would cause crash - if (BuildConfig.DEBUG) - Log.d(TAG, "onSaveInstanceState"); - + Log.d(TAG, "onSaveInstanceState"); } @Override @@ -205,8 +194,7 @@ public class AudioplayerActivity extends MediaplayerActivity implements ItemDesc * @return true if restoreFromPrefernces changed the activity's state */ private boolean restoreFromPreferences() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Restoring instance state"); + Log.d(TAG, "Restoring instance state"); SharedPreferences prefs = getSharedPreferences(PREFS, MODE_PRIVATE); int savedPosition = prefs.getInt(PREF_KEY_SELECTED_FRAGMENT_POSITION, -1); @@ -220,15 +208,10 @@ public class AudioplayerActivity extends MediaplayerActivity implements ItemDesc switchToFragment(savedPosition); return true; } else if (controller == null || controller.getMedia() == null) { - if (BuildConfig.DEBUG) - Log.d(TAG, - "Couldn't restore from preferences: controller or media was null"); + Log.d(TAG, "Couldn't restore from preferences: controller or media was null"); } else { - if (BuildConfig.DEBUG) - Log.d(TAG, - "Couldn't restore from preferences: savedPosition was -1 or saved identifier and playable identifier didn't match.\nsavedPosition: " - + savedPosition + ", id: " + playableId - ); + Log.d(TAG, "Couldn't restore from preferences: savedPosition was -1 or saved identifier and playable identifier didn't match.\nsavedPosition: " + + savedPosition + ", id: " + playableId); } return false; @@ -239,9 +222,7 @@ public class AudioplayerActivity extends MediaplayerActivity implements ItemDesc super.onResume(); if (StringUtils.equals(getIntent().getAction(), Intent.ACTION_VIEW)) { Intent intent = getIntent(); - if (BuildConfig.DEBUG) - Log.d(TAG, "Received VIEW intent: " - + intent.getData().getPath()); + Log.d(TAG, "Received VIEW intent: " + intent.getData().getPath()); ExternalMedia media = new ExternalMedia(intent.getData().getPath(), MediaType.AUDIO); Intent launchIntent = new Intent(this, PlaybackService.class); @@ -269,8 +250,7 @@ public class AudioplayerActivity extends MediaplayerActivity implements ItemDesc @Override protected void onAwaitingVideoSurface() { - if (BuildConfig.DEBUG) - Log.d(TAG, "onAwaitingVideoSurface was called in audio player -> switching to video player"); + Log.d(TAG, "onAwaitingVideoSurface was called in audio player -> switching to video player"); startActivity(new Intent(this, VideoplayerActivity.class)); } @@ -295,8 +275,7 @@ public class AudioplayerActivity extends MediaplayerActivity implements ItemDesc * @param pos Must be POS_COVER, POS_DESCR, or POS_CHAPTERS */ private void switchToFragment(int pos) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Switching contentView to position " + pos); + Log.d(TAG, "Switching contentView to position " + pos); if (currentlyShownPosition != pos && controller != null) { Playable media = controller.getMedia(); if (media != null) { @@ -354,9 +333,7 @@ public class AudioplayerActivity extends MediaplayerActivity implements ItemDesc lastShownPosition = currentlyShownPosition; currentlyShownPosition = pos; if (detachedFragments[pos] != null) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Reattaching fragment at position " - + pos); + Log.d(TAG, "Reattaching fragment at position " + pos); ft.attach(detachedFragments[pos]); } else { ft.add(R.id.contentView, currentlyShownFragment); @@ -434,25 +411,7 @@ public class AudioplayerActivity extends MediaplayerActivity implements ItemDesc butShowCover = (ImageButton) findViewById(R.id.butCover); txtvTitle = (TextView) findViewById(R.id.txtvTitle); - drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, R.string.drawer_open, R.string.drawer_close) { - CharSequence currentTitle = getSupportActionBar().getTitle(); - - @Override - public void onDrawerOpened(View drawerView) { - super.onDrawerOpened(drawerView); - currentTitle = getSupportActionBar().getTitle(); - getSupportActionBar().setTitle(R.string.app_name); - supportInvalidateOptionsMenu(); - } - - @Override - public void onDrawerClosed(View drawerView) { - super.onDrawerClosed(drawerView); - getSupportActionBar().setTitle(currentTitle); - supportInvalidateOptionsMenu(); - } - }; - + drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, R.string.drawer_open, R.string.drawer_close); drawerToggle.setDrawerIndicatorEnabled(false); drawerLayout.setDrawerListener(drawerToggle); @@ -463,10 +422,9 @@ public class AudioplayerActivity extends MediaplayerActivity implements ItemDesc public void onItemClick(AdapterView<?> parent, View view, int position, long id) { int viewType = parent.getAdapter().getItemViewType(position); if (viewType != NavListAdapter.VIEW_TYPE_SECTION_DIVIDER) { - int relPos = (viewType == NavListAdapter.VIEW_TYPE_NAV) ? position : position - NavListAdapter.SUBSCRIPTION_OFFSET; Intent intent = new Intent(AudioplayerActivity.this, MainActivity.class); intent.putExtra(MainActivity.EXTRA_NAV_TYPE, viewType); - intent.putExtra(MainActivity.EXTRA_NAV_INDEX, relPos); + intent.putExtra(MainActivity.EXTRA_NAV_INDEX, position); startActivity(intent); } drawerLayout.closeDrawer(navDrawer); @@ -630,9 +588,7 @@ public class AudioplayerActivity extends MediaplayerActivity implements ItemDesc @Override protected void onReloadNotification(int notificationCode) { if (notificationCode == PlaybackService.EXTRA_CODE_VIDEO) { - if (BuildConfig.DEBUG) - Log.d(TAG, - "ReloadNotification received, switching to Videoplayer now"); + Log.d(TAG, "ReloadNotification received, switching to Videoplayer now"); finish(); startActivity(new Intent(this, VideoplayerActivity.class)); @@ -659,24 +615,6 @@ public class AudioplayerActivity extends MediaplayerActivity implements ItemDesc return drawerLayout != null && navDrawer != null && drawerLayout.isDrawerOpen(navDrawer); } - @Override - public boolean onCreateOptionsMenu(Menu menu) { - if (!MenuItemUtils.isActivityDrawerOpen(this)) { - return super.onCreateOptionsMenu(menu); - } else { - return false; - } - } - - @Override - public boolean onPrepareOptionsMenu(Menu menu) { - if (!MenuItemUtils.isActivityDrawerOpen(this)) { - return super.onPrepareOptionsMenu(menu); - } else { - return false; - } - } - public interface AudioplayerContentFragment { public void onDataSetChanged(Playable media); } @@ -729,8 +667,7 @@ public class AudioplayerActivity extends MediaplayerActivity implements ItemDesc @Override public void update(EventDistributor eventDistributor, Integer arg) { if ((EventDistributor.FEED_LIST_UPDATE & arg) != 0) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Received contentUpdate Intent."); + Log.d(TAG, "Received contentUpdate Intent."); loadData(); } } @@ -766,8 +703,13 @@ public class AudioplayerActivity extends MediaplayerActivity implements ItemDesc } @Override - public int getNumberOfUnreadItems() { - return (navDrawerData != null) ? navDrawerData.numUnreadItems : 0; + public int getNumberOfNewItems() { + return (navDrawerData != null) ? navDrawerData.numNewItems : 0; + } + + @Override + public int getNumberOfUnreadFeedItems(long feedId) { + return (navDrawerData != null) ? navDrawerData.numUnreadFeedItems.get(feedId) : 0; } }; } 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 93c71a868..24b684752 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/FeedInfoActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/FeedInfoActivity.java @@ -1,5 +1,7 @@ package de.danoeh.antennapod.activity; +import android.content.ClipData; +import android.content.Context; import android.os.AsyncTask; import android.os.Bundle; import android.support.v7.app.ActionBarActivity; @@ -9,15 +11,17 @@ import android.util.Log; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; +import android.view.View; import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.EditText; import android.widget.ImageView; import android.widget.TextView; +import android.widget.Toast; +import com.joanzapata.android.iconify.Iconify; import com.squareup.picasso.Picasso; -import de.danoeh.antennapod.BuildConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.dialog.DownloadRequestErrorDialogCreator; import de.danoeh.antennapod.core.feed.Feed; @@ -44,10 +48,32 @@ public class FeedInfoActivity extends ActionBarActivity { private TextView txtvDescription; private TextView txtvLanguage; private TextView txtvAuthor; + private TextView txtvUrl; private EditText etxtUsername; private EditText etxtPassword; private CheckBox cbxAutoDownload; + private final View.OnClickListener copyUrlToClipboard = new View.OnClickListener() { + @Override + public void onClick(View v) { + if(feed != null && feed.getDownload_url() != null) { + String url = feed.getDownload_url(); + if (android.os.Build.VERSION.SDK_INT >= 11) { + ClipData clipData = ClipData.newPlainText(url, url); + android.content.ClipboardManager cm = (android.content.ClipboardManager) FeedInfoActivity.this + .getSystemService(Context.CLIPBOARD_SERVICE); + cm.setPrimaryClip(clipData); + } else { + android.text.ClipboardManager cm = (android.text.ClipboardManager) FeedInfoActivity.this + .getSystemService(Context.CLIPBOARD_SERVICE); + cm.setText(url); + } + Toast t = Toast.makeText(FeedInfoActivity.this, R.string.copied_url_msg, Toast.LENGTH_SHORT); + t.show(); + } + } + }; + @Override protected void onCreate(Bundle savedInstanceState) { setTheme(UserPreferences.getTheme()); @@ -61,10 +87,13 @@ public class FeedInfoActivity extends ActionBarActivity { txtvDescription = (TextView) findViewById(R.id.txtvDescription); txtvLanguage = (TextView) findViewById(R.id.txtvLanguage); txtvAuthor = (TextView) findViewById(R.id.txtvAuthor); + txtvUrl = (TextView) findViewById(R.id.txtvUrl); cbxAutoDownload = (CheckBox) findViewById(R.id.cbxAutoDownload); etxtUsername = (EditText) findViewById(R.id.etxtUsername); etxtPassword = (EditText) findViewById(R.id.etxtPassword); + txtvUrl.setOnClickListener(copyUrlToClipboard); + AsyncTask<Long, Void, Feed> loadTask = new AsyncTask<Long, Void, Feed>() { @Override @@ -76,10 +105,9 @@ public class FeedInfoActivity extends ActionBarActivity { protected void onPostExecute(Feed result) { if (result != null) { feed = result; - if (BuildConfig.DEBUG) - Log.d(TAG, "Language is " + feed.getLanguage()); - if (BuildConfig.DEBUG) - Log.d(TAG, "Author is " + feed.getAuthor()); + Log.d(TAG, "Language is " + feed.getLanguage()); + Log.d(TAG, "Author is " + feed.getAuthor()); + Log.d(TAG, "URL is " + feed.getDownload_url()); imgvCover.post(new Runnable() { @Override @@ -92,7 +120,7 @@ public class FeedInfoActivity extends ActionBarActivity { }); txtvTitle.setText(feed.getTitle()); - txtvDescription.setText(feed.getDescription()); + txtvDescription.setText(feed.getDescription().trim()); if (feed.getAuthor() != null) { txtvAuthor.setText(feed.getAuthor()); } @@ -100,6 +128,8 @@ public class FeedInfoActivity extends ActionBarActivity { txtvLanguage.setText(LangUtils .getLanguageString(feed.getLanguage())); } + txtvUrl.setText(feed.getDownload_url() + " {fa-paperclip}"); + Iconify.addIcons(txtvUrl); cbxAutoDownload.setEnabled(UserPreferences.isEnableAutodownload()); cbxAutoDownload.setChecked(feed.getPreferences().getAutoDownload()); diff --git a/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java index a28e4114b..d62612c76 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java @@ -1,8 +1,11 @@ package de.danoeh.antennapod.activity; +import android.app.AlertDialog; +import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Configuration; +import android.database.DataSetObserver; import android.media.AudioManager; import android.os.AsyncTask; import android.os.Bundle; @@ -21,6 +24,7 @@ import android.view.View; import android.widget.AdapterView; import android.widget.ListView; +import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.Validate; import java.util.List; @@ -34,6 +38,7 @@ import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.util.StorageUtils; import de.danoeh.antennapod.fragment.AddFeedFragment; +import de.danoeh.antennapod.fragment.AllEpisodesFragment; import de.danoeh.antennapod.fragment.DownloadsFragment; import de.danoeh.antennapod.fragment.ExternalPlayerFragment; import de.danoeh.antennapod.fragment.ItemlistFragment; @@ -48,7 +53,9 @@ import de.greenrobot.event.EventBus; * The activity that is shown when the user launches the app. */ public class MainActivity extends ActionBarActivity implements NavDrawerActivity { + private static final String TAG = "MainActivity"; + private static final int EVENTS = EventDistributor.DOWNLOAD_HANDLED | EventDistributor.DOWNLOAD_QUEUED | EventDistributor.FEED_LIST_UPDATE @@ -56,21 +63,24 @@ public class MainActivity extends ActionBarActivity implements NavDrawerActivity public static final String PREF_NAME = "MainActivityPrefs"; public static final String PREF_IS_FIRST_LAUNCH = "prefMainActivityIsFirstLaunch"; + public static final String PREF_LAST_FRAGMENT_TAG = "prefMainActivityLastFragmentTag"; - public static final String EXTRA_NAV_INDEX = "nav_index"; public static final String EXTRA_NAV_TYPE = "nav_type"; + public static final String EXTRA_NAV_INDEX = "nav_index"; + public static final String EXTRA_FRAGMENT_TAG = "fragment_tag"; public static final String EXTRA_FRAGMENT_ARGS = "fragment_args"; public static final String SAVE_BACKSTACK_COUNT = "backstackCount"; - public static final String SAVE_SELECTED_NAV_INDEX = "selectedNavIndex"; public static final String SAVE_TITLE = "title"; - - public static final int POS_QUEUE = 0, - POS_NEW = 1, - POS_DOWNLOADS = 2, - POS_HISTORY = 3, - POS_ADD = 4; + public static final String[] NAV_DRAWER_TAGS = { + QueueFragment.TAG, + NewEpisodesFragment.TAG, + AllEpisodesFragment.TAG, + DownloadsFragment.TAG, + PlaybackHistoryFragment.TAG, + AddFeedFragment.TAG + }; private Toolbar toolbar; private ExternalPlayerFragment externalPlayerFragment; @@ -85,7 +95,6 @@ public class MainActivity extends ActionBarActivity implements NavDrawerActivity private CharSequence drawerTitle; private CharSequence currentTitle; - @Override public void onCreate(Bundle savedInstanceState) { setTheme(UserPreferences.getNoTitleTheme()); @@ -103,30 +112,12 @@ public class MainActivity extends ActionBarActivity implements NavDrawerActivity drawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout); navList = (ListView) findViewById(R.id.nav_list); navDrawer = findViewById(R.id.nav_layout); - Log.i(TAG, ""); - drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, R.string.drawer_open, R.string.drawer_close) { - @Override - public void onDrawerOpened(View drawerView) { - super.onDrawerOpened(drawerView); - currentTitle = getSupportActionBar().getTitle(); - getSupportActionBar().setTitle(drawerTitle); - supportInvalidateOptionsMenu(); - } - - @Override - public void onDrawerClosed(View drawerView) { - super.onDrawerClosed(drawerView); - getSupportActionBar().setTitle(currentTitle); - supportInvalidateOptionsMenu(); - - } - }; + drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, R.string.drawer_open, R.string.drawer_close); if (savedInstanceState != null) { int backstackCount = savedInstanceState.getInt(SAVE_BACKSTACK_COUNT, 0); drawerToggle.setDrawerIndicatorEnabled(backstackCount == 0); } - drawerLayout.setDrawerListener(drawerToggle); final FragmentManager fm = getSupportFragmentManager(); @@ -138,25 +129,20 @@ public class MainActivity extends ActionBarActivity implements NavDrawerActivity } }); - FragmentTransaction transaction = fm.beginTransaction(); - - Fragment mainFragment = fm.findFragmentByTag("main"); - if (mainFragment != null) { - transaction.replace(R.id.main_view, mainFragment); - } else { - loadFragment(NavListAdapter.VIEW_TYPE_NAV, POS_QUEUE, null); - } - - externalPlayerFragment = new ExternalPlayerFragment(); - transaction.replace(R.id.playerFragment, externalPlayerFragment); - transaction.commit(); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setHomeButtonEnabled(true); navAdapter = new NavListAdapter(itemAccess, this); navList.setAdapter(navAdapter); navList.setOnItemClickListener(navListClickListener); + navList.setOnItemLongClickListener(newListLongClickListener); + + navAdapter.registerDataSetObserver(new DataSetObserver() { + @Override + public void onChanged() { + selectedNavListIndex = getSelectedNavListIndex(); + } + }); findViewById(R.id.nav_settings).setOnClickListener(new View.OnClickListener() { @Override @@ -166,9 +152,42 @@ public class MainActivity extends ActionBarActivity implements NavDrawerActivity } }); + FragmentTransaction transaction = fm.beginTransaction(); + + Fragment mainFragment = fm.findFragmentByTag("main"); + if (mainFragment != null) { + transaction.replace(R.id.main_view, mainFragment); + } else { + String lastFragment = getLastNavFragment(); + if(ArrayUtils.contains(NAV_DRAWER_TAGS, lastFragment)) { + loadFragment(lastFragment, null); + } + // else: lastFragment contains feed id - drawer data is not loaded yet, + // so loading is postponed until then + } + externalPlayerFragment = new ExternalPlayerFragment(); + transaction.replace(R.id.playerFragment, externalPlayerFragment); + transaction.commit(); + checkFirstLaunch(); } + private void saveLastNavFragment(String tag) { + SharedPreferences prefs = getSharedPreferences(PREF_NAME, MODE_PRIVATE); + SharedPreferences.Editor edit = prefs.edit(); + if(tag != null) { + edit.putString(PREF_LAST_FRAGMENT_TAG, tag); + } else { + edit.remove(PREF_LAST_FRAGMENT_TAG); + } + edit.commit(); + } + + private String getLastNavFragment() { + SharedPreferences prefs = getSharedPreferences(PREF_NAME, MODE_PRIVATE); + return prefs.getString(PREF_LAST_FRAGMENT_TAG, QueueFragment.TAG); + } + private void checkFirstLaunch() { SharedPreferences prefs = getSharedPreferences(PREF_NAME, MODE_PRIVATE); if (prefs.getBoolean(PREF_IS_FIRST_LAUNCH, true)) { @@ -185,6 +204,40 @@ public class MainActivity extends ActionBarActivity implements NavDrawerActivity } } + public void showDrawerPreferencesDialog() { + final List<String> hiddenDrawerItems = UserPreferences.getHiddenDrawerItems(); + String[] navLabels = new String[NAV_DRAWER_TAGS.length]; + final boolean[] checked = new boolean[NAV_DRAWER_TAGS.length]; + for (int i = 0; i < NAV_DRAWER_TAGS.length; i++) { + String tag = NAV_DRAWER_TAGS[i]; + navLabels[i] = navAdapter.getLabel(tag); + if (!hiddenDrawerItems.contains(tag)) { + checked[i] = true; + } + } + + AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this); + builder.setTitle(R.string.drawer_preferences); + builder.setMultiChoiceItems(navLabels, checked, new DialogInterface.OnMultiChoiceClickListener() { + @Override + public void onClick(DialogInterface dialog, int which, boolean isChecked) { + if (isChecked) { + hiddenDrawerItems.remove(NAV_DRAWER_TAGS[which]); + } else { + hiddenDrawerItems.add(NAV_DRAWER_TAGS[which]); + } + } + }); + builder.setPositiveButton(R.string.confirm_label, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + UserPreferences.setHiddenDrawerItems(MainActivity.this, hiddenDrawerItems); + } + }); + builder.setNegativeButton(R.string.cancel_label, null); + builder.create().show(); + } + public ActionBar getMainActivtyActionBar() { return getSupportActionBar(); } @@ -197,70 +250,91 @@ public class MainActivity extends ActionBarActivity implements NavDrawerActivity return (navDrawerData != null) ? navDrawerData.feeds : null; } - private void loadFragment(int viewType, int relPos, Bundle args) { - FragmentManager fragmentManager = getSupportFragmentManager(); - // clear back stack - for (int i = 0; i < fragmentManager.getBackStackEntryCount(); i++) { - fragmentManager.popBackStack(); + public void loadFragment(int index, Bundle args) { + if (index < navAdapter.getSubscriptionOffset()) { + String tag = navAdapter.getTags().get(index); + loadFragment(tag, args); + } else { + int pos = index - navAdapter.getSubscriptionOffset(); + loadFeedFragmentByPosition(pos, args); } + } - FragmentTransaction fT = fragmentManager.beginTransaction(); + public void loadFragment(final String tag, Bundle args) { + Log.d(TAG, "loadFragment(\"" + tag + "\", " + args + ")"); Fragment fragment = null; - if (viewType == NavListAdapter.VIEW_TYPE_NAV) { - switch (relPos) { - case POS_NEW: - fragment = new NewEpisodesFragment(); - break; - case POS_QUEUE: - fragment = new QueueFragment(); - break; - case POS_DOWNLOADS: - fragment = new DownloadsFragment(); - break; - case POS_HISTORY: - fragment = new PlaybackHistoryFragment(); - break; - case POS_ADD: - fragment = new AddFeedFragment(); - break; - - } - currentTitle = getString(NavListAdapter.NAV_TITLES[relPos]); - selectedNavListIndex = relPos; - - } else if (viewType == NavListAdapter.VIEW_TYPE_SUBSCRIPTION) { - Feed feed = itemAccess.getItem(relPos); - currentTitle = ""; - fragment = ItemlistFragment.newInstance(feed.getId()); - selectedNavListIndex = NavListAdapter.SUBSCRIPTION_OFFSET + relPos; - + switch (tag) { + case QueueFragment.TAG: + fragment = new QueueFragment(); + break; + case NewEpisodesFragment.TAG: + fragment = new NewEpisodesFragment(); + break; + case AllEpisodesFragment.TAG: + fragment = new AllEpisodesFragment(); + break; + case DownloadsFragment.TAG: + fragment = new DownloadsFragment(); + break; + case PlaybackHistoryFragment.TAG: + fragment = new PlaybackHistoryFragment(); + break; + case AddFeedFragment.TAG: + fragment = new AddFeedFragment(); + break; } - if (fragment != null) { - if (args != null) { - fragment.setArguments(args); - } - fT.replace(R.id.main_view, fragment, "main"); - fragmentManager.popBackStack(); - } - fT.commit(); + currentTitle = navAdapter.getLabel(tag); getSupportActionBar().setTitle(currentTitle); - if (navAdapter != null) { - navAdapter.notifyDataSetChanged(); + saveLastNavFragment(tag); + if (args != null) { + fragment.setArguments(args); } + loadFragment(fragment); } - public void loadNavFragment(int position, Bundle args) { - loadFragment(NavListAdapter.VIEW_TYPE_NAV, position, args); + private void loadFeedFragmentByPosition(int relPos, Bundle args) { + if(relPos < 0) { + return; + } + Feed feed = itemAccess.getItem(relPos); + long feedId = feed.getId(); + Fragment fragment = ItemlistFragment.newInstance(feedId); + if(args != null) { + fragment.setArguments(args); + } + saveLastNavFragment(String.valueOf(feed.getId())); + currentTitle = ""; + getSupportActionBar().setTitle(currentTitle); + loadFragment(fragment); } - public void loadFeedFragment(long feedID) { + public void loadFeedFragmentById(long feedId) { if (navDrawerData != null) { - for (int i = 0; i < navDrawerData.feeds.size(); i++) { - if (navDrawerData.feeds.get(i).getId() == feedID) { - loadFragment(NavListAdapter.VIEW_TYPE_SUBSCRIPTION, i, null); - break; + int relPos = -1; + List<Feed> feeds = navDrawerData.feeds; + for (int i = 0; relPos < 0 && i < feeds.size(); i++) { + if (feeds.get(i).getId() == feedId) { + relPos = i; } } + if(relPos >= 0) { + loadFeedFragmentByPosition(relPos, null); + } + } + } + + private void loadFragment(Fragment fragment) { + FragmentManager fragmentManager = getSupportFragmentManager(); + // clear back stack + for (int i = 0; i < fragmentManager.getBackStackEntryCount(); i++) { + fragmentManager.popBackStack(); + } + FragmentTransaction t = fragmentManager.beginTransaction(); + t.replace(R.id.main_view, fragment, "main"); + fragmentManager.popBackStack(); + t.commit(); + if (navAdapter != null) { + navAdapter.notifyDataSetChanged(); } } @@ -281,20 +355,50 @@ public class MainActivity extends ActionBarActivity implements NavDrawerActivity return toolbar; } + private int getSelectedNavListIndex() { + String lastFragment = getLastNavFragment(); + int tagIndex = navAdapter.getTags().indexOf(lastFragment); + if(tagIndex >= 0) { + return tagIndex; + } else if(ArrayUtils.contains(NAV_DRAWER_TAGS, lastFragment)) { + // the fragment was just hidden + return -1; + } else { // last fragment was not a list, but a feed + long feedId = Long.parseLong(lastFragment); + List<Feed> feeds = navDrawerData.feeds; + for (int i = 0; i < feeds.size(); i++) { + if (feeds.get(i).getId() == feedId) { + return i + navAdapter.getSubscriptionOffset(); + } + } + return -1; + } + } + private AdapterView.OnItemClickListener navListClickListener = new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { int viewType = parent.getAdapter().getItemViewType(position); if (viewType != NavListAdapter.VIEW_TYPE_SECTION_DIVIDER && position != selectedNavListIndex) { - int relPos = (viewType == NavListAdapter.VIEW_TYPE_NAV) ? position : position - NavListAdapter.SUBSCRIPTION_OFFSET; - loadFragment(viewType, relPos, null); - selectedNavListIndex = position; - navAdapter.notifyDataSetChanged(); + loadFragment(position, null); } drawerLayout.closeDrawer(navDrawer); } }; + private AdapterView.OnItemLongClickListener newListLongClickListener = new AdapterView.OnItemLongClickListener() { + @Override + public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) { + if(position < navAdapter.getTags().size()) { + showDrawerPreferencesDialog(); + return true; + } else { + return false; + } + } + }; + + @Override protected void onPostCreate(Bundle savedInstanceState) { super.onPostCreate(savedInstanceState); @@ -304,7 +408,7 @@ public class MainActivity extends ActionBarActivity implements NavDrawerActivity if (!drawerLayout.isDrawerOpen(navDrawer)) { getSupportActionBar().setTitle(currentTitle); } - selectedNavListIndex = savedInstanceState.getInt(SAVE_SELECTED_NAV_INDEX); + selectedNavListIndex = getSelectedNavListIndex(); } } @@ -318,9 +422,7 @@ public class MainActivity extends ActionBarActivity implements NavDrawerActivity protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putString(SAVE_TITLE, getSupportActionBar().getTitle().toString()); - outState.putInt(SAVE_SELECTED_NAV_INDEX, selectedNavListIndex); outState.putInt(SAVE_BACKSTACK_COUNT, getSupportFragmentManager().getBackStackEntryCount()); - } @Override @@ -341,7 +443,8 @@ public class MainActivity extends ActionBarActivity implements NavDrawerActivity StorageUtils.checkStorageAvailability(this); Intent intent = getIntent(); - if (navDrawerData != null && intent.hasExtra(EXTRA_NAV_INDEX) && intent.hasExtra(EXTRA_NAV_TYPE)) { + if (navDrawerData != null && intent.hasExtra(EXTRA_NAV_TYPE) && + (intent.hasExtra(EXTRA_NAV_INDEX) || intent.hasExtra(EXTRA_FRAGMENT_TAG))) { handleNavIntent(); } @@ -370,7 +473,6 @@ public class MainActivity extends ActionBarActivity implements NavDrawerActivity } } - private DBReader.NavDrawerData navDrawerData; private AsyncTask<Void, Void, DBReader.NavDrawerData> loadTask; private int selectedNavListIndex = 0; @@ -405,10 +507,14 @@ public class MainActivity extends ActionBarActivity implements NavDrawerActivity } @Override - public int getNumberOfUnreadItems() { - return (navDrawerData != null) ? navDrawerData.numUnreadItems : 0; + public int getNumberOfNewItems() { + return (navDrawerData != null) ? navDrawerData.numNewItems : 0; } + @Override + public int getNumberOfUnreadFeedItems(long feedId) { + return (navDrawerData != null) ? navDrawerData.numUnreadFeedItems.get(feedId) : 0; + } }; @@ -428,6 +534,13 @@ public class MainActivity extends ActionBarActivity implements NavDrawerActivity navDrawerData = result; navAdapter.notifyDataSetChanged(); + String lastFragment = getLastNavFragment(); + if(!ArrayUtils.contains(NAV_DRAWER_TAGS, lastFragment)) { + long feedId = Long.valueOf(lastFragment); + loadFeedFragmentById(feedId); + saveLastNavFragment(null); + } + if (handleIntent) { handleNavIntent(); } @@ -459,12 +572,18 @@ public class MainActivity extends ActionBarActivity implements NavDrawerActivity }; private void handleNavIntent() { + Log.d(TAG, "handleNavIntent()"); Intent intent = getIntent(); - if (intent.hasExtra(EXTRA_NAV_INDEX) && intent.hasExtra(EXTRA_NAV_TYPE)) { - int index = intent.getIntExtra(EXTRA_NAV_INDEX, 0); - int type = intent.getIntExtra(EXTRA_NAV_TYPE, NavListAdapter.VIEW_TYPE_NAV); + if (intent.hasExtra(EXTRA_NAV_TYPE) && + intent.hasExtra(EXTRA_NAV_INDEX) || intent.hasExtra(EXTRA_FRAGMENT_TAG)) { + int index = intent.getIntExtra(EXTRA_NAV_INDEX, -1); + String tag = intent.getStringExtra(EXTRA_FRAGMENT_TAG); Bundle args = intent.getBundleExtra(EXTRA_FRAGMENT_ARGS); - loadFragment(type, index, args); + if (index >= 0) { + loadFragment(index, args); + } else if (tag != null) { + loadFragment(tag, args); + } } setIntent(new Intent(MainActivity.this, MainActivity.class)); // to avoid handling the intent twice when the configuration changes } diff --git a/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java index 8edd4185c..0cd388b9d 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java @@ -12,6 +12,8 @@ import android.util.Log; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; + +import android.view.View; import android.widget.ImageButton; import android.widget.SeekBar; import android.widget.SeekBar.OnSeekBarChangeListener; @@ -46,7 +48,9 @@ public abstract class MediaplayerActivity extends ActionBarActivity protected SeekBar sbPosition; protected ImageButton butPlay; protected ImageButton butRev; + protected TextView txtvRev; protected ImageButton butFF; + protected TextView txtvFF; private PlaybackController newPlaybackController() { return new PlaybackController(this, false) { @@ -222,7 +226,6 @@ public abstract class MediaplayerActivity extends ActionBarActivity protected void onStop() { super.onStop(); Log.d(TAG, "onStop()"); - if (controller != null) { controller.release(); } @@ -373,6 +376,8 @@ public abstract class MediaplayerActivity extends ActionBarActivity if (controller != null) { int currentPosition = controller.getPosition(); int duration = controller.getDuration(); + Log.d(TAG, "currentPosition " + Converter + .getDurationStringLong(currentPosition)); if (currentPosition != PlaybackService.INVALID_TIME && duration != PlaybackService.INVALID_TIME && controller.getMedia() != null) { @@ -381,8 +386,7 @@ public abstract class MediaplayerActivity extends ActionBarActivity txtvLength.setText(Converter.getDurationStringLong(duration)); updateProgressbarPosition(currentPosition, duration); } else { - Log.w(TAG, - "Could not react to position observer update because of invalid time"); + Log.w(TAG, "Could not react to position observer update because of invalid time"); } } } @@ -426,7 +430,15 @@ public abstract class MediaplayerActivity extends ActionBarActivity txtvLength = (TextView) findViewById(R.id.txtvLength); butPlay = (ImageButton) findViewById(R.id.butPlay); butRev = (ImageButton) findViewById(R.id.butRev); + txtvRev = (TextView) findViewById(R.id.txtvRev); + if(txtvRev != null) { + txtvRev.setText(String.valueOf(UserPreferences.getRewindSecs())); + } butFF = (ImageButton) findViewById(R.id.butFF); + txtvFF = (TextView) findViewById(R.id.txtvFF); + if(txtvFF != null) { + txtvFF.setText(String.valueOf(UserPreferences.getFastFowardSecs())); + } // SEEKBAR SETUP @@ -437,10 +449,100 @@ public abstract class MediaplayerActivity extends ActionBarActivity butPlay.setOnClickListener(controller.newOnPlayButtonClickListener()); if (butFF != null) { - butFF.setOnClickListener(controller.newOnFFButtonClickListener()); + butFF.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + int curr = controller.getPosition(); + controller.seekTo(curr + UserPreferences.getFastFowardSecs() * 1000); + } + }); + butFF.setOnLongClickListener(new View.OnLongClickListener() { + + int choice; + + @Override + public boolean onLongClick(View v) { + int checked = 0; + int rewindSecs = UserPreferences.getFastFowardSecs(); + final int[] values = getResources().getIntArray(R.array.seek_delta_values); + final String[] choices = new String[values.length]; + for(int i=0; i < values.length; i++) { + if (rewindSecs == values[i]) { + checked = i; + } + choices[i] = String.valueOf(values[i]) + " " + + getString(R.string.time_unit_seconds); + } + choice = values[checked]; + AlertDialog.Builder builder = new AlertDialog.Builder(MediaplayerActivity.this); + builder.setTitle(R.string.pref_fast_forward); + builder.setSingleChoiceItems(choices, checked, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + choice = values[which]; + } + }); + builder.setNegativeButton(R.string.cancel_label, null); + builder.setPositiveButton(R.string.confirm_label, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + UserPreferences.setPrefFastForwardSecs(choice); + txtvFF.setText(String.valueOf(choice)); + } + }); + builder.create().show(); + return true; + } + }); } if (butRev != null) { - butRev.setOnClickListener(controller.newOnRevButtonClickListener()); + butRev.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + int curr = controller.getPosition(); + controller.seekTo(curr - UserPreferences.getRewindSecs() * 1000); + } + }); + butRev.setOnLongClickListener(new View.OnLongClickListener() { + + int choice; + + @Override + public boolean onLongClick(View v) { + int checked = 0; + int rewindSecs = UserPreferences.getRewindSecs(); + final int[] values = getResources().getIntArray(R.array.seek_delta_values); + final String[] choices = new String[values.length]; + for(int i=0; i < values.length; i++) { + if (rewindSecs == values[i]) { + checked = i; + } + choices[i] = String.valueOf(values[i]) + " " + + getString(R.string.time_unit_seconds); + } + choice = values[checked]; + AlertDialog.Builder builder = new AlertDialog.Builder(MediaplayerActivity.this); + builder.setTitle(R.string.pref_rewind); + builder.setSingleChoiceItems(choices, checked, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + choice = values[which]; + } + }); + builder.setNegativeButton(R.string.cancel_label, null); + builder.setPositiveButton(R.string.confirm_label, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + UserPreferences.setPrefRewindSecs(choice); + txtvRev.setText(String.valueOf(choice)); + } + }); + builder.create().show(); + return true; + } + }); } } diff --git a/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java index 3b03ed2db..2b1b13ae6 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java @@ -26,7 +26,6 @@ import java.util.Map; import javax.xml.parsers.ParserConfigurationException; -import de.danoeh.antennapod.BuildConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.feed.FeedPreferences; @@ -91,12 +90,10 @@ public abstract class OnlineFeedViewActivity extends ActionBarActivity { getSupportActionBar().setTitle(R.string.add_new_feed_label); } else { - throw new IllegalArgumentException( - "Activity must be started with feedurl argument!"); + throw new IllegalArgumentException("Activity must be started with feedurl argument!"); } - if (BuildConfig.DEBUG) - Log.d(TAG, "Activity was started with url " + feedUrl); + Log.d(TAG, "Activity was started with url " + feedUrl); setLoadingLayout(); if (savedInstanceState == null) { startFeedDownload(feedUrl, null, null); @@ -147,7 +144,7 @@ public abstract class OnlineFeedViewActivity extends ActionBarActivity { @Override public void run() { - if (BuildConfig.DEBUG) Log.d(TAG, "Download was completed"); + Log.d(TAG, "Download was completed"); DownloadStatus status = downloader.getResult(); if (status != null) { if (!status.isCancelled()) { @@ -164,15 +161,13 @@ public abstract class OnlineFeedViewActivity extends ActionBarActivity { OnlineFeedViewActivity.this); if (errorMsg != null && status.getReasonDetailed() != null) { - errorMsg += " (" - + status.getReasonDetailed() + ")"; + errorMsg += " (" + status.getReasonDetailed() + ")"; } showErrorDialog(errorMsg); } } } else { - Log.wtf(TAG, - "DownloadStatus returned by Downloader was null"); + Log.wtf(TAG, "DownloadStatus returned by Downloader was null"); finish(); } } @@ -181,21 +176,18 @@ public abstract class OnlineFeedViewActivity extends ActionBarActivity { } private void startFeedDownload(String url, String username, String password) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Starting feed download"); + Log.d(TAG, "Starting feed download"); url = URLChecker.prepareURL(url); feed = new Feed(url, new Date(0)); if (username != null && password != null) { feed.setPreferences(new FeedPreferences(0, false, username, password)); } String fileUrl = new File(getExternalCacheDir(), - FileNameGenerator.generateFileName(feed.getDownload_url())) - .toString(); + FileNameGenerator.generateFileName(feed.getDownload_url())).toString(); feed.setFile_url(fileUrl); final DownloadRequest request = new DownloadRequest(feed.getFile_url(), feed.getDownload_url(), "OnlineFeed", 0, Feed.FEEDFILETYPE_FEED, username, password, true, null); - downloader = new HttpDownloader( - request); + downloader = new HttpDownloader(request); new Thread() { @Override public void run() { @@ -233,8 +225,7 @@ public abstract class OnlineFeedViewActivity extends ActionBarActivity { "feed must be non-null and downloaded when parseFeed is called"); } - if (BuildConfig.DEBUG) - Log.d(TAG, "Parsing feed"); + Log.d(TAG, "Parsing feed"); Thread thread = new Thread() { @@ -258,7 +249,7 @@ public abstract class OnlineFeedViewActivity extends ActionBarActivity { e.printStackTrace(); reasonDetailed = e.getMessage(); } catch (UnsupportedFeedtypeException e) { - if (BuildConfig.DEBUG) Log.d(TAG, "Unsupported feed type detected"); + Log.d(TAG, "Unsupported feed type detected"); if (StringUtils.equalsIgnoreCase("html", e.getRootElement())) { if (showFeedDiscoveryDialog(new File(feed.getFile_url()), feed.getDownload_url())) { return; @@ -269,8 +260,7 @@ public abstract class OnlineFeedViewActivity extends ActionBarActivity { } } finally { boolean rc = new File(feed.getFile_url()).delete(); - if (BuildConfig.DEBUG) - Log.d(TAG, "Deleted feed source file. Result: " + rc); + Log.d(TAG, "Deleted feed source file. Result: " + rc); } if (successful) { @@ -386,7 +376,12 @@ public abstract class OnlineFeedViewActivity extends ActionBarActivity { String selectedUrl = urls.get(which); dialog.dismiss(); resetIntent(selectedUrl, titles.get(which)); - startFeedDownload(selectedUrl, null, null); + FeedPreferences prefs = feed.getPreferences(); + if(prefs != null) { + startFeedDownload(selectedUrl, prefs.getUsername(), prefs.getPassword()); + } else { + startFeedDownload(selectedUrl, null, null); + } } }; diff --git a/app/src/main/java/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java index d7b069b19..511115b3c 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java @@ -11,7 +11,20 @@ import android.util.Log; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; -import android.widget.*; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ProgressBar; +import android.widget.Spinner; +import android.widget.TextView; +import android.widget.ViewFlipper; + +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + import de.danoeh.antennapod.BuildConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; @@ -22,11 +35,6 @@ import de.danoeh.antennapod.core.preferences.GpodnetPreferences; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.GpodnetSyncService; -import java.security.SecureRandom; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.atomic.AtomicReference; - /** * Guides the user through the authentication process * Step 1: Request username and password from user diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/ActionButtonUtils.java b/app/src/main/java/de/danoeh/antennapod/adapter/ActionButtonUtils.java index 8d3e73429..8e347a819 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/ActionButtonUtils.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/ActionButtonUtils.java @@ -10,7 +10,9 @@ import org.apache.commons.lang3.Validate; import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DownloadRequester; +import de.danoeh.antennapod.core.util.LongList; /** * Utility methods for the action button that is displayed on the right hand side @@ -26,9 +28,21 @@ public class ActionButtonUtils { Validate.notNull(context); this.context = context; - drawables = context.obtainStyledAttributes(new int[]{ - R.attr.av_play, R.attr.navigation_cancel, R.attr.av_download, R.attr.av_pause, R.attr.navigation_accept}); - labels = new int[]{R.string.play_label, R.string.cancel_download_label, R.string.download_label, R.string.mark_read_label}; + drawables = context.obtainStyledAttributes(new int[] { + R.attr.av_play, + R.attr.navigation_cancel, + R.attr.av_download, + R.attr.av_pause, + R.attr.navigation_accept, + R.attr.content_new + }); + labels = new int[] { + R.string.play_label, + R.string.cancel_download_label, + R.string.download_label, + R.string.mark_read_label, + R.string.add_to_queue_label + }; } /** @@ -50,18 +64,26 @@ public class ActionButtonUtils { butSecondary.setContentDescription(context.getString(labels[1])); } else { // item is not downloaded and not being downloaded - butSecondary.setVisibility(View.VISIBLE); - butSecondary.setImageDrawable(drawables.getDrawable(2)); - butSecondary.setContentDescription(context.getString(labels[2])); + LongList queueIds = DBReader.getQueueIDList(context); + if(DefaultActionButtonCallback.userAllowedMobileDownloads() || + !DefaultActionButtonCallback.userChoseAddToQueue() || queueIds.contains(item.getId())) { + butSecondary.setVisibility(View.VISIBLE); + butSecondary.setImageDrawable(drawables.getDrawable(2)); + butSecondary.setContentDescription(context.getString(labels[2])); + } else { + // mobile download not allowed yet, item is not in queue and user chose add to queue + butSecondary.setVisibility(View.VISIBLE); + butSecondary.setImageDrawable(drawables.getDrawable(5)); + butSecondary.setContentDescription(context.getString(labels[4])); + } } } else { - // item is not being downloaded + // item is downloaded butSecondary.setVisibility(View.VISIBLE); if (media.isCurrentlyPlaying()) { butSecondary.setImageDrawable(drawables.getDrawable(3)); } else { - butSecondary - .setImageDrawable(drawables.getDrawable(0)); + butSecondary.setImageDrawable(drawables.getDrawable(0)); } butSecondary.setContentDescription(context.getString(labels[0])); } diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/NewEpisodesListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/AllEpisodesListAdapter.java index 2d481a7ef..ea0c96be9 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/NewEpisodesListAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/AllEpisodesListAdapter.java @@ -1,7 +1,6 @@ package de.danoeh.antennapod.adapter; import android.content.Context; -import android.graphics.Color; import android.text.format.DateUtils; import android.view.LayoutInflater; import android.view.View; @@ -12,11 +11,9 @@ import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.TextView; -import com.nineoldandroids.view.ViewHelper; import com.squareup.picasso.Picasso; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.feed.EventDistributor; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.storage.DownloadRequester; @@ -25,19 +22,22 @@ import de.danoeh.antennapod.core.util.Converter; /** * List adapter for the list of new episodes */ -public class NewEpisodesListAdapter extends BaseAdapter { +public class AllEpisodesListAdapter extends BaseAdapter { private final Context context; private final ItemAccess itemAccess; private final ActionButtonCallback actionButtonCallback; private final ActionButtonUtils actionButtonUtils; + private final boolean showOnlyNewEpisodes; - public NewEpisodesListAdapter(Context context, ItemAccess itemAccess, ActionButtonCallback actionButtonCallback) { + public AllEpisodesListAdapter(Context context, ItemAccess itemAccess, ActionButtonCallback actionButtonCallback, + boolean showOnlyNewEpisodes) { super(); this.context = context; this.itemAccess = itemAccess; this.actionButtonUtils = new ActionButtonUtils(context); this.actionButtonCallback = actionButtonCallback; + this.showOnlyNewEpisodes = showOnlyNewEpisodes; } @Override @@ -91,7 +91,7 @@ public class NewEpisodesListAdapter extends BaseAdapter { holder.title.setText(item.getTitle()); holder.pubDate.setText(DateUtils.formatDateTime(context, item.getPubDate().getTime(), DateUtils.FORMAT_ABBREV_ALL)); - if (item.isRead()) { + if (showOnlyNewEpisodes || item.isRead() || false == itemAccess.isNew(item)) { holder.statusUnread.setVisibility(View.INVISIBLE); } else { holder.statusUnread.setVisibility(View.VISIBLE); @@ -125,7 +125,11 @@ public class NewEpisodesListAdapter extends BaseAdapter { holder.downloadProgress.setProgress(itemAccess.getItemDownloadProgressPercent(item)); } } + } else { + holder.downloadProgress.setVisibility(View.GONE); + holder.txtvDuration.setVisibility(View.GONE); } + if (itemAccess.isInQueue(item)) { holder.queueStatus.setVisibility(View.VISIBLE); } else { @@ -142,13 +146,6 @@ public class NewEpisodesListAdapter extends BaseAdapter { .fit() .into(holder.imageView); - if (item.isRead()) { - // grey it out - ViewHelper.setAlpha(convertView, .2f); - } else { - ViewHelper.setAlpha(convertView, 1.0f); - } - return convertView; } @@ -181,5 +178,8 @@ public class NewEpisodesListAdapter extends BaseAdapter { int getItemDownloadProgressPercent(FeedItem item); boolean isInQueue(FeedItem item); + + boolean isNew(FeedItem item); + } } diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/DefaultActionButtonCallback.java b/app/src/main/java/de/danoeh/antennapod/adapter/DefaultActionButtonCallback.java index d3843934a..3d233817b 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/DefaultActionButtonCallback.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/DefaultActionButtonCallback.java @@ -1,6 +1,8 @@ package de.danoeh.antennapod.adapter; +import android.app.AlertDialog; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.widget.Toast; @@ -10,45 +12,77 @@ import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.dialog.DownloadRequestErrorDialogCreator; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; + +import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction; import de.danoeh.antennapod.core.preferences.GpodnetPreferences; +import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.storage.DownloadRequestException; import de.danoeh.antennapod.core.storage.DownloadRequester; +import de.danoeh.antennapod.core.util.LongList; +import de.danoeh.antennapod.core.util.NetworkUtils; /** * Default implementation of an ActionButtonCallback */ public class DefaultActionButtonCallback implements ActionButtonCallback { + private static final String TAG = "DefaultActionButtonCallback"; private final Context context; + private static final int TEN_MINUTES_IN_MILLIS = 60 * 1000 * 10; + + // remember timestamp when user allowed downloading via mobile connection + private static long allowMobileDownloadsTimestamp; + private static long onlyAddToQueueTimeStamp; + public DefaultActionButtonCallback(Context context) { Validate.notNull(context); this.context = context; } + public static boolean userAllowedMobileDownloads() { + return System.currentTimeMillis() - allowMobileDownloadsTimestamp < TEN_MINUTES_IN_MILLIS; + } + + public static boolean userChoseAddToQueue() { + return System.currentTimeMillis() - onlyAddToQueueTimeStamp < TEN_MINUTES_IN_MILLIS; + } + @Override public void onActionButtonPressed(final FeedItem item) { - if (item.hasMedia()) { final FeedMedia media = item.getMedia(); boolean isDownloading = DownloadRequester.getInstance().isDownloadingFile(media); if (!isDownloading && !media.isDownloaded()) { - try { - DBTasks.downloadFeedItems(context, item); - Toast.makeText(context, R.string.status_downloading_label, Toast.LENGTH_SHORT).show(); - } catch (DownloadRequestException e) { - e.printStackTrace(); - DownloadRequestErrorDialogCreator.newRequestErrorDialog(context, e.getMessage()); + LongList queueIds = DBReader.getQueueIDList(context); + if (NetworkUtils.isDownloadAllowed(context) || userAllowedMobileDownloads()) { + try { + DBTasks.downloadFeedItems(context, item); + Toast.makeText(context, R.string.status_downloading_label, Toast.LENGTH_SHORT).show(); + } catch (DownloadRequestException e) { + e.printStackTrace(); + DownloadRequestErrorDialogCreator.newRequestErrorDialog(context, e.getMessage()); + } + } else if(userChoseAddToQueue() && !queueIds.contains(item.getId())) { + DBWriter.addQueueItem(context, item.getId()); + Toast.makeText(context, R.string.added_to_queue_label, Toast.LENGTH_SHORT).show(); + } else { + confirmMobileDownload(context, item); } } else if (isDownloading) { DownloadRequester.getInstance().cancelDownload(context, media); - Toast.makeText(context, R.string.download_cancelled_msg, Toast.LENGTH_SHORT).show(); + if(UserPreferences.isEnableAutodownload()) { + DBWriter.setFeedItemAutoDownload(context, media.getItem(), false); + Toast.makeText(context, R.string.download_canceled_autodownload_enabled_msg, Toast.LENGTH_LONG).show(); + } else { + Toast.makeText(context, R.string.download_canceled_msg, Toast.LENGTH_LONG).show(); + } } else { // media is downloaded if (item.hasMedia() && item.getMedia().isCurrentlyPlaying()) { context.sendBroadcast(new Intent(PlaybackService.ACTION_PAUSE_PLAY_CURRENT_EPISODE)); @@ -79,4 +113,43 @@ public class DefaultActionButtonCallback implements ActionButtonCallback { } } } + + private void confirmMobileDownload(final Context context, final FeedItem item) { + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder + .setTitle(R.string.confirm_mobile_download_dialog_title) + .setMessage(context.getText(R.string.confirm_mobile_download_dialog_message)) + .setPositiveButton(R.string.confirm_mobile_download_dialog_enable_temporarily, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + allowMobileDownloadsTimestamp = System.currentTimeMillis(); + try { + DBTasks.downloadFeedItems(context, item); + Toast.makeText(context, R.string.status_downloading_label, Toast.LENGTH_SHORT).show(); + } catch (DownloadRequestException e) { + e.printStackTrace(); + DownloadRequestErrorDialogCreator.newRequestErrorDialog(context, e.getMessage()); + } + } + }); + LongList queueIds = DBReader.getQueueIDList(context); + if(!queueIds.contains(item.getId())) { + builder.setNeutralButton(R.string.confirm_mobile_download_dialog_only_add_to_queue, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + onlyAddToQueueTimeStamp = System.currentTimeMillis(); + DBWriter.addQueueItem(context, item.getId()); + Toast.makeText(context, R.string.added_to_queue_label, Toast.LENGTH_SHORT).show(); + } + }) + .setMessage(context.getText(R.string.confirm_mobile_download_dialog_message_not_in_queue)); + } else { + builder.setMessage(context.getText(R.string.confirm_mobile_download_dialog_message)); + } + builder.setNegativeButton(R.string.cancel_label, null) + .create() + .show(); + } } diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/DownloadLogAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/DownloadLogAdapter.java index f982e86ce..f29cfdf2f 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/DownloadLogAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/DownloadLogAdapter.java @@ -2,20 +2,34 @@ package de.danoeh.antennapod.adapter; import android.content.Context; import android.text.format.DateUtils; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; +import android.widget.Button; import android.widget.TextView; +import android.widget.Toast; + +import com.joanzapata.android.iconify.Iconify; + +import java.util.Date; + import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.dialog.DownloadRequestErrorDialogCreator; import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.feed.FeedImage; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.service.download.DownloadStatus; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DBTasks; +import de.danoeh.antennapod.core.storage.DownloadRequestException; /** Displays a list of DownloadStatus entries. */ public class DownloadLogAdapter extends BaseAdapter { + private final String TAG = "DownloadLogAdapter"; + private Context context; private ItemAccess itemAccess; @@ -35,11 +49,11 @@ public class DownloadLogAdapter extends BaseAdapter { LayoutInflater inflater = (LayoutInflater) context .getSystemService(Context.LAYOUT_INFLATER_SERVICE); convertView = inflater.inflate(R.layout.downloadlog_item, parent, false); + holder.icon = (TextView) convertView.findViewById(R.id.txtvIcon); + holder.retry = (Button) convertView.findViewById(R.id.btnRetry); + holder.date = (TextView) convertView.findViewById(R.id.txtvDate); holder.title = (TextView) convertView.findViewById(R.id.txtvTitle); holder.type = (TextView) convertView.findViewById(R.id.txtvType); - holder.date = (TextView) convertView.findViewById(R.id.txtvDate); - holder.successful = (TextView) convertView - .findViewById(R.id.txtvStatus); holder.reason = (TextView) convertView .findViewById(R.id.txtvReason); convertView.setTag(holder); @@ -62,33 +76,99 @@ public class DownloadLogAdapter extends BaseAdapter { status.getCompletionDate().getTime(), System.currentTimeMillis(), 0, 0)); if (status.isSuccessful()) { - holder.successful.setTextColor(convertView.getResources().getColor( + holder.icon.setTextColor(convertView.getResources().getColor( R.color.download_success_green)); - holder.successful.setText(R.string.download_successful); + holder.icon.setText("{fa-check-circle}"); + Iconify.addIcons(holder.icon); + holder.retry.setVisibility(View.GONE); holder.reason.setVisibility(View.GONE); } else { - holder.successful.setTextColor(convertView.getResources().getColor( + holder.icon.setTextColor(convertView.getResources().getColor( R.color.download_failed_red)); - holder.successful.setText(R.string.download_failed); + holder.icon.setText("{fa-times-circle}"); + Iconify.addIcons(holder.icon); String reasonText = status.getReason().getErrorString(context); if (status.getReasonDetailed() != null) { reasonText += ": " + status.getReasonDetailed(); } holder.reason.setText(reasonText); holder.reason.setVisibility(View.VISIBLE); + if(status.getFeedfileType() != FeedImage.FEEDFILETYPE_FEEDIMAGE && + !newerWasSuccessful(position, status.getFeedfileType(), status.getFeedfileId())) { + holder.retry.setVisibility(View.VISIBLE); + holder.retry.setText("{fa-repeat}"); + Iconify.addIcons(holder.retry); + holder.retry.setOnClickListener(clickListener); + ButtonHolder btnHolder; + if(holder.retry.getTag() != null) { + btnHolder = (ButtonHolder) holder.retry.getTag(); + } else { + btnHolder = new ButtonHolder(); + } + btnHolder.typeId = status.getFeedfileType(); + btnHolder.id = status.getFeedfileId(); + holder.retry.setTag(btnHolder); + } else { + holder.retry.setVisibility(View.GONE); + holder.retry.setOnClickListener(null); + holder.retry.setTag(null); + } } return convertView; } + private final View.OnClickListener clickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + ButtonHolder holder = (ButtonHolder) v.getTag(); + if(holder.typeId == Feed.FEEDFILETYPE_FEED) { + Feed feed = DBReader.getFeed(context, holder.id); + feed.setLastUpdate(new Date(0)); // force refresh + try { + DBTasks.refreshFeed(context, feed); + } catch (DownloadRequestException e) { + e.printStackTrace(); + } + } else if(holder.typeId == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { + FeedMedia media = DBReader.getFeedMedia(context, holder.id); + try { + DBTasks.downloadFeedItems(context, media.getItem()); + Toast.makeText(context, R.string.status_downloading_label, Toast.LENGTH_SHORT).show(); + } catch (DownloadRequestException e) { + e.printStackTrace(); + DownloadRequestErrorDialogCreator.newRequestErrorDialog(context, e.getMessage()); + } + } else { + Log.wtf(TAG, "Unexpected type id: " + holder.typeId); + } + v.setVisibility(View.GONE); + } + }; + + private boolean newerWasSuccessful(int position, int feedTypeId, long id) { + for (int i = 0; i < position; i++) { + DownloadStatus status = getItem(i); + if (status.getFeedfileType() == feedTypeId && status.getFeedfileId() == id && + status.isSuccessful()) return true; + } + return false; + } + static class Holder { + TextView icon; + Button retry; TextView title; TextView type; TextView date; - TextView successful; TextView reason; } + static class ButtonHolder { + int typeId; + long id; + } + @Override public int getCount() { return itemAccess.getCount(); @@ -104,9 +184,9 @@ public class DownloadLogAdapter extends BaseAdapter { return position; } - public static interface ItemAccess { - public int getCount(); - public DownloadStatus getItem(int position); + public interface ItemAccess { + int getCount(); + DownloadStatus getItem(int position); } } diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistAdapter.java index d56bfc587..b39e23d42 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistAdapter.java @@ -7,9 +7,16 @@ import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; -import android.widget.*; +import android.widget.Adapter; +import android.widget.BaseAdapter; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.TextView; + +import com.nineoldandroids.view.ViewHelper; + import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.feed.EventDistributor; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.MediaType; @@ -105,22 +112,15 @@ public class FeedItemlistAdapter extends BaseAdapter { } holder.title.setText(buffer.toString()); - FeedItem.State state = item.getState(); - switch (state) { - case PLAYING: - holder.statusUnread.setVisibility(View.INVISIBLE); - holder.episodeProgress.setVisibility(View.VISIBLE); - break; - case IN_PROGRESS: - holder.statusUnread.setVisibility(View.INVISIBLE); - holder.episodeProgress.setVisibility(View.VISIBLE); - break; - case NEW: - holder.statusUnread.setVisibility(View.VISIBLE); - break; - default: - holder.statusUnread.setVisibility(View.INVISIBLE); - break; + if(false == item.isRead() && itemAccess.isNew(item)) { + holder.statusUnread.setVisibility(View.VISIBLE); + } else { + holder.statusUnread.setVisibility(View.INVISIBLE); + } + if(item.isRead()) { + ViewHelper.setAlpha(convertView, 0.5f); + } else { + ViewHelper.setAlpha(convertView, 1.0f); } holder.published.setText(DateUtils.formatDateTime(context, item.getPubDate().getTime(), DateUtils.FORMAT_ABBREV_ALL)); @@ -146,10 +146,10 @@ public class FeedItemlistAdapter extends BaseAdapter { item.getMedia())) { holder.episodeProgress.setVisibility(View.VISIBLE); holder.episodeProgress.setProgress(((ItemAccess) itemAccess).getItemDownloadProgressPercent(item)); - holder.published.setVisibility(View.GONE); } else { - holder.episodeProgress.setVisibility(View.GONE); - holder.published.setVisibility(View.VISIBLE); + if(media.getPosition() == 0) { + holder.episodeProgress.setVisibility(View.GONE); + } } TypedArray typeDrawables = context.obtainStyledAttributes( @@ -211,14 +211,18 @@ public class FeedItemlistAdapter extends BaseAdapter { notifyDataSetChanged(); } - public static interface ItemAccess { - public boolean isInQueue(FeedItem item); + public interface ItemAccess { + + boolean isInQueue(FeedItem item); int getItemDownloadProgressPercent(FeedItem item); int getCount(); FeedItem getItem(int position); + + boolean isNew(FeedItem item); + } } diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java index 05783e3ee..13982f57d 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java @@ -1,35 +1,51 @@ package de.danoeh.antennapod.adapter; import android.content.Context; +import android.content.SharedPreferences; import android.content.res.TypedArray; import android.graphics.Typeface; import android.graphics.drawable.Drawable; +import android.preference.PreferenceManager; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; +import android.widget.IconTextView; import android.widget.ImageView; import android.widget.TextView; import com.squareup.picasso.Picasso; +import org.apache.commons.lang3.ArrayUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.fragment.AddFeedFragment; +import de.danoeh.antennapod.fragment.AllEpisodesFragment; +import de.danoeh.antennapod.fragment.DownloadsFragment; +import de.danoeh.antennapod.fragment.NewEpisodesFragment; +import de.danoeh.antennapod.fragment.PlaybackHistoryFragment; +import de.danoeh.antennapod.fragment.QueueFragment; /** * BaseAdapter for the navigation drawer */ -public class NavListAdapter extends BaseAdapter { +public class NavListAdapter extends BaseAdapter + implements SharedPreferences.OnSharedPreferenceChangeListener { public static final int VIEW_TYPE_COUNT = 3; public static final int VIEW_TYPE_NAV = 0; public static final int VIEW_TYPE_SECTION_DIVIDER = 1; public static final int VIEW_TYPE_SUBSCRIPTION = 2; - public static final int[] NAV_TITLES = {R.string.queue_label, R.string.new_episodes_label, R.string.downloads_label, R.string.playback_history_label, R.string.add_feed_label}; - - private final Drawable[] drawables; - - public static final int SUBSCRIPTION_OFFSET = 1 + NAV_TITLES.length; + private static List<String> tags; + private static String[] titles; private ItemAccess itemAccess; private Context context; @@ -38,23 +54,79 @@ public class NavListAdapter extends BaseAdapter { this.itemAccess = itemAccess; this.context = context; - TypedArray ta = context.obtainStyledAttributes(new int[]{R.attr.stat_playlist, R.attr.ic_new, - R.attr.av_download, R.attr.ic_history, R.attr.content_new}); - drawables = new Drawable[]{ta.getDrawable(0), ta.getDrawable(1), ta.getDrawable(2), - ta.getDrawable(3), ta.getDrawable(4)}; + titles = context.getResources().getStringArray(R.array.nav_drawer_titles); + loadItems(); + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + prefs.registerOnSharedPreferenceChangeListener(this); + } + + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if (key.equals(UserPreferences.PREF_HIDDEN_DRAWER_ITEMS)) { + loadItems(); + } + } + + private void loadItems() { + List<String> newTags = new ArrayList<String>(Arrays.asList(MainActivity.NAV_DRAWER_TAGS)); + List<String> hiddenFragments = UserPreferences.getHiddenDrawerItems(); + for(String hidden : hiddenFragments) { + newTags.remove(hidden); + } + tags = newTags; + notifyDataSetChanged(); + } + + public String getLabel(String tag) { + int index = ArrayUtils.indexOf(MainActivity.NAV_DRAWER_TAGS, tag); + return titles[index]; + } + + private Drawable getDrawable(String tag) { + int icon; + switch (tag) { + case QueueFragment.TAG: + icon = R.attr.stat_playlist; + break; + case NewEpisodesFragment.TAG: + icon = R.attr.ic_new; + break; + case AllEpisodesFragment.TAG: + icon = R.attr.feed; + break; + case DownloadsFragment.TAG: + icon = R.attr.av_download; + break; + case PlaybackHistoryFragment.TAG: + icon = R.attr.ic_history; + break; + case AddFeedFragment.TAG: + icon = R.attr.content_new; + break; + default: + return null; + } + TypedArray ta = context.obtainStyledAttributes(new int[] { icon } ); + Drawable result = ta.getDrawable(0); ta.recycle(); + return result; + } + + public List<String> getTags() { + return Collections.unmodifiableList(tags); } + @Override public int getCount() { - return NAV_TITLES.length + 1 + itemAccess.getCount(); + return getSubscriptionOffset() + itemAccess.getCount(); } @Override public Object getItem(int position) { int viewType = getItemViewType(position); if (viewType == VIEW_TYPE_NAV) { - return context.getString(NAV_TITLES[position]); + return getLabel(tags.get(position)); } else if (viewType == VIEW_TYPE_SECTION_DIVIDER) { return ""; } else { @@ -69,9 +141,9 @@ public class NavListAdapter extends BaseAdapter { @Override public int getItemViewType(int position) { - if (0 <= position && position < NAV_TITLES.length) { + if (0 <= position && position < tags.size()) { return VIEW_TYPE_NAV; - } else if (position < NAV_TITLES.length + 1) { + } else if (position < getSubscriptionOffset()) { return VIEW_TYPE_SECTION_DIVIDER; } else { return VIEW_TYPE_SUBSCRIPTION; @@ -83,6 +155,11 @@ public class NavListAdapter extends BaseAdapter { return VIEW_TYPE_COUNT; } + public int getSubscriptionOffset() { + return tags.size() > 0 ? tags.size() + 1 : 0; + } + + @Override public View getView(int position, View convertView, ViewGroup parent) { int viewType = getItemViewType(position); @@ -92,7 +169,7 @@ public class NavListAdapter extends BaseAdapter { } else if (viewType == VIEW_TYPE_SECTION_DIVIDER) { v = getSectionDividerView(convertView, parent); } else { - v = getFeedView(position - SUBSCRIPTION_OFFSET, convertView, parent); + v = getFeedView(position - getSubscriptionOffset(), convertView, parent); } if (v != null && viewType != VIEW_TYPE_SECTION_DIVIDER) { TextView txtvTitle = (TextView) v.findViewById(R.id.txtvTitle); @@ -114,9 +191,9 @@ public class NavListAdapter extends BaseAdapter { convertView = inflater.inflate(R.layout.nav_listitem, parent, false); + holder.image = (ImageView) convertView.findViewById(R.id.imgvCover); holder.title = (TextView) convertView.findViewById(R.id.txtvTitle); holder.count = (TextView) convertView.findViewById(R.id.txtvCount); - holder.image = (ImageView) convertView.findViewById(R.id.imgvCover); convertView.setTag(holder); } else { holder = (NavHolder) convertView.getTag(); @@ -124,7 +201,7 @@ public class NavListAdapter extends BaseAdapter { holder.title.setText(title); - if (NAV_TITLES[position] == R.string.queue_label) { + if (tags.get(position).equals(QueueFragment.TAG)) { int queueSize = itemAccess.getQueueSize(); if (queueSize > 0) { holder.count.setVisibility(View.VISIBLE); @@ -132,8 +209,8 @@ public class NavListAdapter extends BaseAdapter { } else { holder.count.setVisibility(View.GONE); } - } else if (NAV_TITLES[position] == R.string.new_episodes_label) { - int unreadItems = itemAccess.getNumberOfUnreadItems(); + } else if (tags.get(position).equals(NewEpisodesFragment.TAG)) { + int unreadItems = itemAccess.getNumberOfNewItems(); if (unreadItems > 0) { holder.count.setVisibility(View.VISIBLE); holder.count.setText(String.valueOf(unreadItems)); @@ -144,7 +221,7 @@ public class NavListAdapter extends BaseAdapter { holder.count.setVisibility(View.GONE); } - holder.image.setImageDrawable(drawables[position]); + holder.image.setImageDrawable(getDrawable(tags.get(position))); return convertView; } @@ -172,45 +249,59 @@ public class NavListAdapter extends BaseAdapter { convertView = inflater.inflate(R.layout.nav_feedlistitem, parent, false); - holder.title = (TextView) convertView.findViewById(R.id.txtvTitle); holder.image = (ImageView) convertView.findViewById(R.id.imgvCover); + holder.title = (TextView) convertView.findViewById(R.id.txtvTitle); + holder.failure = (IconTextView) convertView.findViewById(R.id.itxtvFailure); + holder.count = (TextView) convertView.findViewById(R.id.txtvCount); convertView.setTag(holder); } else { holder = (FeedHolder) convertView.getTag(); } - holder.title.setText(feed.getTitle()); - Picasso.with(context) .load(feed.getImageUri()) .fit() .into(holder.image); + holder.title.setText(feed.getTitle()); + + + if(feed.hasLastUpdateFailed()) { + holder.failure.setVisibility(View.VISIBLE); + } else { + holder.failure.setVisibility(View.GONE); + } + int feedUnreadItems = itemAccess.getNumberOfUnreadFeedItems(feed.getId()); + if(feedUnreadItems > 0) { + holder.count.setVisibility(View.VISIBLE); + holder.count.setText(String.valueOf(feedUnreadItems)); + holder.count.setTypeface(holder.title.getTypeface()); + } else { + holder.count.setVisibility(View.INVISIBLE); + } return convertView; } static class NavHolder { + ImageView image; TextView title; TextView count; - ImageView image; } static class FeedHolder { - TextView title; ImageView image; + TextView title; + IconTextView failure; + TextView count; } - public interface ItemAccess { - public int getCount(); - - public Feed getItem(int position); - - public int getSelectedItemIndex(); - - public int getQueueSize(); - - public int getNumberOfUnreadItems(); + int getCount(); + Feed getItem(int position); + int getSelectedItemIndex(); + int getQueueSize(); + int getNumberOfNewItems(); + int getNumberOfUnreadFeedItems(long feedId); } } diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/QueueListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/QueueListAdapter.java index a256dc129..bba5a00a9 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/QueueListAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/QueueListAdapter.java @@ -14,9 +14,9 @@ import android.widget.TextView; import com.squareup.picasso.Picasso; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.feed.EventDistributor; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.storage.DownloadRequester; import de.danoeh.antennapod.core.util.Converter; @@ -31,6 +31,8 @@ public class QueueListAdapter extends BaseAdapter { private final ActionButtonCallback actionButtonCallback; private final ActionButtonUtils actionButtonUtils; + private boolean locked; + public QueueListAdapter(Context context, ItemAccess itemAccess, ActionButtonCallback actionButtonCallback) { super(); @@ -38,6 +40,12 @@ public class QueueListAdapter extends BaseAdapter { this.itemAccess = itemAccess; this.actionButtonUtils = new ActionButtonUtils(context); this.actionButtonCallback = actionButtonCallback; + locked = UserPreferences.isQueueLocked(); + } + + public void setLocked(boolean locked) { + this.locked = locked; + notifyDataSetChanged(); } @Override @@ -67,6 +75,7 @@ public class QueueListAdapter extends BaseAdapter { .getSystemService(Context.LAYOUT_INFLATER_SERVICE); convertView = inflater.inflate(R.layout.queue_listitem, parent, false); + holder.dragHandle = (ImageView) convertView.findViewById(R.id.drag_handle); holder.imageView = (ImageView) convertView.findViewById(R.id.imgvImage); holder.title = (TextView) convertView.findViewById(R.id.txtvTitle); holder.pubDate = (TextView) convertView.findViewById(R.id.txtvPubDate); @@ -83,6 +92,12 @@ public class QueueListAdapter extends BaseAdapter { holder = (Holder) convertView.getTag(); } + if(locked) { + holder.dragHandle.setVisibility(View.GONE); + } else { + holder.dragHandle.setVisibility(View.VISIBLE); + } + holder.title.setText(item.getTitle()); FeedMedia media = item.getMedia(); @@ -143,6 +158,7 @@ public class QueueListAdapter extends BaseAdapter { static class Holder { + ImageView dragHandle; ImageView imageView; TextView title; TextView pubDate; diff --git a/app/src/main/java/de/danoeh/antennapod/config/DownloadServiceCallbacksImpl.java b/app/src/main/java/de/danoeh/antennapod/config/DownloadServiceCallbacksImpl.java index 3a9e62435..998bfa6c9 100644 --- a/app/src/main/java/de/danoeh/antennapod/config/DownloadServiceCallbacksImpl.java +++ b/app/src/main/java/de/danoeh/antennapod/config/DownloadServiceCallbacksImpl.java @@ -20,7 +20,7 @@ public class DownloadServiceCallbacksImpl implements DownloadServiceCallbacks { public PendingIntent getNotificationContentIntent(Context context) { Intent intent = new Intent(context, MainActivity.class); intent.putExtra(MainActivity.EXTRA_NAV_TYPE, NavListAdapter.VIEW_TYPE_NAV); - intent.putExtra(MainActivity.EXTRA_NAV_INDEX, MainActivity.POS_DOWNLOADS); + intent.putExtra(MainActivity.EXTRA_FRAGMENT_TAG, DownloadsFragment.TAG); Bundle args = new Bundle(); args.putInt(DownloadsFragment.ARG_SELECTED_TAB, DownloadsFragment.POS_RUNNING); intent.putExtra(MainActivity.EXTRA_FRAGMENT_ARGS, args); @@ -41,7 +41,7 @@ public class DownloadServiceCallbacksImpl implements DownloadServiceCallbacks { public PendingIntent getReportNotificationContentIntent(Context context) { Intent intent = new Intent(context, MainActivity.class); intent.putExtra(MainActivity.EXTRA_NAV_TYPE, NavListAdapter.VIEW_TYPE_NAV); - intent.putExtra(MainActivity.EXTRA_NAV_INDEX, MainActivity.POS_DOWNLOADS); + intent.putExtra(MainActivity.EXTRA_FRAGMENT_TAG, DownloadsFragment.TAG); Bundle args = new Bundle(); args.putInt(DownloadsFragment.ARG_SELECTED_TAB, DownloadsFragment.POS_LOG); intent.putExtra(MainActivity.EXTRA_FRAGMENT_ARGS, args); diff --git a/app/src/main/java/de/danoeh/antennapod/config/StorageCallbacksImpl.java b/app/src/main/java/de/danoeh/antennapod/config/StorageCallbacksImpl.java index 10a3c1b32..943e05690 100644 --- a/app/src/main/java/de/danoeh/antennapod/config/StorageCallbacksImpl.java +++ b/app/src/main/java/de/danoeh/antennapod/config/StorageCallbacksImpl.java @@ -13,7 +13,7 @@ public class StorageCallbacksImpl implements StorageCallbacks { @Override public int getDatabaseVersion() { - return 14; + return 15; } @Override @@ -124,5 +124,30 @@ public class StorageCallbacksImpl implements StorageCallbacks { PodDBAdapter.KEY_LINK, PodDBAdapter.KEY_CHAPTER_TYPE)); } + if(oldVersion <= 14) { + + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS + + " ADD COLUMN " + PodDBAdapter.KEY_AUTO_DOWNLOAD + " INTEGER"); + db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS + + " SET " + PodDBAdapter.KEY_AUTO_DOWNLOAD + " = " + + "(SELECT " + PodDBAdapter.KEY_AUTO_DOWNLOAD + + " FROM " + PodDBAdapter.TABLE_NAME_FEEDS + + " WHERE " + PodDBAdapter.TABLE_NAME_FEEDS + "." + PodDBAdapter.KEY_ID + + " = " + PodDBAdapter.TABLE_NAME_FEED_ITEMS + "." + PodDBAdapter.KEY_FEED + ")"); + + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + + " ADD COLUMN " + PodDBAdapter.KEY_HIDE + " TEXT"); + + + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + + " ADD COLUMN " + PodDBAdapter.KEY_LAST_UPDATE_FAILED + " INTEGER DEFAULT 0"); + + // create indexes + db.execSQL(PodDBAdapter.CREATE_INDEX_FEEDITEMS_FEED); + db.execSQL(PodDBAdapter.CREATE_INDEX_FEEDITEMS_IMAGE); + db.execSQL(PodDBAdapter.CREATE_INDEX_FEEDMEDIA_FEEDITEM); + db.execSQL(PodDBAdapter.CREATE_INDEX_QUEUE_FEEDITEM); + db.execSQL(PodDBAdapter.CREATE_INDEX_SIMPLECHAPTERS_FEEDITEM); + } } } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java index e4ae1683b..bbe6fab46 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java @@ -20,7 +20,8 @@ import de.danoeh.antennapod.fragment.gpodnet.GpodnetMainFragment; * Provides actions for adding new podcast subscriptions */ public class AddFeedFragment extends Fragment { - private static final String TAG = "AddFeedFragment"; + + public static final String TAG = "AddFeedFragment"; /** * Preset value for url text field. diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/AllEpisodesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/AllEpisodesFragment.java new file mode 100644 index 000000000..ff5485251 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/AllEpisodesFragment.java @@ -0,0 +1,539 @@ +package de.danoeh.antennapod.fragment; + +import android.app.Activity; +import android.content.Context; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.Handler; +import android.support.v4.app.Fragment; +import android.support.v4.view.MenuItemCompat; +import android.support.v7.widget.SearchView; +import android.util.Log; +import android.view.ContextMenu; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.Toast; + +import com.mobeta.android.dslv.DragSortListView; + +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.adapter.DefaultActionButtonCallback; +import de.danoeh.antennapod.adapter.AllEpisodesListAdapter; +import de.danoeh.antennapod.core.asynctask.DownloadObserver; +import de.danoeh.antennapod.core.dialog.ConfirmationDialog; +import de.danoeh.antennapod.core.feed.EventDistributor; +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.service.download.DownloadService; +import de.danoeh.antennapod.core.service.download.Downloader; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DBTasks; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.storage.DownloadRequestException; +import de.danoeh.antennapod.core.storage.DownloadRequester; +import de.danoeh.antennapod.core.util.LongList; +import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler; +import de.danoeh.antennapod.menuhandler.MenuItemUtils; + +/** + * Shows unread or recently published episodes + */ +public class AllEpisodesFragment extends Fragment { + + public static final String TAG = "AllEpisodesFragment"; + + private static final int EVENTS = EventDistributor.DOWNLOAD_HANDLED | + EventDistributor.DOWNLOAD_QUEUED | + EventDistributor.UNREAD_ITEMS_UPDATE | + EventDistributor.PLAYER_STATUS_UPDATE; + + private static final int RECENT_EPISODES_LIMIT = 150; + private static final String DEFAULT_PREF_NAME = "PrefAllEpisodesFragment"; + private static final String PREF_KEY_LIST_TOP = "list_top"; + private static final String PREF_KEY_LIST_SELECTION = "list_selection"; + + private String prefName; + protected DragSortListView listView; + private AllEpisodesListAdapter listAdapter; + private TextView txtvEmpty; + private ProgressBar progLoading; + private ContextMenu contextMenu; + + private List<FeedItem> episodes; + private LongList queuedItemsIds; + private LongList newItemsIds; + private List<Downloader> downloaderList; + + private boolean itemsLoaded = false; + private boolean viewsCreated = false; + private final boolean showOnlyNewEpisodes; + + private AtomicReference<MainActivity> activity = new AtomicReference<MainActivity>(); + + private DownloadObserver downloadObserver = null; + + private boolean isUpdatingFeeds; + + public AllEpisodesFragment() { + // by default we show all the episodes + this(false, DEFAULT_PREF_NAME); + } + + // this is only going to be called by our sub-class. + // The Android docs say to avoid non-default constructors + // but I think this will be OK since it will only be invoked + // from a fragment via a default constructor + protected AllEpisodesFragment(boolean showOnlyNewEpisodes, String prefName) { + this.showOnlyNewEpisodes = showOnlyNewEpisodes; + this.prefName = prefName; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setRetainInstance(true); + setHasOptionsMenu(true); + } + + @Override + public void onResume() { + super.onResume(); + startItemLoader(); + } + + @Override + public void onStart() { + super.onStart(); + EventDistributor.getInstance().register(contentUpdate); + this.activity.set((MainActivity) getActivity()); + if (downloadObserver != null) { + downloadObserver.setActivity(getActivity()); + downloadObserver.onResume(); + } + if (viewsCreated && itemsLoaded) { + onFragmentLoaded(); + } + } + + @Override + public void onPause() { + super.onPause(); + saveScrollPosition(); + } + + @Override + public void onStop() { + super.onStop(); + EventDistributor.getInstance().unregister(contentUpdate); + stopItemLoader(); + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + this.activity.set((MainActivity) getActivity()); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + resetViewState(); + } + + private void saveScrollPosition() { + SharedPreferences prefs = getActivity().getSharedPreferences(prefName, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + View v = listView.getChildAt(0); + int top = (v == null) ? 0 : (v.getTop() - listView.getPaddingTop()); + editor.putInt(PREF_KEY_LIST_SELECTION, listView.getFirstVisiblePosition()); + editor.putInt(PREF_KEY_LIST_TOP, top); + editor.commit(); + } + + private void restoreScrollPosition() { + SharedPreferences prefs = getActivity().getSharedPreferences(prefName, Context.MODE_PRIVATE); + int listSelection = prefs.getInt(PREF_KEY_LIST_SELECTION, 0); + int top = prefs.getInt(PREF_KEY_LIST_TOP, 0); + if (listSelection > 0 || top > 0) { + listView.setSelectionFromTop(listSelection, top); + // restore once, then forget + SharedPreferences.Editor editor = prefs.edit(); + editor.putInt(PREF_KEY_LIST_SELECTION, 0); + editor.putInt(PREF_KEY_LIST_TOP, 0); + editor.commit(); + } + } + + protected void resetViewState() { + listAdapter = null; + activity.set(null); + viewsCreated = false; + if (downloadObserver != null) { + downloadObserver.onPause(); + } + } + + + private final MenuItemUtils.UpdateRefreshMenuItemChecker updateRefreshMenuItemChecker = new MenuItemUtils.UpdateRefreshMenuItemChecker() { + @Override + public boolean isRefreshing() { + return DownloadService.isRunning && DownloadRequester.getInstance().isDownloadingFeeds(); + } + }; + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + if (itemsLoaded) { + inflater.inflate(R.menu.new_episodes, menu); + + MenuItem searchItem = menu.findItem(R.id.action_search); + final SearchView sv = (SearchView) MenuItemCompat.getActionView(searchItem); + MenuItemUtils.adjustTextColor(getActivity(), sv); + sv.setQueryHint(getString(R.string.search_hint)); + sv.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String s) { + sv.clearFocus(); + ((MainActivity) getActivity()).loadChildFragment(SearchFragment.newInstance(s)); + return true; + } + + @Override + public boolean onQueryTextChange(String s) { + return false; + } + }); + isUpdatingFeeds = MenuItemUtils.updateRefreshMenuItem(menu, R.id.refresh_item, updateRefreshMenuItemChecker); + } + } + + @Override + public void onPrepareOptionsMenu(Menu menu) { + super.onPrepareOptionsMenu(menu); + if (itemsLoaded) { + MenuItem menuItem = menu.findItem(R.id.mark_all_read_item); + if (menuItem != null) { + menuItem.setVisible(episodes != null && !episodes.isEmpty()); + } + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (!super.onOptionsItemSelected(item)) { + switch (item.getItemId()) { + case R.id.refresh_item: + List<Feed> feeds = ((MainActivity) getActivity()).getFeeds(); + if (feeds != null) { + DBTasks.refreshAllFeeds(getActivity(), feeds); + } + return true; + case R.id.mark_all_read_item: + ConfirmationDialog conDialog = new ConfirmationDialog(getActivity(), + R.string.mark_all_read_label, + R.string.mark_all_read_confirmation_msg) { + + @Override + public void onConfirmButtonPressed( + DialogInterface dialog) { + dialog.dismiss(); + DBWriter.markAllItemsRead(getActivity()); + Toast.makeText(getActivity(), R.string.mark_all_read_msg, Toast.LENGTH_SHORT).show(); + } + }; + conDialog.createNewDialog().show(); + return true; + default: + return false; + } + } else { + return true; + } + + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return onCreateViewHelper(inflater, container, savedInstanceState, + R.layout.all_episodes_fragment, R.string.all_episodes_label); + } + + protected View onCreateViewHelper(LayoutInflater inflater, + ViewGroup container, + Bundle savedInstanceState, + int fragmentResource, + int titleString) { + super.onCreateView(inflater, container, savedInstanceState); + ((MainActivity) getActivity()).getSupportActionBar().setTitle(titleString); + + View root = inflater.inflate(fragmentResource, container, false); + + listView = (DragSortListView) root.findViewById(android.R.id.list); + txtvEmpty = (TextView) root.findViewById(android.R.id.empty); + progLoading = (ProgressBar) root.findViewById(R.id.progLoading); + + listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + FeedItem item = (FeedItem) listAdapter.getItem(position - listView.getHeaderViewsCount()); + if (item != null) { + ((MainActivity) getActivity()).loadChildFragment(ItemFragment.newInstance(item.getId())); + } + + } + }); + + registerForContextMenu(listView); + + if (!itemsLoaded) { + progLoading.setVisibility(View.VISIBLE); + txtvEmpty.setVisibility(View.GONE); + } + + viewsCreated = true; + + if (itemsLoaded && activity.get() != null) { + onFragmentLoaded(); + } + + return root; + } + + private final FeedItemMenuHandler.MenuInterface contextMenuInterface = new FeedItemMenuHandler.MenuInterface() { + @Override + public void setItemVisibility(int id, boolean visible) { + if(contextMenu == null) { + return; + } + MenuItem item = contextMenu.findItem(id); + if (item != null) { + item.setVisible(visible); + } + } + }; + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + AdapterView.AdapterContextMenuInfo adapterInfo = (AdapterView.AdapterContextMenuInfo) menuInfo; + FeedItem item = itemAccess.getItem(adapterInfo.position); + + MenuInflater inflater = getActivity().getMenuInflater(); + inflater.inflate(R.menu.allepisodes_context, menu); + + if (item != null) { + menu.setHeaderTitle(item.getTitle()); + } + + contextMenu = menu; + FeedItemMenuHandler.onPrepareMenu(contextMenuInterface, item, true, queuedItemsIds); + } + + @Override + public boolean onContextItemSelected(MenuItem item) { + AdapterView.AdapterContextMenuInfo menuInfo = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo(); + FeedItem selectedItem = itemAccess.getItem(menuInfo.position); + + if (selectedItem == null) { + Log.i(TAG, "Selected item at position " + menuInfo.position + " was null, ignoring selection"); + return super.onContextItemSelected(item); + } + + try { + return FeedItemMenuHandler.onMenuItemClicked(getActivity(), item.getItemId(), selectedItem); + } catch (DownloadRequestException e) { + e.printStackTrace(); + Toast.makeText(getActivity(), e.getMessage(), Toast.LENGTH_LONG).show(); + return true; + } + } + + private void onFragmentLoaded() { + if (listAdapter == null) { + listAdapter = new AllEpisodesListAdapter(activity.get(), itemAccess, + new DefaultActionButtonCallback(activity.get()), showOnlyNewEpisodes); + listView.setAdapter(listAdapter); + listView.setEmptyView(txtvEmpty); + downloadObserver = new DownloadObserver(activity.get(), new Handler(), downloadObserverCallback); + downloadObserver.onResume(); + } + listAdapter.notifyDataSetChanged(); + restoreScrollPosition(); + getActivity().supportInvalidateOptionsMenu(); + updateShowOnlyEpisodesListViewState(); + } + + private DownloadObserver.Callback downloadObserverCallback = new DownloadObserver.Callback() { + @Override + public void onContentChanged() { + if (listAdapter != null) { + listAdapter.notifyDataSetChanged(); + } + } + + @Override + public void onDownloadDataAvailable(List<Downloader> downloaderList) { + AllEpisodesFragment.this.downloaderList = downloaderList; + if (listAdapter != null) { + listAdapter.notifyDataSetChanged(); + } + } + }; + + private AllEpisodesListAdapter.ItemAccess itemAccess = new AllEpisodesListAdapter.ItemAccess() { + + @Override + public int getCount() { + if (itemsLoaded) { + return episodes.size(); + } + return 0; + } + + @Override + public FeedItem getItem(int position) { + if (itemsLoaded) { + return episodes.get(position); + } + return null; + } + + @Override + public int getItemDownloadProgressPercent(FeedItem item) { + if (downloaderList != null) { + for (Downloader downloader : downloaderList) { + if (downloader.getDownloadRequest().getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA + && downloader.getDownloadRequest().getFeedfileId() == item.getMedia().getId()) { + return downloader.getDownloadRequest().getProgressPercent(); + } + } + } + return 0; + } + + @Override + public boolean isInQueue(FeedItem item) { + if (itemsLoaded) { + return queuedItemsIds.contains(item.getId()); + } else { + return false; + } + } + + @Override + public boolean isNew(FeedItem item) { + if (itemsLoaded) { + // should actually never be called in NewEpisodesFragment, but better safe than sorry + return showOnlyNewEpisodes || newItemsIds.contains(item.getId()); + } else { + return false; + } + } + + + }; + + private EventDistributor.EventListener contentUpdate = new EventDistributor.EventListener() { + @Override + public void update(EventDistributor eventDistributor, Integer arg) { + if ((arg & EVENTS) != 0) { + startItemLoader(); + if (isUpdatingFeeds != updateRefreshMenuItemChecker.isRefreshing()) { + getActivity().supportInvalidateOptionsMenu(); + } + } + } + }; + + private void updateShowOnlyEpisodesListViewState() { + if (showOnlyNewEpisodes) { + listView.setEmptyView(null); + txtvEmpty.setVisibility(View.GONE); + } else { + listView.setEmptyView(txtvEmpty); + } + } + + private ItemLoader itemLoader; + + protected void startItemLoader() { + if (itemLoader != null) { + itemLoader.cancel(true); + } + itemLoader = new ItemLoader(); + itemLoader.execute(); + } + + protected void stopItemLoader() { + if (itemLoader != null) { + itemLoader.cancel(true); + } + } + + private class ItemLoader extends AsyncTask<Void, Void, Object[]> { + + @Override + protected void onPreExecute() { + super.onPreExecute(); + if (viewsCreated && !itemsLoaded) { + listView.setVisibility(View.GONE); + txtvEmpty.setVisibility(View.GONE); + progLoading.setVisibility(View.VISIBLE); + } + } + + @Override + protected Object[] doInBackground(Void... params) { + Context context = activity.get(); + if (context != null) { + if(showOnlyNewEpisodes) { + return new Object[] { + DBReader.getNewItemsList(context), + DBReader.getQueueIDList(context), + null // see ItemAccess.isNew + }; + } else { + return new Object[]{ + DBReader.getRecentlyPublishedEpisodes(context, RECENT_EPISODES_LIMIT), + DBReader.getQueueIDList(context), + DBReader.getNewItemIds(context) + }; + } + } else { + return null; + } + } + + @Override + protected void onPostExecute(Object[] lists) { + super.onPostExecute(lists); + listView.setVisibility(View.VISIBLE); + progLoading.setVisibility(View.GONE); + + if (lists != null) { + episodes = (List<FeedItem>) lists[0]; + queuedItemsIds = (LongList) lists[1]; + newItemsIds = (LongList) lists[2]; + itemsLoaded = true; + if (viewsCreated && activity.get() != null) { + onFragmentLoaded(); + } + } + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java index 0f6f7d53c..074a87ea0 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java @@ -20,8 +20,6 @@ import de.danoeh.antennapod.core.feed.EventDistributor; import de.danoeh.antennapod.core.service.download.DownloadStatus; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBWriter; -import de.danoeh.antennapod.menuhandler.MenuItemUtils; -import de.danoeh.antennapod.menuhandler.NavDrawerActivity; /** * Shows the download log @@ -119,7 +117,7 @@ public class DownloadLogFragment extends ListFragment { @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); - if (itemsLoaded && !MenuItemUtils.isActivityDrawerOpen((NavDrawerActivity) getActivity())) { + if (itemsLoaded) { MenuItem clearHistory = menu.add(Menu.NONE, R.id.clear_history_item, Menu.CATEGORY_CONTAINER, R.string.clear_history_label); MenuItemCompat.setShowAsAction(clearHistory, MenuItemCompat.SHOW_AS_ACTION_IF_ROOM); TypedArray drawables = getActivity().obtainStyledAttributes(new int[]{R.attr.content_discard}); @@ -131,8 +129,11 @@ public class DownloadLogFragment extends ListFragment { @Override public void onPrepareOptionsMenu(Menu menu) { super.onPrepareOptionsMenu(menu); - if (itemsLoaded && !MenuItemUtils.isActivityDrawerOpen((NavDrawerActivity) getActivity())) { - menu.findItem(R.id.clear_history_item).setVisible(downloadLog != null && !downloadLog.isEmpty()); + if (itemsLoaded) { + MenuItem menuItem = menu.findItem(R.id.clear_history_item); + if(menuItem != null) { + menuItem.setVisible(downloadLog != null && !downloadLog.isEmpty()); + } } } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/DownloadsFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/DownloadsFragment.java index 712db1421..1ce379cf8 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/DownloadsFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/DownloadsFragment.java @@ -17,6 +17,8 @@ import de.danoeh.antennapod.R; */ public class DownloadsFragment extends Fragment { + public static final String TAG = "DownloadsFragment"; + public static final String ARG_SELECTED_TAB = "selected_tab"; public static final int POS_RUNNING = 0; diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java index 10409a421..51a1e2252 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java @@ -270,7 +270,7 @@ public class ItemFragment extends Fragment implements LoaderManager.LoaderCallba return; } popupMenu.getMenu().clear(); - popupMenu.inflate(R.menu.feeditem_dialog); + popupMenu.inflate(R.menu.feeditem_options); if (item.hasMedia()) { FeedItemMenuHandler.onPrepareMenu(popupMenuInterface, item, true, queue); } else { diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ItemlistFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ItemlistFragment.java index a2cb8da18..a9cbe8291 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ItemlistFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ItemlistFragment.java @@ -9,21 +9,27 @@ import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.support.v4.app.ListFragment; -import android.support.v4.util.Pair; +import android.support.v4.view.MenuItemCompat; + import android.support.v7.app.ActionBarActivity; import android.support.v7.widget.SearchView; import android.util.Log; +import android.view.ContextMenu; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; +import android.widget.AdapterView; +import android.widget.IconTextView; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.ListAdapter; import android.widget.ListView; +import android.widget.RelativeLayout; import android.widget.TextView; +import com.joanzapata.android.iconify.Iconify; import com.squareup.picasso.Picasso; import org.apache.commons.lang3.Validate; @@ -42,7 +48,9 @@ import de.danoeh.antennapod.core.dialog.ConfirmationDialog; import de.danoeh.antennapod.core.dialog.DownloadRequestErrorDialogCreator; import de.danoeh.antennapod.core.feed.EventDistributor; import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedEvent; import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedItemFilter; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.QueueEvent; import de.danoeh.antennapod.core.service.download.DownloadService; @@ -53,9 +61,9 @@ import de.danoeh.antennapod.core.storage.DownloadRequestException; import de.danoeh.antennapod.core.storage.DownloadRequester; import de.danoeh.antennapod.core.util.LongList; import de.danoeh.antennapod.core.util.gui.MoreContentListFooterUtil; +import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler; import de.danoeh.antennapod.menuhandler.FeedMenuHandler; import de.danoeh.antennapod.menuhandler.MenuItemUtils; -import de.danoeh.antennapod.menuhandler.NavDrawerActivity; import de.greenrobot.event.EventBus; /** @@ -74,10 +82,13 @@ public class ItemlistFragment extends ListFragment { public static final String ARGUMENT_FEED_ID = "argument.de.danoeh.antennapod.feed_id"; protected FeedItemlistAdapter adapter; + private ContextMenu contextMenu; private long feedID; private Feed feed; - private LongList queue; + private LongList queuedItemsIds; + private LongList newItemsIds; + private boolean itemsLoaded = false; private boolean viewsCreated = false; @@ -88,6 +99,10 @@ public class ItemlistFragment extends ListFragment { private MoreContentListFooterUtil listFooter; private boolean isUpdatingFeed; + + private IconTextView txtvFailure; + + private TextView txtvInformation; /** * Creates new ItemlistFragment which shows the Feeditems of a specific @@ -180,11 +195,12 @@ public class ItemlistFragment extends ListFragment { public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); - if (itemsLoaded && !MenuItemUtils.isActivityDrawerOpen((NavDrawerActivity) getActivity())) { + if (itemsLoaded) { FeedMenuHandler.onCreateOptionsMenu(inflater, menu); - final SearchView sv = new SearchView(getActivity()); - MenuItemUtils.addSearchItem(menu, sv); + MenuItem searchItem = menu.findItem(R.id.action_search); + final SearchView sv = (SearchView) MenuItemCompat.getActionView(searchItem); + MenuItemUtils.adjustTextColor(getActivity(), sv); sv.setQueryHint(getString(R.string.search_hint)); sv.setOnQueryTextListener(new SearchView.OnQueryTextListener() { @Override @@ -207,7 +223,7 @@ public class ItemlistFragment extends ListFragment { @Override public void onPrepareOptionsMenu(Menu menu) { - if (itemsLoaded && !MenuItemUtils.isActivityDrawerOpen((NavDrawerActivity) getActivity())) { + if (itemsLoaded) { FeedMenuHandler.onPrepareOptionsMenu(menu, feed); } } @@ -224,7 +240,7 @@ public class ItemlistFragment extends ListFragment { @Override protected void onPostExecute(Void result) { super.onPostExecute(result); - ((MainActivity) getActivity()).loadNavFragment(MainActivity.POS_NEW, null); + ((MainActivity) getActivity()).loadFragment(NewEpisodesFragment.TAG, null); } }; ConfirmationDialog conDialog = new ConfirmationDialog(getActivity(), @@ -255,10 +271,61 @@ public class ItemlistFragment extends ListFragment { } else { return true; } + } + + private final FeedItemMenuHandler.MenuInterface contextMenuInterface = new FeedItemMenuHandler.MenuInterface() { + @Override + public void setItemVisibility(int id, boolean visible) { + if(contextMenu == null) { + return; + } + MenuItem item = contextMenu.findItem(id); + if (item != null) { + item.setVisible(visible); + } + } + }; + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + AdapterView.AdapterContextMenuInfo adapterInfo = (AdapterView.AdapterContextMenuInfo) menuInfo; + + // because of addHeaderView(), positions are increased by 1! + FeedItem item = itemAccess.getItem(adapterInfo.position-1); + + MenuInflater inflater = getActivity().getMenuInflater(); + inflater.inflate(R.menu.feeditemlist_context, menu); + + if (item != null) { + menu.setHeaderTitle(item.getTitle()); + } + contextMenu = menu; + FeedItemMenuHandler.onPrepareMenu(contextMenuInterface, item, true, queuedItemsIds); } @Override + public boolean onContextItemSelected(MenuItem item) { + AdapterView.AdapterContextMenuInfo menuInfo = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo(); + // because of addHeaderView(), positions are increased by 1! + FeedItem selectedItem = itemAccess.getItem(menuInfo.position-1); + + if (selectedItem == null) { + Log.i(TAG, "Selected item at position " + menuInfo.position + " was null, ignoring selection"); + return super.onContextItemSelected(item); + } + + try { + return FeedItemMenuHandler.onMenuItemClicked(getActivity(), item.getItemId(), selectedItem); + } catch (DownloadRequestException e) { + // context menu doesn't contain download functionality + return true; + } + } + + + @Override public void setListAdapter(ListAdapter adapter) { // This workaround prevents the ListFragment from setting a list adapter when its state is restored. // This is only necessary on API 10 because addFooterView throws an internal exception in this case. @@ -272,6 +339,8 @@ public class ItemlistFragment extends ListFragment { super.onViewCreated(view, savedInstanceState); ((ActionBarActivity) getActivity()).getSupportActionBar().setTitle(""); + registerForContextMenu(getListView()); + viewsCreated = true; if (itemsLoaded) { onFragmentLoaded(); @@ -291,12 +360,19 @@ public class ItemlistFragment extends ListFragment { startItemLoader(); } + public void onEvent(FeedEvent event) { + Log.d(TAG, "onEvent(" + event + ")"); + if(event.feedId == feedID) { + startItemLoader(); + } + } + private EventDistributor.EventListener contentUpdate = new EventDistributor.EventListener() { @Override public void update(EventDistributor eventDistributor, Integer arg) { if ((EVENTS & arg) != 0) { - Log.d(TAG, "Received contentUpdate Intent."); + Log.d(TAG, "Received contentUpdate Intent. arg " + arg); if ((EventDistributor.DOWNLOAD_QUEUED & arg) != 0) { updateProgressBarVisibility(); } else { @@ -330,6 +406,7 @@ public class ItemlistFragment extends ListFragment { downloadObserver = new DownloadObserver(getActivity(), new Handler(), downloadObserverCallback); downloadObserver.onResume(); } + refreshHeaderView(); setListShown(true); adapter.notifyDataSetChanged(); @@ -343,6 +420,32 @@ public class ItemlistFragment extends ListFragment { } + private void refreshHeaderView() { + if(feed.hasLastUpdateFailed()) { + txtvFailure.setVisibility(View.VISIBLE); + } else { + txtvFailure.setVisibility(View.GONE); + } + if(feed.getItemFilter() != null) { + FeedItemFilter filter = feed.getItemFilter(); + if(filter.getValues().length > 0) { + if(feed.hasLastUpdateFailed()) { + RelativeLayout.LayoutParams p = (RelativeLayout.LayoutParams) txtvInformation.getLayoutParams(); + p.addRule(RelativeLayout.BELOW, R.id.txtvFailure); + } + txtvInformation.setText("{fa-info-circle} " + this.getString(R.string.filtered_label)); + Iconify.addIcons(txtvInformation); + txtvInformation.setVisibility(View.VISIBLE); + } else { + txtvInformation.setVisibility(View.GONE); + } + } else { + + txtvInformation.setVisibility(View.GONE); + } + } + + private DownloadObserver.Callback downloadObserverCallback = new DownloadObserver.Callback() { @Override public void onContentChanged() { @@ -376,6 +479,8 @@ public class ItemlistFragment extends ListFragment { ImageView imgvBackground = (ImageView) header.findViewById(R.id.imgvBackground); ImageView imgvCover = (ImageView) header.findViewById(R.id.imgvCover); ImageButton butShowInfo = (ImageButton) header.findViewById(R.id.butShowInfo); + txtvInformation = (TextView) header.findViewById(R.id.txtvInformation); + txtvFailure = (IconTextView) header.findViewById(R.id.txtvFailure); txtvTitle.setText(feed.getTitle()); txtvAuthor.setText(feed.getAuthor()); @@ -406,6 +511,7 @@ public class ItemlistFragment extends ListFragment { }); } + private void setupFooterView() { if (getListView() == null || feed == null) { Log.e(TAG, "Unable to setup listview: listView = null or feed = null"); @@ -438,17 +544,22 @@ public class ItemlistFragment extends ListFragment { @Override public FeedItem getItem(int position) { - return (feed != null) ? feed.getItemAtIndex(true, position) : null; + return (feed != null) ? feed.getItemAtIndex(position) : null; } @Override public int getCount() { - return (feed != null) ? feed.getNumOfItems(true) : 0; + return (feed != null) ? feed.getNumOfItems() : 0; } @Override public boolean isInQueue(FeedItem item) { - return (queue != null) && queue.contains(item.getId()); + return (queuedItemsIds != null) && queuedItemsIds.contains(item.getId()); + } + + @Override + public boolean isNew(FeedItem item) { + return (newItemsIds != null) && newItemsIds.contains(item.getId()); } @Override @@ -481,26 +592,32 @@ public class ItemlistFragment extends ListFragment { } } - private class ItemLoader extends AsyncTask<Long, Void, Pair<Feed,LongList>> { + private class ItemLoader extends AsyncTask<Long, Void, Object[]> { @Override - protected Pair<Feed,LongList> doInBackground(Long... params) { + protected Object[] doInBackground(Long... params) { long feedID = params[0]; Context context = getActivity(); if (context != null) { Feed feed = DBReader.getFeed(context, feedID); - LongList queue = DBReader.getQueueIDList(context); - return Pair.create(feed, queue); + if(feed.getItemFilter() != null) { + FeedItemFilter filter = feed.getItemFilter(); + feed.setItems(filter.filter(context, feed.getItems())); + } + LongList queuedItemsIds = DBReader.getQueueIDList(context); + LongList newItemsIds = DBReader.getNewItemIds(context); + return new Object[] { feed, queuedItemsIds, newItemsIds }; } else { return null; } } @Override - protected void onPostExecute(Pair<Feed,LongList> res) { + protected void onPostExecute(Object[] res) { super.onPostExecute(res); if (res != null) { - feed = res.first; - queue = res.second; + feed = (Feed) res[0]; + queuedItemsIds = (LongList) res[1]; + newItemsIds = res[2] == null ? null : (LongList) res[2]; itemsLoaded = true; if (viewsCreated) { onFragmentLoaded(); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ItunesSearchFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ItunesSearchFragment.java index c14b0cc6e..16789d694 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ItunesSearchFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ItunesSearchFragment.java @@ -1,6 +1,7 @@ package de.danoeh.antennapod.fragment; import android.content.Intent; +import android.content.res.Resources; import android.os.AsyncTask; import android.os.Bundle; import android.support.v4.app.Fragment; @@ -28,6 +29,7 @@ import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.DefaultOnlineFeedViewActivity; import de.danoeh.antennapod.activity.OnlineFeedViewActivity; import de.danoeh.antennapod.adapter.itunes.ItunesAdapter; +import de.danoeh.antennapod.core.preferences.UserPreferences; import static de.danoeh.antennapod.adapter.itunes.ItunesAdapter.*; @@ -124,6 +126,13 @@ public class ItunesSearchFragment extends Fragment { } }); + SearchView.SearchAutoComplete textField = (SearchView.SearchAutoComplete) searchView.findViewById(de.danoeh.antennapod.R.id.search_src_text); + if(UserPreferences.getTheme() == de.danoeh.antennapod.R.style.Theme_AntennaPod_Dark) { + textField.setTextColor(Resources.getSystem().getColor(android.R.color.white)); + } else { + textField.setTextColor(Resources.getSystem().getColor(android.R.color.black)); + } + return view; } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/NewEpisodesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/NewEpisodesFragment.java index eda01a851..4bce3c7ba 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/NewEpisodesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/NewEpisodesFragment.java @@ -1,295 +1,75 @@ package de.danoeh.antennapod.fragment; -import android.app.Activity; import android.content.Context; -import android.content.DialogInterface; -import android.content.SharedPreferences; -import android.os.AsyncTask; import android.os.Bundle; -import android.os.Handler; -import android.support.v4.app.Fragment; -import android.support.v7.widget.SearchView; import android.util.Log; import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.ProgressBar; -import android.widget.TextView; -import android.widget.Toast; import com.mobeta.android.dslv.DragSortListView; -import java.util.List; -import java.util.concurrent.atomic.AtomicReference; - import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.adapter.DefaultActionButtonCallback; -import de.danoeh.antennapod.adapter.NewEpisodesListAdapter; -import de.danoeh.antennapod.core.asynctask.DownloadObserver; -import de.danoeh.antennapod.core.dialog.ConfirmationDialog; -import de.danoeh.antennapod.core.feed.EventDistributor; -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.QueueEvent; import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.core.service.download.DownloadService; -import de.danoeh.antennapod.core.service.download.Downloader; import de.danoeh.antennapod.core.storage.DBReader; -import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.storage.DBWriter; -import de.danoeh.antennapod.core.storage.DownloadRequester; -import de.danoeh.antennapod.core.util.LongList; import de.danoeh.antennapod.core.util.gui.FeedItemUndoToken; import de.danoeh.antennapod.core.util.gui.UndoBarController; -import de.danoeh.antennapod.menuhandler.MenuItemUtils; -import de.danoeh.antennapod.menuhandler.NavDrawerActivity; import de.greenrobot.event.EventBus; + /** - * Shows unread or recently published episodes + * Like 'EpisodesFragment' except that it only shows new episodes and + * supports swiping to mark as read. */ -public class NewEpisodesFragment extends Fragment { - private static final String TAG = "NewEpisodesFragment"; - private static final int EVENTS = EventDistributor.DOWNLOAD_HANDLED | - EventDistributor.DOWNLOAD_QUEUED | - EventDistributor.UNREAD_ITEMS_UPDATE | - EventDistributor.PLAYER_STATUS_UPDATE; - - private static final int RECENT_EPISODES_LIMIT = 150; - private static final String PREF_NAME = "PrefNewEpisodesFragment"; - private static final String PREF_EPISODE_FILTER_BOOL = "newEpisodeFilterEnabled"; - private static final String PREF_KEY_LIST_TOP = "list_top"; - private static final String PREF_KEY_LIST_SELECTION = "list_selection"; - - private DragSortListView listView; - private NewEpisodesListAdapter listAdapter; - private TextView txtvEmpty; - private ProgressBar progLoading; - - private UndoBarController undoBarController; - private List<FeedItem> unreadItems; - private List<FeedItem> recentItems; - private LongList queue; - private List<Downloader> downloaderList; +public class NewEpisodesFragment extends AllEpisodesFragment { - private boolean itemsLoaded = false; - private boolean viewsCreated = false; - private boolean showOnlyNewEpisodes = false; + public static final String TAG = "NewEpisodesFragment"; - private AtomicReference<MainActivity> activity = new AtomicReference<MainActivity>(); - - private DownloadObserver downloadObserver = null; - - private boolean isUpdatingFeeds; + private static final String PREF_NAME = "PrefNewEpisodesFragment"; - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setRetainInstance(true); - setHasOptionsMenu(true); + private UndoBarController undoBarController; - updateShowOnlyEpisodes(); + public NewEpisodesFragment() { + super(true, PREF_NAME); } - @Override - public void onResume() { - super.onResume(); + public void onEvent(QueueEvent event) { + Log.d(TAG, "onEvent(" + event + ")"); startItemLoader(); } @Override public void onStart() { super.onStart(); - EventDistributor.getInstance().register(contentUpdate); EventBus.getDefault().register(this); - this.activity.set((MainActivity) getActivity()); - if (downloadObserver != null) { - downloadObserver.setActivity(getActivity()); - downloadObserver.onResume(); - } - if (viewsCreated && itemsLoaded) { - onFragmentLoaded(); - } - } - - @Override - public void onPause() { - super.onPause(); - saveScrollPosition(); } @Override public void onStop() { super.onStop(); - EventDistributor.getInstance().unregister(contentUpdate); EventBus.getDefault().unregister(this); - stopItemLoader(); - if(undoBarController.isShowing()) { - undoBarController.close(); - } - } - - @Override - public void onAttach(Activity activity) { - super.onAttach(activity); - this.activity.set((MainActivity) getActivity()); } @Override - public void onDestroyView() { - super.onDestroyView(); - resetViewState(); - } - - private void saveScrollPosition() { - SharedPreferences prefs = getActivity().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); - SharedPreferences.Editor editor = prefs.edit(); - View v = listView.getChildAt(0); - int top = (v == null) ? 0 : (v.getTop() - listView.getPaddingTop()); - editor.putInt(PREF_KEY_LIST_SELECTION, listView.getFirstVisiblePosition()); - editor.putInt(PREF_KEY_LIST_TOP, top); - editor.commit(); - } - - private void restoreScrollPosition() { - SharedPreferences prefs = getActivity().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); - int listSelection = prefs.getInt(PREF_KEY_LIST_SELECTION, 0); - int top = prefs.getInt(PREF_KEY_LIST_TOP, 0); - if(listSelection > 0 || top > 0) { - listView.setSelectionFromTop(listSelection, top); - // restore once, then forget - SharedPreferences.Editor editor = prefs.edit(); - editor.putInt(PREF_KEY_LIST_SELECTION, 0); - editor.putInt(PREF_KEY_LIST_TOP, 0); - editor.commit(); - } - } - - private void resetViewState() { - listAdapter = null; - activity.set(null); - viewsCreated = false; + protected void resetViewState() { + super.resetViewState(); undoBarController = null; - if (downloadObserver != null) { - downloadObserver.onPause(); - } - } - - - private final MenuItemUtils.UpdateRefreshMenuItemChecker updateRefreshMenuItemChecker = new MenuItemUtils.UpdateRefreshMenuItemChecker() { - @Override - public boolean isRefreshing() { - return DownloadService.isRunning && DownloadRequester.getInstance().isDownloadingFeeds(); - } - }; - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - super.onCreateOptionsMenu(menu, inflater); - if (itemsLoaded && !MenuItemUtils.isActivityDrawerOpen((NavDrawerActivity) getActivity())) { - inflater.inflate(R.menu.new_episodes, menu); - - final SearchView sv = new SearchView(getActivity()); - MenuItemUtils.addSearchItem(menu, sv); - sv.setQueryHint(getString(R.string.search_hint)); - sv.setOnQueryTextListener(new SearchView.OnQueryTextListener() { - @Override - public boolean onQueryTextSubmit(String s) { - sv.clearFocus(); - ((MainActivity) getActivity()).loadChildFragment(SearchFragment.newInstance(s)); - return true; - } - - @Override - public boolean onQueryTextChange(String s) { - return false; - } - }); - isUpdatingFeeds = MenuItemUtils.updateRefreshMenuItem(menu, R.id.refresh_item, updateRefreshMenuItemChecker); - } - } - - @Override - public void onPrepareOptionsMenu(Menu menu) { - super.onPrepareOptionsMenu(menu); - if (itemsLoaded && !MenuItemUtils.isActivityDrawerOpen((NavDrawerActivity) getActivity())) { - menu.findItem(R.id.mark_all_read_item).setVisible(unreadItems != null && !unreadItems.isEmpty()); - menu.findItem(R.id.episode_filter_item).setChecked(showOnlyNewEpisodes); - } - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - if (!super.onOptionsItemSelected(item)) { - switch (item.getItemId()) { - case R.id.refresh_item: - List<Feed> feeds = ((MainActivity) getActivity()).getFeeds(); - if (feeds != null) { - DBTasks.refreshAllFeeds(getActivity(), feeds); - } - return true; - case R.id.mark_all_read_item: - ConfirmationDialog conDialog = new ConfirmationDialog(getActivity(), - R.string.mark_all_read_label, - R.string.mark_all_read_confirmation_msg) { - - @Override - public void onConfirmButtonPressed( - DialogInterface dialog) { - dialog.dismiss(); - DBWriter.markAllItemsRead(getActivity()); - Toast.makeText(getActivity(), R.string.mark_all_read_msg, Toast.LENGTH_SHORT).show(); - } - }; - conDialog.createNewDialog().show(); - return true; - case R.id.episode_filter_item: - boolean newVal = !item.isChecked(); - setShowOnlyNewEpisodes(newVal); - item.setChecked(newVal); - return true; - default: - return false; - } - } else { - return true; - } - } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - super.onCreateView(inflater, container, savedInstanceState); - ((MainActivity) getActivity()).getSupportActionBar().setTitle(R.string.new_episodes_label); - - View root = inflater.inflate(R.layout.new_episodes_fragment, container, false); - - listView = (DragSortListView) root.findViewById(android.R.id.list); - txtvEmpty = (TextView) root.findViewById(android.R.id.empty); - progLoading = (ProgressBar) root.findViewById(R.id.progLoading); - - listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView<?> parent, View view, int position, long id) { - FeedItem item = (FeedItem) listAdapter.getItem(position - listView.getHeaderViewsCount()); - if (item != null) { - ((MainActivity) getActivity()).loadChildFragment(ItemFragment.newInstance(item.getId())); - } - - } - }); + View root = super.onCreateViewHelper(inflater, container, savedInstanceState, + R.layout.new_episodes_fragment, R.string.new_episodes_label); listView.setRemoveListener(new DragSortListView.RemoveListener() { @Override public void remove(int which) { - Log.d(TAG, "remove("+which+")"); + Log.d(TAG, "remove(" + which + ")"); stopItemLoader(); FeedItem item = (FeedItem) listView.getAdapter().getItem(which); DBWriter.markItemRead(getActivity(), item.getId(), true); @@ -323,198 +103,7 @@ public class NewEpisodesFragment extends Fragment { } } }); - - final int secondColor = (UserPreferences.getTheme() == R.style.Theme_AntennaPod_Dark) ? R.color.swipe_refresh_secondary_color_dark : R.color.swipe_refresh_secondary_color_light; - - if (!itemsLoaded) { - progLoading.setVisibility(View.VISIBLE); - txtvEmpty.setVisibility(View.GONE); - } - - viewsCreated = true; - - if (itemsLoaded && activity.get() != null) { - onFragmentLoaded(); - } - return root; } - private void onFragmentLoaded() { - if (listAdapter == null) { - listAdapter = new NewEpisodesListAdapter(activity.get(), itemAccess, new DefaultActionButtonCallback(activity.get())); - listView.setAdapter(listAdapter); - listView.setEmptyView(txtvEmpty); - downloadObserver = new DownloadObserver(activity.get(), new Handler(), downloadObserverCallback); - downloadObserver.onResume(); - } - listAdapter.notifyDataSetChanged(); - restoreScrollPosition(); - getActivity().supportInvalidateOptionsMenu(); - updateShowOnlyEpisodesListViewState(); - } - - private DownloadObserver.Callback downloadObserverCallback = new DownloadObserver.Callback() { - @Override - public void onContentChanged() { - if (listAdapter != null) { - listAdapter.notifyDataSetChanged(); - } - } - - @Override - public void onDownloadDataAvailable(List<Downloader> downloaderList) { - NewEpisodesFragment.this.downloaderList = downloaderList; - if (listAdapter != null) { - listAdapter.notifyDataSetChanged(); - } - } - }; - - private NewEpisodesListAdapter.ItemAccess itemAccess = new NewEpisodesListAdapter.ItemAccess() { - - @Override - public int getCount() { - if (itemsLoaded) { - return (showOnlyNewEpisodes) ? unreadItems.size() : recentItems.size(); - } - return 0; - } - - @Override - public FeedItem getItem(int position) { - if (itemsLoaded) { - return (showOnlyNewEpisodes) ? unreadItems.get(position) : recentItems.get(position); - } - return null; - } - - @Override - public int getItemDownloadProgressPercent(FeedItem item) { - if (downloaderList != null) { - for (Downloader downloader : downloaderList) { - if (downloader.getDownloadRequest().getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA - && downloader.getDownloadRequest().getFeedfileId() == item.getMedia().getId()) { - return downloader.getDownloadRequest().getProgressPercent(); - } - } - } - return 0; - } - - @Override - public boolean isInQueue(FeedItem item) { - if (itemsLoaded) { - return queue.contains(item.getId()); - } else { - return false; - } - } - - - }; - - public void onEvent(QueueEvent event) { - Log.d(TAG, "onEvent(" + event + ")"); - startItemLoader(); - } - - private EventDistributor.EventListener contentUpdate = new EventDistributor.EventListener() { - @Override - public void update(EventDistributor eventDistributor, Integer arg) { - if ((arg & EVENTS) != 0) { - startItemLoader(); - if (isUpdatingFeeds != updateRefreshMenuItemChecker.isRefreshing()) { - getActivity().supportInvalidateOptionsMenu(); - } - } - } - }; - - private void updateShowOnlyEpisodes() { - SharedPreferences prefs = getActivity().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); - showOnlyNewEpisodes = prefs.getBoolean(PREF_EPISODE_FILTER_BOOL, true); - } - - private void setShowOnlyNewEpisodes(boolean newVal) { - showOnlyNewEpisodes = newVal; - SharedPreferences prefs = getActivity().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); - SharedPreferences.Editor editor = prefs.edit(); - editor.putBoolean(PREF_EPISODE_FILTER_BOOL, showOnlyNewEpisodes); - editor.commit(); - if (itemsLoaded && viewsCreated) { - listAdapter.notifyDataSetChanged(); - activity.get().supportInvalidateOptionsMenu(); - updateShowOnlyEpisodesListViewState(); - } - } - - private void updateShowOnlyEpisodesListViewState() { - if (showOnlyNewEpisodes) { - listView.setEmptyView(null); - txtvEmpty.setVisibility(View.GONE); - } else { - listView.setEmptyView(txtvEmpty); - } - } - - private ItemLoader itemLoader; - - private void startItemLoader() { - if (itemLoader != null) { - itemLoader.cancel(true); - } - itemLoader = new ItemLoader(); - itemLoader.execute(); - } - - private void stopItemLoader() { - if (itemLoader != null) { - itemLoader.cancel(true); - } - } - - private class ItemLoader extends AsyncTask<Void, Void, Object[]> { - - @Override - protected void onPreExecute() { - super.onPreExecute(); - if (viewsCreated && !itemsLoaded) { - listView.setVisibility(View.GONE); - txtvEmpty.setVisibility(View.GONE); - progLoading.setVisibility(View.VISIBLE); - } - } - - @Override - protected Object[] doInBackground(Void... params) { - Context context = activity.get(); - if (context != null) { - return new Object[] { - DBReader.getUnreadItemsList(context), - DBReader.getRecentlyPublishedEpisodes(context, RECENT_EPISODES_LIMIT), - DBReader.getQueueIDList(context) - }; - } else { - return null; - } - } - - @Override - protected void onPostExecute(Object[] lists) { - super.onPostExecute(lists); - listView.setVisibility(View.VISIBLE); - progLoading.setVisibility(View.GONE); - - if (lists != null) { - unreadItems = (List<FeedItem>) lists[0]; - recentItems = (List<FeedItem>) lists[1]; - queue = (LongList) lists[2]; - itemsLoaded = true; - if (viewsCreated && activity.get() != null) { - onFragmentLoaded(); - } - } - } - } } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java index 647fe550d..9099829d8 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java @@ -32,12 +32,12 @@ import de.danoeh.antennapod.core.service.download.Downloader; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.util.LongList; -import de.danoeh.antennapod.menuhandler.MenuItemUtils; -import de.danoeh.antennapod.menuhandler.NavDrawerActivity; import de.greenrobot.event.EventBus; public class PlaybackHistoryFragment extends ListFragment { - private static final String TAG = "PlaybackHistoryFragment"; + + public static final String TAG = "PlaybackHistoryFragment"; + private static final int EVENTS = EventDistributor.PLAYBACK_HISTORY_UPDATE | EventDistributor.PLAYER_STATUS_UPDATE; @@ -139,7 +139,7 @@ public class PlaybackHistoryFragment extends ListFragment { @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); - if (itemsLoaded && !MenuItemUtils.isActivityDrawerOpen((NavDrawerActivity) getActivity())) { + if (itemsLoaded) { MenuItem clearHistory = menu.add(Menu.NONE, R.id.clear_history_item, Menu.CATEGORY_CONTAINER, R.string.clear_history_label); MenuItemCompat.setShowAsAction(clearHistory, MenuItemCompat.SHOW_AS_ACTION_IF_ROOM); TypedArray drawables = getActivity().obtainStyledAttributes(new int[]{R.attr.content_discard}); @@ -151,8 +151,11 @@ public class PlaybackHistoryFragment extends ListFragment { @Override public void onPrepareOptionsMenu(Menu menu) { super.onPrepareOptionsMenu(menu); - if (itemsLoaded && !MenuItemUtils.isActivityDrawerOpen((NavDrawerActivity) getActivity())) { - menu.findItem(R.id.clear_history_item).setVisible(playbackHistory != null && !playbackHistory.isEmpty()); + if (itemsLoaded) { + MenuItem menuItem = menu.findItem(R.id.clear_history_item); + if (menuItem != null) { + menuItem.setVisible(playbackHistory != null && !playbackHistory.isEmpty()); + } } } @@ -223,6 +226,11 @@ public class PlaybackHistoryFragment extends ListFragment { } @Override + public boolean isNew(FeedItem item) { + return false; + } + + @Override public int getItemDownloadProgressPercent(FeedItem item) { if (downloaderList != null) { for (Downloader downloader : downloaderList) { diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java index a3721d7b3..d82c7b8f7 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java @@ -8,6 +8,7 @@ import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.support.v4.app.Fragment; +import android.support.v4.view.MenuItemCompat; import android.support.v7.widget.SearchView; import android.util.Log; import android.view.ContextMenu; @@ -20,6 +21,7 @@ import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.ProgressBar; import android.widget.TextView; +import android.widget.Toast; import com.mobeta.android.dslv.DragSortListView; @@ -43,19 +45,23 @@ import de.danoeh.antennapod.core.service.download.Downloader; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.storage.DownloadRequestException; import de.danoeh.antennapod.core.storage.DownloadRequester; +import de.danoeh.antennapod.core.util.LongList; import de.danoeh.antennapod.core.util.QueueSorter; import de.danoeh.antennapod.core.util.gui.FeedItemUndoToken; import de.danoeh.antennapod.core.util.gui.UndoBarController; +import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler; import de.danoeh.antennapod.menuhandler.MenuItemUtils; -import de.danoeh.antennapod.menuhandler.NavDrawerActivity; import de.greenrobot.event.EventBus; /** * Shows all items in the queue */ public class QueueFragment extends Fragment { - private static final String TAG = "QueueFragment"; + + public static final String TAG = "QueueFragment"; + private static final int EVENTS = EventDistributor.DOWNLOAD_HANDLED | EventDistributor.DOWNLOAD_QUEUED | EventDistributor.PLAYER_STATUS_UPDATE; @@ -65,6 +71,8 @@ public class QueueFragment extends Fragment { private TextView txtvEmpty; private ProgressBar progLoading; + private ContextMenu contextMenu; + private UndoBarController<FeedItemUndoToken> undoBarController; private List<FeedItem> queue; @@ -200,11 +208,12 @@ public class QueueFragment extends Fragment { @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); - if (itemsLoaded && !MenuItemUtils.isActivityDrawerOpen((NavDrawerActivity) getActivity())) { + if (itemsLoaded) { inflater.inflate(R.menu.queue, menu); - final SearchView sv = new SearchView(getActivity()); - MenuItemUtils.addSearchItem(menu, sv); + MenuItem searchItem = menu.findItem(R.id.action_search); + final SearchView sv = (SearchView) MenuItemCompat.getActionView(searchItem); + MenuItemUtils.adjustTextColor(getActivity(), sv); sv.setQueryHint(getString(R.string.search_hint)); sv.setOnQueryTextListener(new SearchView.OnQueryTextListener() { @Override @@ -219,6 +228,9 @@ public class QueueFragment extends Fragment { return false; } }); + + MenuItemUtils.refreshLockItem(getActivity(), menu); + isUpdatingFeeds = MenuItemUtils.updateRefreshMenuItem(menu, R.id.refresh_item, updateRefreshMenuItemChecker); } } @@ -227,6 +239,17 @@ public class QueueFragment extends Fragment { public boolean onOptionsItemSelected(MenuItem item) { if (!super.onOptionsItemSelected(item)) { switch (item.getItemId()) { + case R.id.queue_lock: + boolean locked = !UserPreferences.isQueueLocked(); + if(locked) { + listView.setDragEnabled(false); + } else { + listView.setDragEnabled(true); + } + UserPreferences.setQueueLocked(locked); + getActivity().supportInvalidateOptionsMenu(); + listAdapter.setLocked(locked); + return true; case R.id.refresh_item: List<Feed> feeds = ((MainActivity) getActivity()).getFeeds(); if (feeds != null) { @@ -275,6 +298,19 @@ public class QueueFragment extends Fragment { } + private final FeedItemMenuHandler.MenuInterface contextMenuInterface = new FeedItemMenuHandler.MenuInterface() { + @Override + public void setItemVisibility(int id, boolean visible) { + if(contextMenu == null) { + return; + } + MenuItem item = contextMenu.findItem(id); + if (item != null) { + item.setVisible(visible); + } + } + }; + @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); @@ -288,8 +324,12 @@ public class QueueFragment extends Fragment { menu.setHeaderTitle(item.getTitle()); } - menu.findItem(R.id.move_to_top_item).setEnabled(!queue.isEmpty() && queue.get(0) != item); - menu.findItem(R.id.move_to_bottom_item).setEnabled(!queue.isEmpty() && queue.get(queue.size() - 1) != item); + contextMenu = menu; + LongList queueIds = new LongList(queue.size()); + for(FeedItem queueItem : queue) { + queueIds.add(queueItem.getId()); + } + FeedItemMenuHandler.onPrepareMenu(contextMenuInterface, item, true, queueIds); } @Override @@ -302,21 +342,16 @@ public class QueueFragment extends Fragment { return super.onContextItemSelected(item); } - switch (item.getItemId()) { - case R.id.move_to_top_item: - DBWriter.moveQueueItemToTop(getActivity(), selectedItem.getId(), true); - return true; - case R.id.move_to_bottom_item: - DBWriter.moveQueueItemToBottom(getActivity(), selectedItem.getId(), true); - return true; - case R.id.remove_from_queue_item: - DBWriter.removeQueueItem(getActivity(), selectedItem, false); - return true; - default: - return super.onContextItemSelected(item); + try { + return FeedItemMenuHandler.onMenuItemClicked(getActivity(), item.getItemId(), selectedItem); + } catch (DownloadRequestException e) { + e.printStackTrace(); + Toast.makeText(getActivity(), e.getMessage(), Toast.LENGTH_LONG).show(); + return true; } } + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { super.onCreateView(inflater, container, savedInstanceState); @@ -328,6 +363,12 @@ public class QueueFragment extends Fragment { progLoading = (ProgressBar) root.findViewById(R.id.progLoading); listView.setEmptyView(txtvEmpty); + if(UserPreferences.isQueueLocked()) { + listView.setDragEnabled(false); + } else { + listView.setDragEnabled(true); + } + listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { @@ -361,7 +402,6 @@ public class QueueFragment extends Fragment { Log.d(TAG, "remove(" + which + ")"); stopItemLoader(); FeedItem item = (FeedItem) listView.getAdapter().getItem(which); - DBWriter.markItemRead(getActivity(), item.getId(), true); DBWriter.removeQueueItem(getActivity(), item, true); } }); @@ -376,7 +416,6 @@ public class QueueFragment extends Fragment { if (token != null) { long itemId = token.getFeedItemId(); int position = token.getPosition(); - DBWriter.markItemRead(context, itemId, false); DBWriter.addQueueItemAt(context, itemId, position, false); } } @@ -395,7 +434,6 @@ public class QueueFragment extends Fragment { }); - registerForContextMenu(listView); if (!itemsLoaded) { diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/RunningDownloadsFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/RunningDownloadsFragment.java index f578d4338..b1b61f74b 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/RunningDownloadsFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/RunningDownloadsFragment.java @@ -5,15 +5,21 @@ import android.os.Handler; import android.support.v4.app.ListFragment; import android.view.View; import android.widget.ListView; +import android.widget.Toast; + +import java.util.List; import de.danoeh.antennapod.R; import de.danoeh.antennapod.adapter.DownloadlistAdapter; import de.danoeh.antennapod.core.asynctask.DownloadObserver; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.service.download.DownloadRequest; import de.danoeh.antennapod.core.service.download.Downloader; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.storage.DownloadRequester; -import java.util.List; - /** * Displays all running downloads and provides actions to cancel them */ @@ -73,7 +79,17 @@ public class RunningDownloadsFragment extends ListFragment { @Override public void onSecondaryActionClick(Downloader downloader) { - DownloadRequester.getInstance().cancelDownload(getActivity(), downloader.getDownloadRequest().getSource()); + DownloadRequest downloadRequest = downloader.getDownloadRequest(); + DownloadRequester.getInstance().cancelDownload(getActivity(), downloadRequest.getSource()); + + if(downloadRequest.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA && + UserPreferences.isEnableAutodownload()) { + FeedMedia media = DBReader.getFeedMedia(getActivity(), downloadRequest.getFeedfileId()); + DBWriter.setFeedItemAutoDownload(getActivity(), media.getItem(), false); + Toast.makeText(getActivity(), R.string.download_canceled_autodownload_enabled_msg, Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(getActivity(), R.string.download_canceled_msg, Toast.LENGTH_SHORT).show(); + } } }; } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java index 3d6722370..fc6225409 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java @@ -25,8 +25,6 @@ import de.danoeh.antennapod.core.feed.FeedComponent; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.SearchResult; import de.danoeh.antennapod.core.storage.FeedSearcher; -import de.danoeh.antennapod.menuhandler.MenuItemUtils; -import de.danoeh.antennapod.menuhandler.NavDrawerActivity; /** * Performs a search operation on all feeds or one specific feed and displays the search result. @@ -122,7 +120,7 @@ public class SearchFragment extends ListFragment { SearchResult result = (SearchResult) l.getAdapter().getItem(position); FeedComponent comp = result.getComponent(); if (comp.getClass() == Feed.class) { - ((MainActivity) getActivity()).loadFeedFragment(comp.getId()); + ((MainActivity) getActivity()).loadFeedFragmentById(comp.getId()); } else { if (comp.getClass() == FeedItem.class) { FeedItem item = (FeedItem) comp; @@ -134,7 +132,7 @@ public class SearchFragment extends ListFragment { @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); - if (itemsLoaded && !MenuItemUtils.isActivityDrawerOpen((NavDrawerActivity) getActivity())) { + if (itemsLoaded) { MenuItem item = menu.add(Menu.NONE, R.id.search_item, Menu.NONE, R.string.search_label); MenuItemCompat.setShowAsAction(item, MenuItemCompat.SHOW_AS_ACTION_IF_ROOM); final SearchView sv = new SearchView(getActivity()); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/GpodnetMainFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/GpodnetMainFragment.java index 45b2403c8..55d4b940f 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/GpodnetMainFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/GpodnetMainFragment.java @@ -31,8 +31,8 @@ public class GpodnetMainFragment extends Fragment { private static final int NUM_PAGES = 2; - private static final int POS_TAGS = 0; - private static final int POS_TOPLIST = 1; + private static final int POS_TOPLIST = 0; + private static final int POS_TAGS = 1; private static final int POS_SUGGESTIONS = 2; Resources resources; diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java index 15a0b55b1..6139a4901 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java @@ -5,11 +5,23 @@ import android.content.Intent; import android.os.AsyncTask; import android.os.Bundle; import android.support.v4.app.Fragment; +import android.support.v4.view.MenuItemCompat; +import android.support.v7.widget.SearchView; import android.util.Log; -import android.view.*; -import android.widget.*; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.Button; +import android.widget.GridView; +import android.widget.ProgressBar; +import android.widget.TextView; + +import java.util.List; -import de.danoeh.antennapod.BuildConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.DefaultOnlineFeedViewActivity; import de.danoeh.antennapod.activity.MainActivity; @@ -19,14 +31,12 @@ import de.danoeh.antennapod.core.gpoddernet.GpodnetService; import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException; import de.danoeh.antennapod.core.gpoddernet.model.GpodnetPodcast; import de.danoeh.antennapod.menuhandler.MenuItemUtils; -import de.danoeh.antennapod.menuhandler.NavDrawerActivity; - -import java.util.List; /** * Displays a list of GPodnetPodcast-Objects in a GridView */ public abstract class PodcastListFragment extends Fragment { + private static final String TAG = "PodcastListFragment"; private GridView gridView; @@ -43,24 +53,24 @@ public abstract class PodcastListFragment extends Fragment { @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); - if (!MenuItemUtils.isActivityDrawerOpen((NavDrawerActivity) getActivity())) { - final android.support.v7.widget.SearchView sv = new android.support.v7.widget.SearchView(getActivity()); - MenuItemUtils.addSearchItem(menu, sv); - sv.setQueryHint(getString(R.string.gpodnet_search_hint)); - sv.setOnQueryTextListener(new android.support.v7.widget.SearchView.OnQueryTextListener() { - @Override - public boolean onQueryTextSubmit(String s) { - sv.clearFocus(); - ((MainActivity) getActivity()).loadChildFragment(SearchListFragment.newInstance(s)); - return true; - } + inflater.inflate(R.menu.gpodder_podcasts, menu); + MenuItem searchItem = menu.findItem(R.id.action_search); + final SearchView sv = (SearchView) MenuItemCompat.getActionView(searchItem); + MenuItemUtils.adjustTextColor(getActivity(), sv); + sv.setQueryHint(getString(R.string.gpodnet_search_hint)); + sv.setOnQueryTextListener(new android.support.v7.widget.SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String s) { + sv.clearFocus(); + ((MainActivity) getActivity()).loadChildFragment(SearchListFragment.newInstance(s)); + return true; + } - @Override - public boolean onQueryTextChange(String s) { - return false; - } - }); - } + @Override + public boolean onQueryTextChange(String s) { + return false; + } + }); } @Override @@ -90,7 +100,7 @@ public abstract class PodcastListFragment extends Fragment { } protected void onPodcastSelected(GpodnetPodcast selection) { - if (BuildConfig.DEBUG) Log.d(TAG, "Selected podcast: " + selection.toString()); + Log.d(TAG, "Selected podcast: " + selection.toString()); Intent intent = new Intent(getActivity(), DefaultOnlineFeedViewActivity.class); intent.putExtra(OnlineFeedViewActivity.ARG_FEEDURL, selection.getUrl()); intent.putExtra(DefaultOnlineFeedViewActivity.ARG_TITLE, getString(R.string.gpodnet_main_label)); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/SearchListFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/SearchListFragment.java index 635842196..613e06805 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/SearchListFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/SearchListFragment.java @@ -1,9 +1,11 @@ package de.danoeh.antennapod.fragment.gpodnet; import android.os.Bundle; +import android.support.v4.view.MenuItemCompat; import android.support.v7.widget.SearchView; import android.view.Menu; import android.view.MenuInflater; +import android.view.MenuItem; import org.apache.commons.lang3.Validate; @@ -14,7 +16,6 @@ import de.danoeh.antennapod.core.gpoddernet.GpodnetService; import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException; import de.danoeh.antennapod.core.gpoddernet.model.GpodnetPodcast; import de.danoeh.antennapod.menuhandler.MenuItemUtils; -import de.danoeh.antennapod.menuhandler.NavDrawerActivity; /** * Performs a search on the gpodder.net directory and displays the results. @@ -45,25 +46,26 @@ public class SearchListFragment extends PodcastListFragment { @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - final SearchView sv = new SearchView(getActivity()); - if (!MenuItemUtils.isActivityDrawerOpen((NavDrawerActivity) getActivity())) { - MenuItemUtils.addSearchItem(menu, sv); - sv.setQueryHint(getString(R.string.gpodnet_search_hint)); - sv.setQuery(query, false); - sv.setOnQueryTextListener(new SearchView.OnQueryTextListener() { - @Override - public boolean onQueryTextSubmit(String s) { - sv.clearFocus(); - changeQuery(s); - return true; - } + super.onCreateOptionsMenu(menu, inflater); + // parent already inflated menu + MenuItem searchItem = menu.findItem(R.id.action_search); + final SearchView sv = (SearchView) MenuItemCompat.getActionView(searchItem); + MenuItemUtils.adjustTextColor(getActivity(), sv); + sv.setQueryHint(getString(R.string.gpodnet_search_hint)); + sv.setQuery(query, false); + sv.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String s) { + sv.clearFocus(); + changeQuery(s); + return true; + } - @Override - public boolean onQueryTextChange(String s) { - return false; - } - }); - } + @Override + public boolean onQueryTextChange(String s) { + return false; + } + }); } @Override diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java index cc87407b4..5bd567a2f 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java @@ -5,9 +5,11 @@ import android.content.Context; import android.os.AsyncTask; import android.os.Bundle; import android.support.v4.app.ListFragment; +import android.support.v4.view.MenuItemCompat; import android.support.v7.widget.SearchView; import android.view.Menu; import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import android.widget.AdapterView; import android.widget.TextView; @@ -21,9 +23,9 @@ import de.danoeh.antennapod.core.gpoddernet.GpodnetService; import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException; import de.danoeh.antennapod.core.gpoddernet.model.GpodnetTag; import de.danoeh.antennapod.menuhandler.MenuItemUtils; -import de.danoeh.antennapod.menuhandler.NavDrawerActivity; public class TagListFragment extends ListFragment { + private static final String TAG = "TagListFragment"; private static final int COUNT = 50; @@ -36,27 +38,27 @@ public class TagListFragment extends ListFragment { @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); - if (!MenuItemUtils.isActivityDrawerOpen((NavDrawerActivity) getActivity())) { - final SearchView sv = new SearchView(getActivity()); - MenuItemUtils.addSearchItem(menu, sv); - sv.setQueryHint(getString(R.string.gpodnet_search_hint)); - sv.setOnQueryTextListener(new SearchView.OnQueryTextListener() { - @Override - public boolean onQueryTextSubmit(String s) { - Activity activity = getActivity(); - if (activity != null) { - sv.clearFocus(); - ((MainActivity) activity).loadChildFragment(SearchListFragment.newInstance(s)); - } - return true; + inflater.inflate(R.menu.gpodder_podcasts, menu); + MenuItem searchItem = menu.findItem(R.id.action_search); + final SearchView sv = (SearchView) MenuItemCompat.getActionView(searchItem); + MenuItemUtils.adjustTextColor(getActivity(), sv); + sv.setQueryHint(getString(R.string.gpodnet_search_hint)); + sv.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String s) { + Activity activity = getActivity(); + if (activity != null) { + sv.clearFocus(); + ((MainActivity) activity).loadChildFragment(SearchListFragment.newInstance(s)); } + return true; + } - @Override - public boolean onQueryTextChange(String s) { - return false; - } - }); - } + @Override + public boolean onQueryTextChange(String s) { + return false; + } + }); } @Override diff --git a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java index ebb0a9e58..fe1a09149 100644 --- a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java +++ b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java @@ -3,14 +3,15 @@ package de.danoeh.antennapod.menuhandler; import android.content.Context; import android.content.Intent; import android.net.Uri; +import android.util.Log; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.BuildConfig; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction; import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction.Action; import de.danoeh.antennapod.core.preferences.GpodnetPreferences; +import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.storage.DBWriter; @@ -23,10 +24,10 @@ import de.danoeh.antennapod.core.util.ShareUtils; * Handles interactions with the FeedItemMenu. */ public class FeedItemMenuHandler { + private static final String TAG = "FeedItemMenuHandler"; private FeedItemMenuHandler() { - } /** @@ -55,18 +56,12 @@ public class FeedItemMenuHandler { * @param queueAccess Used for testing if the queue contains the selected item * @return Returns true if selectedItem is not null. */ - public static boolean onPrepareMenu(MenuInterface mi, - FeedItem selectedItem, boolean showExtendedMenu, LongList queueAccess) { + public static boolean onPrepareMenu(MenuInterface mi, FeedItem selectedItem, + boolean showExtendedMenu, LongList queueAccess) { if (selectedItem == null) { return false; } - DownloadRequester requester = DownloadRequester.getInstance(); boolean hasMedia = selectedItem.getMedia() != null; - boolean downloaded = hasMedia && selectedItem.getMedia().isDownloaded(); - boolean downloading = hasMedia - && requester.isDownloadingFile(selectedItem.getMedia()); - boolean notLoadedAndNotLoading = hasMedia && (!downloaded) - && (!downloading); boolean isPlaying = hasMedia && selectedItem.getState() == FeedItem.State.PLAYING; @@ -75,21 +70,14 @@ public class FeedItemMenuHandler { if (!isPlaying) { mi.setItemVisibility(R.id.skip_episode_item, false); } - if (!downloaded || isPlaying) { - mi.setItemVisibility(R.id.play_item, false); - mi.setItemVisibility(R.id.remove_item, false); - } - if (!notLoadedAndNotLoading) { - mi.setItemVisibility(R.id.download_item, false); - } - if (!(notLoadedAndNotLoading | downloading) | isPlaying) { - mi.setItemVisibility(R.id.stream_item, false); - } - if (!downloading) { - mi.setItemVisibility(R.id.cancel_download_item, false); - } boolean isInQueue = queueAccess.contains(selectedItem.getId()); + if(queueAccess.size() == 0 || queueAccess.get(0) == selectedItem.getId()) { + mi.setItemVisibility(R.id.move_to_top_item, false); + } + if(queueAccess.size() == 0 || queueAccess.get(queueAccess.size()-1) == selectedItem.getId()) { + mi.setItemVisibility(R.id.move_to_bottom_item, false); + } if (!isInQueue || isPlaying) { mi.setItemVisibility(R.id.remove_from_queue_item, false); } @@ -100,12 +88,24 @@ public class FeedItemMenuHandler { mi.setItemVisibility(R.id.share_link_item, false); } - if (!BuildConfig.DEBUG - || !(state == FeedItem.State.IN_PROGRESS || state == FeedItem.State.READ)) { + if (!(state == FeedItem.State.UNREAD || state == FeedItem.State.IN_PROGRESS)) { + mi.setItemVisibility(R.id.mark_read_item, false); + } + if (!(state == FeedItem.State.IN_PROGRESS || state == FeedItem.State.READ)) { mi.setItemVisibility(R.id.mark_unread_item, false); } - if (!(state == FeedItem.State.NEW || state == FeedItem.State.IN_PROGRESS)) { - mi.setItemVisibility(R.id.mark_read_item, false); + + if(selectedItem.getMedia() == null || selectedItem.getMedia().getPosition() == 0) { + mi.setItemVisibility(R.id.reset_position, false); + } + + if(false == UserPreferences.isEnableAutodownload()) { + mi.setItemVisibility(R.id.activate_auto_download, false); + mi.setItemVisibility(R.id.deactivate_auto_download, false); + } else if(selectedItem.getAutoDownload()) { + mi.setItemVisibility(R.id.activate_auto_download, false); + } else { + mi.setItemVisibility(R.id.deactivate_auto_download, false); } if (!showExtendedMenu || selectedItem.getLink() == null) { @@ -142,24 +142,14 @@ public class FeedItemMenuHandler { DownloadRequester requester = DownloadRequester.getInstance(); switch (menuItemId) { case R.id.skip_episode_item: - context.sendBroadcast(new Intent( - PlaybackService.ACTION_SKIP_CURRENT_EPISODE)); - break; - case R.id.download_item: - DBTasks.downloadFeedItems(context, selectedItem); - break; - case R.id.play_item: - DBTasks.playMedia(context, selectedItem.getMedia(), true, true, - false); + context.sendBroadcast(new Intent(PlaybackService.ACTION_SKIP_CURRENT_EPISODE)); break; case R.id.remove_item: DBWriter.deleteFeedMediaOfItem(context, selectedItem.getMedia().getId()); break; - case R.id.cancel_download_item: - requester.cancelDownload(context, selectedItem.getMedia()); - break; case R.id.mark_read_item: - DBWriter.markItemRead(context, selectedItem, true, true); + selectedItem.setRead(true); + DBWriter.markItemRead(context, selectedItem, true, false); if(GpodnetPreferences.loggedIn()) { FeedMedia media = selectedItem.getMedia(); GpodnetEpisodeAction actionPlay = new GpodnetEpisodeAction.Builder(selectedItem, Action.PLAY) @@ -173,7 +163,8 @@ public class FeedItemMenuHandler { } break; case R.id.mark_unread_item: - DBWriter.markItemRead(context, selectedItem, false, true); + selectedItem.setRead(false); + DBWriter.markItemRead(context, selectedItem, false, false); if(GpodnetPreferences.loggedIn()) { GpodnetEpisodeAction actionNew = new GpodnetEpisodeAction.Builder(selectedItem, Action.NEW) .currentDeviceId() @@ -182,15 +173,28 @@ public class FeedItemMenuHandler { GpodnetPreferences.enqueueEpisodeAction(actionNew); } break; + case R.id.move_to_top_item: + DBWriter.moveQueueItemToTop(context, selectedItem.getId(), true); + return true; + case R.id.move_to_bottom_item: + DBWriter.moveQueueItemToBottom(context, selectedItem.getId(), true); case R.id.add_to_queue_item: DBWriter.addQueueItem(context, selectedItem.getId()); break; case R.id.remove_from_queue_item: DBWriter.removeQueueItem(context, selectedItem, true); break; - case R.id.stream_item: - DBTasks.playMedia(context, selectedItem.getMedia(), true, true, - true); + case R.id.reset_position: + selectedItem.getMedia().setPosition(0); + DBWriter.markItemRead(context, selectedItem, false, true); + break; + case R.id.activate_auto_download: + selectedItem.setAutoDownload(true); + DBWriter.setFeedItemAutoDownload(context, selectedItem, true); + break; + case R.id.deactivate_auto_download: + selectedItem.setAutoDownload(false); + DBWriter.setFeedItemAutoDownload(context, selectedItem, false); break; case R.id.visit_website_item: Uri uri = Uri.parse(selectedItem.getLink()); @@ -203,6 +207,7 @@ public class FeedItemMenuHandler { ShareUtils.shareFeedItemLink(context, selectedItem); break; default: + Log.d(TAG, "Unknown menuItemId: " + menuItemId); return false; } // Refresh menu state diff --git a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java index efb4adb01..7bd8fedc9 100644 --- a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java +++ b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java @@ -1,5 +1,6 @@ package de.danoeh.antennapod.menuhandler; +import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; @@ -9,8 +10,11 @@ import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.BuildConfig; import de.danoeh.antennapod.core.dialog.ConfirmationDialog; import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.storage.DBTasks; @@ -34,10 +38,8 @@ public class FeedMenuHandler { return true; } - if (BuildConfig.DEBUG) - Log.d(TAG, "Preparing options menu"); - menu.findItem(R.id.mark_all_read_item).setVisible( - selectedFeed.hasNewItems(true)); + Log.d(TAG, "Preparing options menu"); + menu.findItem(R.id.mark_all_read_item).setVisible(selectedFeed.hasNewItems()); if (selectedFeed.getPaymentLink() != null && selectedFeed.getFlattrStatus().flattrable()) menu.findItem(R.id.support_item).setVisible(true); else @@ -62,6 +64,9 @@ public class FeedMenuHandler { case R.id.refresh_complete_item: DBTasks.refreshCompleteFeed(context, selectedFeed); break; + case R.id.hide_items: + showHideDialog(context, selectedFeed); + break; case R.id.mark_all_read_item: ConfirmationDialog conDialog = new ConfirmationDialog(context, R.string.mark_all_read_label, @@ -94,4 +99,43 @@ public class FeedMenuHandler { } return true; } + + private static void showHideDialog(final Context context, final Feed feed) { + + final String[] items = context.getResources().getStringArray(R.array.episode_hide_options); + final String[] values = context.getResources().getStringArray(R.array.episode_hide_values); + final boolean[] checkedItems = new boolean[items.length]; + + final List<String> hidden = new ArrayList<String>(Arrays.asList(feed.getItemFilter().getValues())); + for(int i=0; i < values.length; i++) { + String value = values[i]; + if(hidden.contains(value)) { + checkedItems[i] = true; + } + } + + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.hide_episodes_title); + builder.setMultiChoiceItems(items, checkedItems, new DialogInterface.OnMultiChoiceClickListener() { + @Override + public void onClick(DialogInterface dialog, int which, boolean isChecked) { + if (isChecked) { + hidden.add(values[which]); + } else { + hidden.remove(values[which]); + } + } + }); + builder.setPositiveButton(R.string.confirm_label, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + feed.setHiddenItemProperties(hidden.toArray(new String[hidden.size()])); + DBWriter.setFeedItemsFilter(context, feed.getId(), hidden); + } + }); + builder.setNegativeButton(R.string.cancel_label, null); + builder.create().show(); + + } + } diff --git a/app/src/main/java/de/danoeh/antennapod/menuhandler/MenuItemUtils.java b/app/src/main/java/de/danoeh/antennapod/menuhandler/MenuItemUtils.java index fc942ce20..cfc540fd6 100644 --- a/app/src/main/java/de/danoeh/antennapod/menuhandler/MenuItemUtils.java +++ b/app/src/main/java/de/danoeh/antennapod/menuhandler/MenuItemUtils.java @@ -1,31 +1,44 @@ package de.danoeh.antennapod.menuhandler; -import android.support.v4.view.MenuItemCompat; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.os.Build; import android.support.v7.widget.SearchView; import android.view.Menu; import android.view.MenuItem; +import android.widget.EditText; -import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.preferences.UserPreferences; /** * Utilities for menu items */ public class MenuItemUtils extends de.danoeh.antennapod.core.menuhandler.MenuItemUtils { - public static MenuItem addSearchItem(Menu menu, SearchView searchView) { - MenuItem item = menu.add(Menu.NONE, R.id.search_item, Menu.NONE, R.string.search_label); - MenuItemCompat.setShowAsAction(item, MenuItemCompat.SHOW_AS_ACTION_ALWAYS); - MenuItemCompat.setActionView(item, searchView); - return item; + public static void adjustTextColor(Context context, SearchView sv) { + if(Build.VERSION.SDK_INT < 14) { + EditText searchEditText = (EditText) sv.findViewById(R.id.search_src_text); + if(UserPreferences.getTheme() == de.danoeh.antennapod.R.style.Theme_AntennaPod_Dark) { + searchEditText.setTextColor(Color.WHITE); + } else { + searchEditText.setTextColor(Color.BLACK); + } + } } - /** - * Checks if the navigation drawer of the DrawerActivity is opened. This can be useful for Fragments - * that hide their menu if the navigation drawer is open. - * - * @return True if the drawer is open, false otherwise (also if the parameter is null) - */ - public static boolean isActivityDrawerOpen(NavDrawerActivity activity) { - return activity != null && activity.isDrawerOpen(); + public static void refreshLockItem(Context context, Menu menu) { + final MenuItem queueLock = menu.findItem(de.danoeh.antennapod.R.id.queue_lock); + int[] lockIcons = new int[] { de.danoeh.antennapod.R.attr.ic_lock_open, de.danoeh.antennapod.R.attr.ic_lock_closed }; + TypedArray ta = context.obtainStyledAttributes(lockIcons); + if (UserPreferences.isQueueLocked()) { + queueLock.setTitle(de.danoeh.antennapod.R.string.unlock_queue); + queueLock.setIcon(ta.getDrawable(1)); + } else { + queueLock.setTitle(de.danoeh.antennapod.R.string.lock_queue); + queueLock.setIcon(ta.getDrawable(0)); + } } + } diff --git a/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceController.java b/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceController.java index 32683c65c..f387b7524 100644 --- a/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceController.java +++ b/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceController.java @@ -1,6 +1,7 @@ package de.danoeh.antennapod.preferences; import android.app.Activity; +import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; @@ -190,6 +191,15 @@ public class PreferenceController { } } ); + ui.findPreference(UserPreferences.PREF_HIDDEN_DRAWER_ITEMS) + .setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + showDrawerPreferencesDialog(); + return true; + } + }); + ui.findPreference(UserPreferences.PREF_ENABLE_AUTODL) .setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { @Override @@ -561,6 +571,43 @@ public class PreferenceController { } } + private void showDrawerPreferencesDialog() { + final Context context = ui.getActivity(); + final List<String> hiddenDrawerItems = UserPreferences.getHiddenDrawerItems(); + final String[] navTitles = context.getResources().getStringArray(R.array.nav_drawer_titles); + final String[] NAV_DRAWER_TAGS = MainActivity.NAV_DRAWER_TAGS; + boolean[] checked = new boolean[MainActivity.NAV_DRAWER_TAGS.length]; + for(int i=0; i < NAV_DRAWER_TAGS.length; i++) { + String tag = NAV_DRAWER_TAGS[i]; + if(!hiddenDrawerItems.contains(tag)) { + checked[i] = true; + } + } + + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.drawer_preferences); + builder.setMultiChoiceItems(navTitles, checked, new DialogInterface.OnMultiChoiceClickListener() { + @Override + public void onClick(DialogInterface dialog, int which, boolean isChecked) { + if (isChecked) { + hiddenDrawerItems.remove(NAV_DRAWER_TAGS[which]); + } else { + hiddenDrawerItems.add(NAV_DRAWER_TAGS[which]); + } + } + }); + builder.setPositiveButton(R.string.confirm_label, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + UserPreferences.setHiddenDrawerItems(context, hiddenDrawerItems); + } + }); + builder.setNegativeButton(R.string.cancel_label, null); + builder.create().show(); + } + + + public static interface PreferenceUI { diff --git a/app/src/main/java/de/danoeh/antennapod/service/PlayerWidgetService.java b/app/src/main/java/de/danoeh/antennapod/service/PlayerWidgetService.java index 1d9e8e412..1fe9e2cf9 100644 --- a/app/src/main/java/de/danoeh/antennapod/service/PlayerWidgetService.java +++ b/app/src/main/java/de/danoeh/antennapod/service/PlayerWidgetService.java @@ -52,19 +52,20 @@ public class PlayerWidgetService extends Service { public void onDestroy() { super.onDestroy(); Log.d(TAG, "Service is about to be destroyed"); - - Playable playable = playbackService.getPlayable(); - if(playable != null && playable instanceof FeedMedia) { - FeedMedia media = (FeedMedia) playable; - if(media.hasAlmostEnded()) { - Log.d(TAG, "smart mark as read"); - FeedItem item = media.getItem(); - DBWriter.markItemRead(this, item, true, false); - DBWriter.removeQueueItem(this, item, false); - DBWriter.addItemToPlaybackHistory(this, media); - if (UserPreferences.isAutoDelete()) { - Log.d(TAG, "Delete " + media.toString()); - DBWriter.deleteFeedMediaOfItem(this, media.getId()); + if (playbackService != null) { + Playable playable = playbackService.getPlayable(); + if (playable != null && playable instanceof FeedMedia) { + FeedMedia media = (FeedMedia) playable; + if (media.hasAlmostEnded()) { + Log.d(TAG, "smart mark as read"); + FeedItem item = media.getItem(); + DBWriter.markItemRead(this, item, true, false); + DBWriter.removeQueueItem(this, item, false); + DBWriter.addItemToPlaybackHistory(this, media); + if (UserPreferences.isAutoDelete()) { + Log.d(TAG, "Delete " + media.toString()); + DBWriter.deleteFeedMediaOfItem(this, media.getId()); + } } } } @@ -116,11 +117,12 @@ public class PlayerWidgetService extends Service { views.setTextViewText(R.id.txtvTitle, media.getEpisodeTitle()); + String progressString = getProgressString(media); + if (progressString != null) { + views.setTextViewText(R.id.txtvProgress, progressString); + } + if (status == PlayerStatus.PLAYING) { - String progressString = getProgressString(playbackService); - if (progressString != null) { - views.setTextViewText(R.id.txtvProgress, progressString); - } views.setImageViewResource(R.id.butPlay, R.drawable.ic_pause_white_24dp); if (Build.VERSION.SDK_INT >= 15) { views.setContentDescription(R.id.butPlay, getString(R.string.pause_label)); @@ -156,11 +158,10 @@ public class PlayerWidgetService extends Service { return PendingIntent.getBroadcast(this, 0, startingIntent, 0); } - private String getProgressString(PlaybackService ps) { - int position = ps.getCurrentPosition(); - int duration = ps.getDuration(); - if (position != PlaybackService.INVALID_TIME - && duration != PlaybackService.INVALID_TIME) { + private String getProgressString(Playable media) { + int position = media.getPosition(); + int duration = media.getDuration(); + if (position > 0 && duration > 0) { return Converter.getDurationStringLong(position) + " / " + Converter.getDurationStringLong(duration); } else { diff --git a/app/src/main/res/layout/all_episodes_fragment.xml b/app/src/main/res/layout/all_episodes_fragment.xml new file mode 100644 index 000000000..19db02f1d --- /dev/null +++ b/app/src/main/res/layout/all_episodes_fragment.xml @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="utf-8"?> + +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:dslv="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <com.mobeta.android.dslv.DragSortListView + android:id="@android:id/list" + android:scrollbarStyle="outsideOverlay" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:paddingTop="@dimen/list_vertical_padding" + android:paddingBottom="@dimen/list_vertical_padding" + android:clipToPadding="false" + dslv:collapsed_height="2dp" + dslv:drag_enabled="false" + dslv:drag_scroll_start="0.33" + dslv:float_alpha="0.6" + dslv:max_drag_scroll_speed="0.5" + dslv:remove_enabled="true" + dslv:remove_mode="flingRemove" + dslv:slide_shuffle_speed="0.3" + dslv:sort_enabled="false" + dslv:track_drag_sort="false" + dslv:float_background_color="?attr/dragview_float_background" + dslv:use_default_controller="true" + tools:background="@android:color/holo_green_dark"/> + + <TextView + android:id="@id/android:empty" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center" + android:gravity="center" + android:text="@string/no_items_label"/> + + <ProgressBar + android:id="@+id/progLoading" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:indeterminateOnly="true" + android:visibility="gone" + tools:visibility="visible" + tools:layout_width="match_parent" + tools:layout_height="64dp" + tools:background="@android:color/holo_red_light"/> + +</FrameLayout>
\ No newline at end of file diff --git a/app/src/main/res/layout/audioplayer_activity.xml b/app/src/main/res/layout/audioplayer_activity.xml index 644d8fe90..827e06e00 100644 --- a/app/src/main/res/layout/audioplayer_activity.xml +++ b/app/src/main/res/layout/audioplayer_activity.xml @@ -95,6 +95,18 @@ tools:src="@drawable/ic_fast_rewind_white_36dp" tools:background="@android:color/holo_blue_dark" /> + <TextView + android:id="@+id/txtvRev" + android:layout_width="wrap_content" + android:layout_height="32dp" + android:layout_alignTop="@id/butRev" + android:layout_alignLeft="@id/butRev" + android:layout_alignRight="@id/butRev" + android:gravity="center" + android:text="30" + android:textSize="8dp" + android:clickable="false"/> + <ImageButton android:id="@+id/butFF" android:layout_width="@dimen/audioplayer_playercontrols_length" @@ -106,6 +118,18 @@ tools:src="@drawable/ic_fast_forward_white_36dp" tools:background="@android:color/holo_blue_dark" /> + <TextView + android:id="@+id/txtvFF" + android:layout_width="wrap_content" + android:layout_height="32dp" + android:layout_alignTop="@id/butFF" + android:layout_alignLeft="@id/butFF" + android:layout_alignRight="@id/butFF" + android:gravity="center" + android:text="30" + android:textSize="8dp" + android:clickable="false"/> + <Button android:id="@+id/butPlaybackSpeed" android:layout_width="@dimen/audioplayer_playercontrols_length" diff --git a/app/src/main/res/layout/downloadlog_item.xml b/app/src/main/res/layout/downloadlog_item.xml index df1501222..c6a34a517 100644 --- a/app/src/main/res/layout/downloadlog_item.xml +++ b/app/src/main/res/layout/downloadlog_item.xml @@ -1,78 +1,84 @@ <?xml version="1.0" encoding="utf-8"?> -<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" +<RelativeLayout + xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:orientation="vertical" + android:paddingTop="8dp" + android:paddingLeft="16dp" + android:paddingRight="16dp" + android:paddingBottom="8dp" tools:background="@android:color/darker_gray"> - <RelativeLayout - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginLeft="@dimen/listitem_threeline_horizontalpadding" - android:layout_marginRight="@dimen/listitem_threeline_horizontalpadding" - android:layout_marginTop="@dimen/listitem_threeline_verticalpadding"> - - <TextView - android:id="@+id/txtvType" - style="@style/AntennaPod.TextView.ListItemSecondaryTitle" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_alignParentRight="true" - android:layout_marginLeft="@dimen/listitem_threeline_textleftpadding" - tools:text="Media file" - tools:background="@android:color/holo_green_dark" /> - - <TextView - android:id="@+id/txtvTitle" - style="@style/AntennaPod.TextView.ListItemPrimaryTitle" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_alignParentLeft="true" - android:layout_alignParentTop="true" - android:layout_toLeftOf="@id/txtvType" - tools:text="Download item title" - tools:background="@android:color/holo_blue_light" /> - </RelativeLayout> + <TextView + android:id="@+id/txtvIcon" + android:layout_width="48dp" + android:layout_height="48dp" + android:layout_alignParentTop="true" + android:layout_alignParentLeft="true" + android:textSize="48sp" + tools:text="[Icon]" + android:gravity="center" /> - <RelativeLayout - android:layout_width="match_parent" + <Button + android:id="@+id/btnRetry" + android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginBottom="@dimen/listitem_threeline_verticalpadding" - android:layout_marginLeft="@dimen/listitem_threeline_horizontalpadding" - android:layout_marginRight="@dimen/listitem_threeline_horizontalpadding"> + android:layout_below="@id/txtvIcon" + android:layout_alignLeft="@id/txtvIcon" + android:layout_alignRight="@id/txtvIcon" + android:layout_marginTop="8dp" + tools:text="↻" /> - <TextView - android:id="@+id/txtvDate" - style="@style/AntennaPod.TextView.ListItemSecondaryTitle" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_alignParentLeft="true" - android:layout_marginRight="8dp" - tools:text="January 23" - tools:background="@android:color/holo_green_dark" /> + <TextView + android:id="@+id/txtvType" + style="@style/AntennaPod.TextView.ListItemSecondaryTitle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentTop="true" + android:layout_alignParentRight="true" + android:layout_marginLeft="8dp" + android:layout_marginBottom="8dp" + tools:text="Media file" + tools:background="@android:color/holo_green_dark" /> - <TextView - android:id="@+id/txtvStatus" - style="@style/AntennaPod.TextView.ListItemSecondaryTitle" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_alignParentRight="true" - tools:text="successful" - tools:background="@android:color/holo_green_dark" /> + <TextView + android:id="@+id/txtvTitle" + style="@style/AntennaPod.TextView.ListItemPrimaryTitle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentTop="true" + android:layout_toRightOf="@id/txtvIcon" + android:layout_toLeftOf="@id/txtvType" + android:layout_marginLeft="8dp" + android:layout_marginBottom="8dp" + android:minLines="1" + android:maxLines="2" + tools:text="Download item title" + tools:background="@android:color/holo_blue_light" /> - </RelativeLayout> + <TextView + android:id="@+id/txtvDate" + style="@style/AntennaPod.TextView.ListItemSecondaryTitle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_toRightOf="@id/txtvIcon" + android:layout_below="@id/txtvTitle" + android:layout_marginLeft="8dp" + android:layout_marginBottom="8dp" + tools:text="January 23" + tools:background="@android:color/holo_green_dark" /> <TextView android:id="@+id/txtvReason" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginBottom="@dimen/listitem_threeline_verticalpadding" - android:layout_marginLeft="@dimen/listitem_threeline_horizontalpadding" - android:layout_marginRight="@dimen/listitem_threeline_horizontalpadding" + android:layout_below="@id/txtvDate" + android:layout_toRightOf="@id/txtvIcon" + android:layout_marginLeft="8dp" android:textColor="?android:attr/textColorTertiary" android:textSize="@dimen/text_size_micro" tools:text="@string/design_time_downloaded_log_failure_reason" tools:background="@android:color/holo_green_dark" /> -</LinearLayout>
\ No newline at end of file +</RelativeLayout>
\ No newline at end of file diff --git a/app/src/main/res/layout/feedinfo.xml b/app/src/main/res/layout/feedinfo.xml index a52104afa..db897865c 100644 --- a/app/src/main/res/layout/feedinfo.xml +++ b/app/src/main/res/layout/feedinfo.xml @@ -1,5 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" @@ -11,7 +12,10 @@ android:focusableInTouchMode="true" android:layout_width="match_parent" android:layout_height="wrap_content" - android:gravity="center_vertical"> + android:gravity="center_vertical" + android:layout_marginLeft="16dp" + android:layout_marginRight="16dp" + android:layout_marginTop="8dp"> <ImageView android:id="@+id/imgvCover" @@ -20,26 +24,26 @@ android:layout_height="70dp" android:layout_alignParentLeft="true" android:layout_alignParentTop="true" - android:layout_margin="4dp" tools:src="@drawable/ic_stat_antenna_default" - tools:background="@android:color/holo_green_dark" /> + tools:background="@android:color/holo_green_dark"/> <TextView android:id="@+id/txtvTitle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerVertical="true" - android:layout_margin="4dp" + android:layout_marginLeft="8dp" android:layout_toRightOf="@id/imgvCover" style="@style/AntennaPod.TextView.Heading" tools:text="Feed title" - tools:background="@android:color/holo_green_dark" /> + tools:background="@android:color/holo_green_dark"/> <View android:id="@+id/divider" android:layout_width="match_parent" android:layout_height="1dp" android:layout_below="@id/imgvCover" + android:layout_marginTop="8dp" android:background="@color/bright_blue"/> </RelativeLayout> @@ -47,99 +51,120 @@ android:id="@+id/scrollView" android:layout_width="match_parent" android:layout_height="0dp" - android:layout_weight="1"> + android:layout_weight="1" + android:scrollbarStyle="outsideInset" + android:paddingLeft="16dp" + android:paddingRight="16dp" + android:paddingBottom="8dp"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> - <RelativeLayout + <android.support.v7.widget.GridLayout android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginBottom="16dp"> - - <View - android:id="@+id/center_divider" - android:layout_width="0dp" - android:layout_height="0dp" - android:layout_centerHorizontal="true"/> + android:layout_marginTop="8dp" + app:columnCount="2" + app:rowCount="3"> <TextView android:id="@+id/lblAuthor" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentLeft="true" - android:layout_margin="8dp" + android:layout_marginRight="8dp" + android:layout_marginBottom="8dp" + app:layout_row="0" + app:layout_column="0" + android:lines="1" android:text="@string/author_label" android:textColor="?android:attr/textColorPrimary" - tools:background="@android:color/holo_red_light" /> + tools:background="@android:color/holo_red_light"/> <TextView android:id="@+id/txtvAuthor" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_alignParentRight="true" - android:layout_margin="8dp" - android:layout_toRightOf="@id/center_divider" + app:layout_row="0" + app:layout_column="1" tools:text="Daniel Oeh" - tools:background="@android:color/holo_green_dark" /> + tools:background="@android:color/holo_green_dark"/> <TextView android:id="@+id/lblLanguage" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_alignParentLeft="true" - android:layout_below="@id/txtvAuthor" - android:layout_margin="8dp" + android:layout_marginRight="8dp" + android:layout_marginBottom="8dp" + app:layout_row="1" + app:layout_column="0" + android:lines="1" android:text="@string/language_label" android:textColor="?android:attr/textColorPrimary" - tools:background="@android:color/holo_red_light" /> + tools:background="@android:color/holo_red_light"/> <TextView android:id="@+id/txtvLanguage" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_alignParentRight="true" - android:layout_below="@id/txtvAuthor" - android:layout_margin="8dp" - android:layout_toRightOf="@id/center_divider" + app:layout_row="1" + app:layout_column="1" tools:text="English" - tools:background="@android:color/holo_green_dark" /> - </RelativeLayout> + tools:background="@android:color/holo_green_dark"/> + + <TextView + android:id="@+id/lblUrl" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginRight="8dp" + android:layout_marginBottom="8dp" + app:layout_row="2" + app:layout_column="0" + android:lines="1" + android:text="@string/url_label" + android:textColor="?android:attr/textColorPrimary" + tools:background="@android:color/holo_red_light"/> + + <TextView + android:id="@+id/txtvUrl" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:layout_row="2" + app:layout_column="1" + tools:text="http://www.example.com/feed" + tools:background="@android:color/holo_green_dark"/> + + </android.support.v7.widget.GridLayout> <TextView android:id="@+id/txtvSettings" style="@style/AntennaPod.TextView.Heading" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_margin="8dp" android:text="@string/podcast_settings_label" - android:layout_marginLeft="8dp" - android:layout_marginBottom="8dp" - android:layout_marginTop="24dp"/> + android:layout_marginTop="8dp"/> <CheckBox android:id="@+id/cbxAutoDownload" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_margin="8dp" + android:layout_marginTop="8dp" android:text="@string/auto_download_label" android:enabled="false" android:textColor="?android:attr/textColorPrimary" - tools:background="@android:color/holo_red_light" /> + tools:background="@android:color/holo_red_light" + android:checked="false"/> <TextView android:id="@+id/txtvAuthentication" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_margin="8dp" + android:layout_marginTop="8dp" android:text="@string/authentication_label" android:textSize="@dimen/text_size_medium" - android:textColor="?android:attr/textColorPrimary" - android:layout_marginLeft="8dp" - android:layout_marginBottom="8dp" - android:layout_marginTop="24dp"/> + android:textColor="?android:attr/textColorPrimary"/> <TextView android:id="@+id/txtvAuthenticationDescr" @@ -148,71 +173,74 @@ android:textColor="?android:attr/textColorPrimary" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_margin="8dp"/> + android:layout_marginTop="8dp"/> - <LinearLayout - android:layout_width="match_parent" + <android.support.v7.widget.GridLayout + android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_margin="8dp"> + android:layout_marginTop="8dp" + app:columnCount="2" + app:rowCount="3" + android:layout_gravity="center_horizontal"> <TextView android:id="@+id/txtvUsername" - android:layout_width="0dp" - android:layout_weight="1" + android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_margin="8dp" + android:layout_marginRight="8dp" + android:layout_marginBottom="8dp" + app:layout_row="0" + app:layout_column="0" android:text="@string/username_label" android:textColor="?android:attr/textColorPrimary"/> <EditText android:id="@+id/etxtUsername" - android:layout_width="0dp" - android:layout_weight="2" + android:layout_width="140sp" android:layout_height="wrap_content" + app:layout_row="0" + app:layout_column="1" android:hint="@string/username_label"/> - </LinearLayout> - - <LinearLayout - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_margin="8dp"> <TextView android:id="@+id/txtvPassword" - android:layout_width="0dp" - android:layout_weight="1" + android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_margin="8dp" + android:layout_marginRight="8dp" + android:layout_marginBottom="8dp" + app:layout_row="1" + app:layout_column="0" android:text="@string/password_label" android:textColor="?android:attr/textColorPrimary"/> <EditText android:id="@+id/etxtPassword" - android:layout_width="0dp" - android:layout_weight="2" + android:layout_width="140sp" android:layout_height="wrap_content" + app:layout_row="1" + app:layout_column="1" android:hint="@string/password_label" android:inputType="textPassword"/> - </LinearLayout> + + </android.support.v7.widget.GridLayout> <TextView style="@style/AntennaPod.TextView.Heading" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginLeft="8dp" - android:layout_marginBottom="8dp" - android:layout_marginTop="24dp" + android:layout_marginTop="8dp" android:text="@string/description_label"/> <TextView android:id="@+id/txtvDescription" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_margin="8dp" + android:layout_marginTop="8dp" android:text="@string/design_time_lorem_ipsum" - tools:background="@android:color/holo_green_dark" /> + tools:background="@android:color/holo_green_dark"/> </LinearLayout> + </ScrollView> </LinearLayout>
\ No newline at end of file diff --git a/app/src/main/res/layout/feeditemlist_header.xml b/app/src/main/res/layout/feeditemlist_header.xml index 4619580e3..667f777af 100644 --- a/app/src/main/res/layout/feeditemlist_header.xml +++ b/app/src/main/res/layout/feeditemlist_header.xml @@ -3,7 +3,7 @@ <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="@dimen/feeditemlist_header_height" + android:layout_height="wrap_content" tools:context="de.danoeh.antennapod.activity.MainActivity" tools:background="@android:color/darker_gray"> @@ -11,7 +11,7 @@ android:id="@+id/imgvBackground" style="@style/BigBlurryBackground" android:layout_width="match_parent" - android:layout_height="match_parent" /> + android:layout_height="@dimen/feeditemlist_header_height" /> <ImageView android:id="@+id/imgvCover" @@ -78,5 +78,31 @@ tools:text="Podcast author" tools:background="@android:color/holo_green_dark" /> + <IconTextView + android:id="@+id/txtvFailure" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@id/imgvBackground" + android:paddingTop="2dp" + android:paddingBottom="2dp" + android:background="@color/download_failed_red" + android:gravity="center" + android:textColor="@color/white" + android:visibility="gone" + android:text="@string/refresh_failed_msg" + tools:text="(!) Last refresh failed" + /> + + <TextView + android:id="@+id/txtvInformation" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@id/imgvBackground" + android:paddingTop="2dp" + android:paddingBottom="2dp" + android:gravity="center" + android:visibility="gone" + tools:text="(i) Information" + /> </RelativeLayout>
\ No newline at end of file diff --git a/app/src/main/res/layout/feeditemlist_item.xml b/app/src/main/res/layout/feeditemlist_item.xml index 03595990e..6b7c45978 100644 --- a/app/src/main/res/layout/feeditemlist_item.xml +++ b/app/src/main/res/layout/feeditemlist_item.xml @@ -22,8 +22,10 @@ android:layout_height="wrap_content" android:layout_alignParentRight="true" android:layout_alignParentTop="true" - android:layout_margin="16dp" - tools:text="Status unread" + android:layout_marginTop="16dp" + android:layout_marginLeft="8dp" + android:layout_marginRight="8dp" + tools:text="NEW" tools:background="@android:color/white" /> <TextView @@ -36,9 +38,18 @@ android:layout_marginBottom="8dp" android:layout_marginTop="@dimen/listitem_threeline_verticalpadding" android:layout_toLeftOf="@id/statusUnread" - tools:text="Feed item name" + tools:text="Episode title" tools:background="@android:color/holo_green_dark" /> + <TextView + android:id="@+id/txtvLenSize" + style="@style/AntennaPod.TextView.ListItemSecondaryTitle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentLeft="true" + android:layout_below="@id/txtvItemname" + tools:text="00:42:23" + tools:background="@android:color/holo_green_dark" /> <ImageView android:id="@+id/imgvInPlaylist" @@ -46,7 +57,7 @@ android:layout_height="@dimen/enc_icons_size" android:layout_alignParentRight="true" android:layout_below="@id/txtvItemname" - android:layout_marginRight="4dp" + android:layout_marginRight="8dp" android:contentDescription="@string/in_queue_label" android:src="?attr/stat_playlist" android:visibility="visible" @@ -58,20 +69,21 @@ android:layout_width="@dimen/enc_icons_size" android:layout_height="@dimen/enc_icons_size" android:layout_below="@id/txtvItemname" - android:layout_marginRight="4dp" - android:layout_toLeftOf="@+id/imgvInPlaylist" + android:layout_marginRight="8dp" + android:layout_toLeftOf="@id/imgvInPlaylist" tools:ignore="ContentDescription" tools:src="@drawable/ic_hearing_white_18dp" tools:background="@android:color/holo_red_light" /> <TextView - android:id="@+id/txtvLenSize" + android:id="@+id/txtvPublished" style="@style/AntennaPod.TextView.ListItemSecondaryTitle" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_alignParentLeft="true" android:layout_below="@id/txtvItemname" - tools:text="00:42:23" + android:layout_marginRight="8dp" + android:layout_toLeftOf="@id/imgvType" + tools:text="Jan 23" tools:background="@android:color/holo_green_dark" /> <ProgressBar @@ -79,24 +91,20 @@ style="?android:attr/progressBarStyleHorizontal" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_below="@id/txtvItemname" - android:layout_marginLeft="4dp" - android:layout_marginRight="4dp" - android:layout_toLeftOf="@id/imgvType" + android:layout_marginLeft="8dp" + android:layout_marginRight="8dp" + android:layout_toLeftOf="@id/txtvPublished" android:layout_toRightOf="@id/txtvLenSize" - tools:background="@android:color/holo_blue_light" /> + android:layout_alignTop="@id/txtvPublished" + android:layout_alignBottom="@id/txtvPublished" + tools:background="@android:color/holo_blue_light" + android:max="100" + android:progress="42" + android:indeterminate="false" + /> + - <TextView - android:id="@+id/txtvPublished" - style="@style/AntennaPod.TextView.ListItemSecondaryTitle" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_below="@id/txtvItemname" - android:layout_marginRight="4dp" - android:layout_toLeftOf="@id/imgvType" - tools:text="Jan 23" - tools:background="@android:color/holo_green_dark" /> </RelativeLayout> <include layout="@layout/vertical_list_divider"/> diff --git a/app/src/main/res/layout/nav_feedlistitem.xml b/app/src/main/res/layout/nav_feedlistitem.xml index e942d1b08..238beff88 100644 --- a/app/src/main/res/layout/nav_feedlistitem.xml +++ b/app/src/main/res/layout/nav_feedlistitem.xml @@ -7,7 +7,6 @@ android:layout_height="@dimen/listitem_iconwithtext_height" tools:background="@android:color/darker_gray"> - <ImageView android:id="@+id/imgvCover" android:contentDescription="@string/cover_label" @@ -18,13 +17,12 @@ android:adjustViewBounds="true" android:cropToPadding="true" android:scaleType="fitXY" - android:layout_marginTop="8dp" - android:layout_marginBottom="8dp" + android:layout_marginTop="4dp" + android:layout_marginBottom="4dp" android:layout_marginLeft="@dimen/listitem_icon_leftpadding" tools:src="@drawable/ic_stat_antenna_default" tools:background="@android:color/holo_green_dark"/> - <TextView android:id="@+id/txtvTitle" android:lines="1" @@ -36,11 +34,37 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="@dimen/listitem_iconwithtext_textleftpadding" - android:layout_marginTop="@dimen/listitem_iconwithtext_textverticalpadding" - android:layout_marginBottom="@dimen/listitem_iconwithtext_textverticalpadding" android:layout_marginRight="@dimen/listitem_icon_rightpadding" android:layout_toRightOf="@id/imgvCover" tools:text="Navigation feed item title" - tools:background="@android:color/holo_green_dark" - /> -</RelativeLayout>
\ No newline at end of file + tools:background="@android:color/holo_green_dark"/> + + <TextView + android:id="@+id/txtvCount" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:lines="1" + android:textColor="?android:attr/textColorTertiary" + android:textSize="@dimen/text_size_navdrawer" + android:layout_marginLeft="8dp" + android:layout_marginRight="@dimen/listitem_icon_rightpadding" + android:layout_alignParentRight="true" + android:layout_centerVertical="true" + tools:text="23" + tools:background="@android:color/holo_green_dark"/> + + <IconTextView + android:id="@+id/itxtvFailure" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_toLeftOf="@id/txtvCount" + android:lines="1" + android:text="{fa-exclamation-circle}" + android:textColor="@color/download_failed_red" + android:textSize="@dimen/text_size_navdrawer" + android:layout_marginLeft="8dp" + android:layout_centerVertical="true" + tools:text="!" + tools:background="@android:color/holo_green_dark"/> + +</RelativeLayout> diff --git a/app/src/main/res/layout/nav_list.xml b/app/src/main/res/layout/nav_list.xml index 8c46e456d..9fcf9d9fc 100644 --- a/app/src/main/res/layout/nav_list.xml +++ b/app/src/main/res/layout/nav_list.xml @@ -1,6 +1,7 @@ <?xml version="1.0" encoding="utf-8"?> -<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" +<RelativeLayout + xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/nav_layout" android:layout_width="@dimen/drawer_width" @@ -9,32 +10,11 @@ android:background="?attr/nav_drawer_background" android:orientation="vertical"> - <ListView - android:id="@+id/nav_list" - android:layout_width="@dimen/drawer_width" - android:layout_height="0dp" - android:layout_weight="1" - android:choiceMode="singleChoice" - android:clipToPadding="false" - android:divider="@android:color/transparent" - android:dividerHeight="0dp" - android:paddingBottom="@dimen/list_vertical_padding" - android:paddingTop="@dimen/list_vertical_padding" - android:scrollbarStyle="outsideOverlay" - tools:listitem="@layout/nav_listitem" - tools:background="@android:color/holo_purple" /> - - <View - android:layout_width="@dimen/drawer_width" - android:layout_height="1dp" - android:layout_centerVertical="true" - android:background="?android:attr/listDivider" - tools:background="@android:color/holo_red_dark" /> - <LinearLayout android:id="@+id/nav_settings" android:layout_width="@dimen/drawer_width" android:layout_height="@dimen/listitem_iconwithtext_height" + android:layout_alignParentBottom="true" android:background="?attr/selectableItemBackground" android:contentDescription="@string/settings_label" android:orientation="horizontal"> @@ -45,9 +25,9 @@ android:layout_height="@dimen/thumbnail_length_navlist" android:layout_alignParentLeft="true" android:layout_centerVertical="true" - android:layout_marginBottom="8dp" + android:layout_marginBottom="4dp" android:layout_marginLeft="@dimen/listitem_icon_leftpadding" - android:layout_marginTop="8dp" + android:layout_marginTop="4dp" android:adjustViewBounds="true" android:contentDescription="@string/cover_label" android:cropToPadding="true" @@ -58,10 +38,10 @@ tools:background="@android:color/holo_orange_dark" /> <TextView - android:layout_width="0dp" - android:layout_height="match_parent" + android:layout_width="wrap_content" + android:layout_height="wrap_content" android:layout_gravity="center_vertical" - android:layout_margin="16dp" + android:layout_marginLeft="16dp" android:layout_weight="1" android:gravity="center_vertical" android:text="@string/settings_label" @@ -71,4 +51,30 @@ </LinearLayout> -</LinearLayout>
\ No newline at end of file + <View + android:id="@+id/divider" + android:layout_width="@dimen/drawer_width" + android:layout_height="1dp" + android:layout_centerVertical="true" + android:layout_above="@id/nav_settings" + android:background="?android:attr/listDivider" + tools:background="@android:color/holo_red_dark" /> + + <ListView + android:id="@+id/nav_list" + android:layout_width="@dimen/drawer_width" + android:layout_height="wrap_content" + android:layout_weight="1" + android:layout_above="@id/divider" + android:layout_alignParentTop="true" + android:choiceMode="singleChoice" + android:clipToPadding="false" + android:divider="@android:color/transparent" + android:dividerHeight="0dp" + android:paddingBottom="@dimen/list_vertical_padding" + android:paddingTop="@dimen/list_vertical_padding" + android:scrollbarStyle="outsideOverlay" + tools:listitem="@layout/nav_listitem" + tools:background="@android:color/holo_purple" /> + +</RelativeLayout> diff --git a/app/src/main/res/layout/nav_listitem.xml b/app/src/main/res/layout/nav_listitem.xml index bb5865ee6..d62672c34 100644 --- a/app/src/main/res/layout/nav_listitem.xml +++ b/app/src/main/res/layout/nav_listitem.xml @@ -16,11 +16,11 @@ android:layout_centerVertical="true" android:adjustViewBounds="true" android:cropToPadding="true" - android:scaleType="centerCrop" - android:padding="8dp" + android:scaleType="centerInside" + android:padding="4dp" android:layout_marginLeft="@dimen/listitem_icon_leftpadding" - android:layout_marginTop="8dp" - android:layout_marginBottom="8dp" + android:layout_marginTop="4dp" + android:layout_marginBottom="4dp" tools:src="@drawable/ic_new_releases_white_24dp" tools:background="@android:color/holo_green_dark"/> @@ -36,8 +36,6 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="@dimen/listitem_iconwithtext_textleftpadding" - android:layout_marginTop="@dimen/listitem_iconwithtext_textverticalpadding" - android:layout_marginBottom="@dimen/listitem_iconwithtext_textverticalpadding" android:layout_marginRight="48dp" android:layout_toRightOf="@id/imgvCover" tools:text="Navigation item title" @@ -52,8 +50,6 @@ android:textColor="?android:attr/textColorTertiary" android:textSize="@dimen/text_size_navdrawer" android:layout_marginLeft="12dp" - android:layout_marginTop="14dp" - android:layout_marginBottom="14dp" android:layout_marginRight="@dimen/listitem_icon_rightpadding" android:layout_alignParentRight="true" android:layout_centerVertical="true" diff --git a/app/src/main/res/layout/nav_section_item.xml b/app/src/main/res/layout/nav_section_item.xml index 77b2ff253..fa1db865d 100644 --- a/app/src/main/res/layout/nav_section_item.xml +++ b/app/src/main/res/layout/nav_section_item.xml @@ -3,7 +3,7 @@ <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="24dp" + android:layout_height="16dp" android:background="@android:color/transparent" android:orientation="vertical"> diff --git a/app/src/main/res/layout/new_episodes_listitem.xml b/app/src/main/res/layout/new_episodes_listitem.xml index 43ada14b0..b738cf836 100644 --- a/app/src/main/res/layout/new_episodes_listitem.xml +++ b/app/src/main/res/layout/new_episodes_listitem.xml @@ -37,7 +37,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentRight="true" - android:layout_alignParentTop="true" /> + android:layout_alignParentTop="true" + android:layout_marginLeft="8dp"/> <TextView android:id="@+id/txtvTitle" @@ -60,38 +61,26 @@ android:layout_marginTop="16dp" tools:background="@android:color/holo_red_light" > + <TextView + android:id="@+id/txtvDuration" + style="@style/AntennaPod.TextView.ListItemSecondaryTitle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentLeft="true" + tools:text="00:42:23" + tools:background="@android:color/holo_blue_dark" /> + <ImageView - android:id="@id/imgvInPlaylist" + android:id="@+id/imgvInPlaylist" android:layout_width="@dimen/enc_icons_size" android:layout_height="@dimen/enc_icons_size" android:layout_alignParentRight="true" android:layout_marginLeft="8dp" - android:layout_marginRight="4dp" android:contentDescription="@string/in_queue_label" android:src="?attr/stat_playlist" tools:src="@drawable/ic_list_grey600_24dp" tools:background="@android:color/black" /> - <ProgressBar - android:id="@+id/pbar_download_progress" - style="?android:attr/progressBarStyleHorizontal" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_alignParentLeft="true" - android:layout_marginRight="8dp" - android:layout_toLeftOf="@id/imgvInPlaylist" - android:max="100" /> - - <TextView - android:id="@+id/txtvDuration" - style="@style/AntennaPod.TextView.ListItemSecondaryTitle" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_alignParentLeft="true" - android:layout_toLeftOf="@id/imgvInPlaylist" - tools:text="00:42:23" - tools:background="@android:color/holo_blue_dark" /> - <TextView android:id="@+id/txtvPublished" style="@style/AntennaPod.TextView.ListItemSecondaryTitle" @@ -103,6 +92,17 @@ tools:text="Jan 23" tools:background="@android:color/holo_green_dark" /> + <ProgressBar + android:id="@+id/pbar_download_progress" + style="?android:attr/progressBarStyleHorizontal" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginLeft="8dp" + android:layout_marginRight="8dp" + android:layout_toLeftOf="@id/txtvPublished" + android:layout_toRightOf="@id/txtvDuration" + android:max="100" /> + </RelativeLayout> </RelativeLayout> diff --git a/app/src/main/res/layout/queue_listitem.xml b/app/src/main/res/layout/queue_listitem.xml index bc5b951a2..39e9c72a5 100644 --- a/app/src/main/res/layout/queue_listitem.xml +++ b/app/src/main/res/layout/queue_listitem.xml @@ -1,17 +1,19 @@ <?xml version="1.0" encoding="utf-8"?> -<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="@dimen/listitem_threeline_height" android:orientation="horizontal" + android:paddingLeft="16dp" tools:background="@android:color/darker_gray" > <ImageView android:id="@+id/drag_handle" android:layout_width="100dp" android:layout_height="match_parent" - android:layout_marginLeft="8dp" + android:layout_marginLeft="-8dp" android:layout_marginRight="-64dp" android:contentDescription="@string/drag_handle_content_description" android:scaleType="fitXY" diff --git a/app/src/main/res/menu/allepisodes_context.xml b/app/src/main/res/menu/allepisodes_context.xml new file mode 100644 index 000000000..f89ad5065 --- /dev/null +++ b/app/src/main/res/menu/allepisodes_context.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> + +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + + <item + android:id="@id/skip_episode_item" + android:menuCategory="container" + android:title="@string/skip_episode_label" /> + + <item + android:id="@+id/mark_read_item" + android:menuCategory="container" + android:title="@string/mark_read_label" /> + <item + android:id="@+id/mark_unread_item" + android:menuCategory="container" + android:title="@string/mark_unread_label" /> + + <item + android:id="@+id/add_to_queue_item" + android:menuCategory="container" + android:title="@string/add_to_queue_label" /> + <item + android:id="@+id/remove_from_queue_item" + android:menuCategory="container" + android:title="@string/remove_from_queue_label" /> + + <item + android:id="@+id/reset_position" + android:menuCategory="container" + android:title="@string/reset_position" /> + + <item + android:id="@+id/activate_auto_download" + android:menuCategory="container" + android:title="@string/activate_auto_download" /> + <item + android:id="@+id/deactivate_auto_download" + android:menuCategory="container" + android:title="@string/deactivate_auto_download" /> + + <item + android:id="@+id/share_link_item" + android:menuCategory="container" + android:title="@string/share_link_label" /> + <item + android:id="@+id/visit_website_item" + android:menuCategory="container" + android:title="@string/visit_website_label" /> + + <item + android:id="@+id/support_item" + android:menuCategory="container" + android:title="@string/support_label" /> + +</menu>
\ No newline at end of file diff --git a/app/src/main/res/menu/feeditem.xml b/app/src/main/res/menu/feeditem.xml deleted file mode 100644 index 8227f8b14..000000000 --- a/app/src/main/res/menu/feeditem.xml +++ /dev/null @@ -1,77 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<menu xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:custom="http://schemas.android.com/apk/res-auto"> - - <item - android:id="@+id/download_item" - android:icon="?attr/av_download" - custom:showAsAction="collapseActionView" - android:title="@string/download_label"> - </item> - <item - android:id="@+id/stream_item" - android:icon="?attr/action_stream" - custom:showAsAction="collapseActionView" - android:title="@string/stream_label"> - </item> - <item - android:id="@+id/play_item" - android:icon="?attr/av_play" - custom:showAsAction="collapseActionView" - android:title="@string/play_label"> - </item> - <item - android:id="@+id/remove_item" - android:icon="?attr/content_discard" - custom:showAsAction="collapseActionView" - android:title="@string/remove_label"> - </item> - <item - android:id="@id/skip_episode_item" - android:title="@string/skip_episode_label" - custom:showAsAction="collapseActionView"> - </item> - <item - android:id="@+id/cancel_download_item" - android:icon="?attr/navigation_cancel" - custom:showAsAction="ifRoom|collapseActionView" - android:title="@string/cancel_download_label"> - </item> - <item - android:id="@+id/mark_read_item" - custom:showAsAction="collapseActionView" - android:title="@string/mark_read_label"> - </item> - <item - android:id="@+id/mark_unread_item" - custom:showAsAction="collapseActionView" - android:title="@string/mark_unread_label"> - </item> - <item - android:id="@+id/add_to_queue_item" - custom:showAsAction="collapseActionView" - android:title="@string/add_to_queue_label"> - </item> - <item - android:id="@+id/remove_from_queue_item" - custom:showAsAction="collapseActionView" - android:title="@string/remove_from_queue_label"> - </item> - <item - android:id="@+id/share_link_item" - custom:showAsAction="collapseActionView" - android:title="@string/share_link_label"> - </item> - <item - android:id="@+id/visit_website_item" - android:icon="?attr/location_web_site" - custom:showAsAction="collapseActionView" - android:title="@string/visit_website_label"> - </item> - <item - android:id="@+id/support_item" - custom:showAsAction="collapseActionView" - android:title="@string/support_label"> - </item> - -</menu>
\ No newline at end of file diff --git a/app/src/main/res/menu/feeditem_dialog.xml b/app/src/main/res/menu/feeditem_options.xml index f33b7502a..f8e9b9c75 100644 --- a/app/src/main/res/menu/feeditem_dialog.xml +++ b/app/src/main/res/menu/feeditem_options.xml @@ -18,6 +18,7 @@ custom:showAsAction="collapseActionView" android:title="@string/mark_unread_label"> </item> + <item android:id="@+id/add_to_queue_item" custom:showAsAction="collapseActionView" @@ -28,6 +29,24 @@ custom:showAsAction="collapseActionView" android:title="@string/remove_from_queue_label"> </item> + + <item + android:id="@+id/reset_position" + custom:showAsAction="collapseActionView" + android:title="@string/reset_position"> + </item> + + <item + android:id="@+id/activate_auto_download" + custom:showAsAction="collapseActionView" + android:title="@string/activate_auto_download"> + </item> + <item + android:id="@+id/deactivate_auto_download" + custom:showAsAction="collapseActionView" + android:title="@string/deactivate_auto_download"> + </item> + <item android:id="@+id/share_link_item" custom:showAsAction="collapseActionView" diff --git a/app/src/main/res/menu/feeditemlist_context.xml b/app/src/main/res/menu/feeditemlist_context.xml new file mode 100644 index 000000000..f89ad5065 --- /dev/null +++ b/app/src/main/res/menu/feeditemlist_context.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> + +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + + <item + android:id="@id/skip_episode_item" + android:menuCategory="container" + android:title="@string/skip_episode_label" /> + + <item + android:id="@+id/mark_read_item" + android:menuCategory="container" + android:title="@string/mark_read_label" /> + <item + android:id="@+id/mark_unread_item" + android:menuCategory="container" + android:title="@string/mark_unread_label" /> + + <item + android:id="@+id/add_to_queue_item" + android:menuCategory="container" + android:title="@string/add_to_queue_label" /> + <item + android:id="@+id/remove_from_queue_item" + android:menuCategory="container" + android:title="@string/remove_from_queue_label" /> + + <item + android:id="@+id/reset_position" + android:menuCategory="container" + android:title="@string/reset_position" /> + + <item + android:id="@+id/activate_auto_download" + android:menuCategory="container" + android:title="@string/activate_auto_download" /> + <item + android:id="@+id/deactivate_auto_download" + android:menuCategory="container" + android:title="@string/deactivate_auto_download" /> + + <item + android:id="@+id/share_link_item" + android:menuCategory="container" + android:title="@string/share_link_label" /> + <item + android:id="@+id/visit_website_item" + android:menuCategory="container" + android:title="@string/visit_website_label" /> + + <item + android:id="@+id/support_item" + android:menuCategory="container" + android:title="@string/support_label" /> + +</menu>
\ No newline at end of file diff --git a/app/src/main/res/menu/feedlist.xml b/app/src/main/res/menu/feedlist.xml index b6512e828..e0da72667 100644 --- a/app/src/main/res/menu/feedlist.xml +++ b/app/src/main/res/menu/feedlist.xml @@ -3,6 +3,13 @@ xmlns:custom="http://schemas.android.com/apk/res-auto"> <item + android:id="@+id/hide_items" + android:icon="?attr/ic_filter" + android:menuCategory="container" + android:title="@string/hide_episodes_title" + custom:showAsAction="always"> + </item> + <item android:id="@+id/refresh_item" android:icon="?attr/navigation_refresh" android:menuCategory="container" @@ -15,6 +22,14 @@ android:title="@string/load_complete_feed" custom:showAsAction="collapseActionView"> </item> + + <item + android:id="@+id/action_search" + android:icon="?attr/action_search" + custom:showAsAction="always" + custom:actionViewClass="android.support.v7.widget.SearchView" + android:title="@string/search_label"/> + <item android:id="@+id/mark_all_read_item" android:menuCategory="container" diff --git a/app/src/main/res/menu/gpodder_podcasts.xml b/app/src/main/res/menu/gpodder_podcasts.xml new file mode 100644 index 000000000..88fa36a4a --- /dev/null +++ b/app/src/main/res/menu/gpodder_podcasts.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> + +<menu xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:custom="http://schemas.android.com/apk/res-auto"> + + <item + android:id="@+id/action_search" + android:icon="?attr/action_search" + custom:showAsAction="collapseActionView|ifRoom" + custom:actionViewClass="android.support.v7.widget.SearchView" + android:title="@string/search_label"/> + +</menu> diff --git a/app/src/main/res/menu/new_episodes.xml b/app/src/main/res/menu/new_episodes.xml index 72661a17e..1e1d7ab78 100644 --- a/app/src/main/res/menu/new_episodes.xml +++ b/app/src/main/res/menu/new_episodes.xml @@ -4,6 +4,13 @@ xmlns:custom="http://schemas.android.com/apk/res-auto"> <item + android:id="@+id/action_search" + android:icon="?attr/action_search" + custom:showAsAction="always" + custom:actionViewClass="android.support.v7.widget.SearchView" + android:title="@string/search_label"/> + + <item android:id="@+id/refresh_item" android:title="@string/refresh_label" android:menuCategory="container" @@ -17,11 +24,4 @@ custom:showAsAction="collapseActionView" android:icon="?attr/navigation_accept"/> - <item - android:id="@+id/episode_filter_item" - android:title="@string/episode_filter_label" - android:menuCategory="container" - android:checkable="true" - custom:showAsAction="collapseActionView"/> - </menu>
\ No newline at end of file diff --git a/app/src/main/res/menu/queue.xml b/app/src/main/res/menu/queue.xml index c7dd4d371..01a11b10e 100644 --- a/app/src/main/res/menu/queue.xml +++ b/app/src/main/res/menu/queue.xml @@ -4,6 +4,12 @@ xmlns:custom="http://schemas.android.com/apk/res-auto"> <item + android:id="@+id/queue_lock" + android:title="" + android:menuCategory="container" + custom:showAsAction="always" /> + + <item android:id="@+id/refresh_item" android:title="@string/refresh_label" android:menuCategory="container" @@ -11,8 +17,15 @@ android:icon="?attr/navigation_refresh"/> <item + android:id="@+id/action_search" + android:icon="?attr/action_search" + custom:showAsAction="collapseActionView|ifRoom" + custom:actionViewClass="android.support.v7.widget.SearchView" + android:title="@string/search_label"/> + + <item android:id="@+id/clear_queue" - android:title="Clear Queue" + android:title="@string/clear_queue_label" android:menuCategory="container" custom:showAsAction="collapseActionView" android:icon="?attr/navigation_accept"/> diff --git a/app/src/main/res/menu/queue_context.xml b/app/src/main/res/menu/queue_context.xml index 327600038..6ab2daabf 100644 --- a/app/src/main/res/menu/queue_context.xml +++ b/app/src/main/res/menu/queue_context.xml @@ -8,13 +8,53 @@ android:title="@string/move_to_top_label" /> <item + android:id="@+id/move_to_bottom_item" + android:menuCategory="container" + android:title="@string/move_to_bottom_label" /> + + <item + android:id="@+id/mark_read_item" + android:menuCategory="container" + android:title="@string/mark_read_label" /> + + <item + android:id="@+id/mark_unread_item" + android:menuCategory="container" + android:title="@string/mark_unread_label" /> + + <item android:id="@+id/remove_from_queue_item" android:menuCategory="container" android:title="@string/remove_from_queue_label" /> <item - android:id="@+id/move_to_bottom_item" + android:id="@+id/reset_position" android:menuCategory="container" - android:title="@string/move_to_bottom_label" /> + android:title="@string/reset_position" /> + + <item + android:id="@+id/activate_auto_download" + android:menuCategory="container" + android:title="@string/activate_auto_download" /> + + <item + android:id="@+id/deactivate_auto_download" + android:menuCategory="container" + android:title="@string/deactivate_auto_download" /> + + <item + android:id="@+id/share_link_item" + android:menuCategory="container" + android:title="@string/share_link_label" /> + <item + android:id="@+id/visit_website_item" + android:menuCategory="container" + android:title="@string/visit_website_label" /> + + <item + android:id="@+id/support_item" + android:menuCategory="container" + android:title="@string/support_label" /> + </menu>
\ No newline at end of file diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index eb6a3fe1f..e848915cf 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -9,6 +9,10 @@ android:key="prefTheme" android:summary="@string/pref_set_theme_sum" android:defaultValue="0"/> + <Preference + android:key="prefHiddenDrawerItems" + android:summary="@string/pref_nav_drawer_items_sum" + android:title="@string/pref_nav_drawer_items_title" /> <CheckBoxPreference android:defaultValue="false" android:enabled="true" @@ -77,14 +81,12 @@ android:key="prefPauseForFocusLoss" android:summary="@string/pref_pausePlaybackForFocusLoss_sum" android:title="@string/pref_pausePlaybackForFocusLoss_title" /> - - <ListPreference - android:defaultValue="30" - android:entries="@array/seek_delta_values" - android:entryValues="@array/seek_delta_values" - android:key="prefSeekDeltaSecs" - android:summary="@string/pref_seek_delta_sum" - android:title="@string/pref_seek_delta_title" /> + <CheckBoxPreference + android:defaultValue="true" + android:enabled="true" + android:key="prefResumeAfterCall" + android:summary="@string/pref_resumeAfterCall_sum" + android:title="@string/pref_resumeAfterCall_title"/> </PreferenceCategory> <PreferenceCategory android:title="@string/network_pref"> diff --git a/app/src/main/assets/about.html b/app/src/main/templates/about.html index f1a1fdf44..3313fa12f 100644 --- a/app/src/main/assets/about.html +++ b/app/src/main/templates/about.html @@ -41,9 +41,11 @@ <div id="header" align="center"> <img src="logo.png" alt="Logo" width="100px" height="100px"/> - <p>AntennaPod, Version 1.1</p> + <p>AntennaPod, Version @versionname@, Build @versioncode@</p> - <p>Copyright © 2014 Daniel Oeh</p> + <p>Created by Daniel Oeh</p> + + <p>Copyright © 2015 AntennaPod Contributors <a href="https://github.com/AntennaPod/AntennaPod/blob/master/CONTRIBUTORS">(View)</a></p> <p>Licensed under the MIT License <a href="LICENSE.html">(View)</a></p> </div> @@ -77,8 +79,11 @@ licensed under the Apache 2.0 license <a href="LICENSE_OKIO.txt">(View)</a> <h2>Material Design Icons <a href="https://github.com/google/material-design-icons">(Link)</a></h2> by Google, licensed under an Attribution-ShareAlike 4.0 International license <a href="LICENSE_MATERIAL_DESIGN_ICONS.txt">(View)</a> -<h2>EventBus <a href="https://github.com/greenrobot/EventBus">(Link>)</a></h2> +<h2>EventBus <a href="https://github.com/greenrobot/EventBus">(Link)</a></h2> by greenrobot, licensed under the Apache 2.0 license <a href="LICENSE_EVENTBUS.txt">(View)</a> +<h2>Iconify <a href="https://github.com/JoanZapata/android-iconify">(Link)</a></h2> +by Joan Zapata, licensed under the Apache 2.0 license <a href="LICENSE_ANDROID_ICONIFY.txt">(View)</a> + </body> </html> diff --git a/build.gradle b/build.gradle index 9f1a2d6ab..363f02283 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:1.0.0' + classpath 'com.android.tools.build:gradle:1.2.3' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files @@ -19,5 +19,5 @@ allprojects { } task wrapper(type: Wrapper) { - gradleVersion = '2.2.1' + gradleVersion = '2.4' }
\ No newline at end of file diff --git a/core/build.gradle b/core/build.gradle index c327f194c..ae2c11070 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -9,6 +9,8 @@ android { targetSdkVersion 21 versionCode 1 versionName "1.0" + testApplicationId "de.danoeh.antennapod.core.tests" + testInstrumentationRunner "de.danoeh.antennapod.core.tests.AntennaPodTestRunner" } buildTypes { release { @@ -40,9 +42,9 @@ dependencies { compile 'commons-io:commons-io:2.4' compile 'com.jayway.android.robotium:robotium-solo:5.2.1' compile 'org.jsoup:jsoup:1.7.3' - compile 'com.squareup.picasso:picasso:2.4.0' - compile 'com.squareup.okhttp:okhttp:2.2.0' - compile 'com.squareup.okhttp:okhttp-urlconnection:2.2.0' + compile 'com.squareup.picasso:picasso:2.5.2' + compile 'com.squareup.okhttp:okhttp:2.3.0' + compile 'com.squareup.okhttp:okhttp-urlconnection:2.3.0' compile 'com.squareup.okio:okio:1.2.0' compile 'com.nineoldandroids:library:2.4.0' compile 'de.greenrobot:eventbus:2.4.0' diff --git a/core/src/androidTest/java/de/danoeh/antennapod/core/ApplicationTest.java b/core/src/androidTest/java/de/danoeh/antennapod/core/ApplicationTest.java deleted file mode 100644 index 894bcfa63..000000000 --- a/core/src/androidTest/java/de/danoeh/antennapod/core/ApplicationTest.java +++ /dev/null @@ -1,13 +0,0 @@ -package de.danoeh.antennapod.core; - -import android.app.Application; -import android.test.ApplicationTestCase; - -/** - * <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a> - */ -public class ApplicationTest extends ApplicationTestCase<Application> { - public ApplicationTest() { - super(Application.class); - } -}
\ No newline at end of file diff --git a/core/src/androidTest/java/de/danoeh/antennapod/core/tests/AntennaPodTestRunner.java b/core/src/androidTest/java/de/danoeh/antennapod/core/tests/AntennaPodTestRunner.java new file mode 100644 index 000000000..fbb5459d4 --- /dev/null +++ b/core/src/androidTest/java/de/danoeh/antennapod/core/tests/AntennaPodTestRunner.java @@ -0,0 +1,15 @@ +package de.danoeh.antennapod.core.tests; + +import android.test.InstrumentationTestRunner; +import android.test.suitebuilder.TestSuiteBuilder; +import junit.framework.TestSuite; + +public class AntennaPodTestRunner extends InstrumentationTestRunner { + + @Override + public TestSuite getAllTests() { + return new TestSuiteBuilder(AntennaPodTestRunner.class) + .includeAllPackagesUnderHere() + .build(); + } +}
\ No newline at end of file diff --git a/core/src/androidTest/java/de/danoeh/antennapod/core/util/DateUtilsTest.java b/core/src/androidTest/java/de/danoeh/antennapod/core/tests/util/DateUtilsTest.java index cca753895..2a2d6414a 100644 --- a/core/src/androidTest/java/de/danoeh/antennapod/core/util/DateUtilsTest.java +++ b/core/src/androidTest/java/de/danoeh/antennapod/core/tests/util/DateUtilsTest.java @@ -1,4 +1,4 @@ -package de.danoeh.antennapod.core.util; +package de.danoeh.antennapod.core.tests.util; import android.test.AndroidTestCase; @@ -7,6 +7,8 @@ import java.util.Date; import java.util.GregorianCalendar; import java.util.TimeZone; +import de.danoeh.antennapod.core.util.DateUtils; + public class DateUtilsTest extends AndroidTestCase { public void testParseDateWithMicroseconds() throws Exception { @@ -56,4 +58,20 @@ public class DateUtilsTest extends AndroidTestCase { assertEquals(900, actual.getTime()%1000); } + public void testParseDateWithTimezoneName() throws Exception { + GregorianCalendar exp = new GregorianCalendar(2015, 2, 28, 6, 31, 4); + exp.setTimeZone(TimeZone.getTimeZone("UTC")); + Date expected = new Date(exp.getTimeInMillis()); + Date actual = DateUtils.parse("Sat, 28 Mar 2015 01:31:04 EST"); + assertEquals(expected, actual); + } + + public void testParseDateWithTimeZoneOffset() throws Exception { + GregorianCalendar exp = new GregorianCalendar(2015, 2, 28, 12, 16, 12); + exp.setTimeZone(TimeZone.getTimeZone("UTC")); + Date expected = new Date(exp.getTimeInMillis()); + Date actual = DateUtils.parse("Sat, 28 March 2015 08:16:12 -0400"); + assertEquals(expected, actual); + } + } diff --git a/core/src/androidTest/java/de/danoeh/antennapod/core/tests/util/LongLongMapTest.java b/core/src/androidTest/java/de/danoeh/antennapod/core/tests/util/LongLongMapTest.java new file mode 100644 index 000000000..50c2a9c3c --- /dev/null +++ b/core/src/androidTest/java/de/danoeh/antennapod/core/tests/util/LongLongMapTest.java @@ -0,0 +1,61 @@ +package de.danoeh.antennapod.core.tests.util; + +import android.test.AndroidTestCase; + +import de.danoeh.antennapod.core.util.LongIntMap; + +public class LongLongMapTest extends AndroidTestCase { + + public void testEmptyMap() { + LongIntMap map = new LongIntMap(); + assertEquals(0, map.size()); + assertEquals("LongLongMap{}", map.toString()); + assertEquals(0, map.get(42)); + assertEquals(-1, map.get(42, -1)); + assertEquals(false, map.delete(42)); + assertEquals(-1, map.indexOfKey(42)); + assertEquals(-1, map.indexOfValue(42)); + assertEquals(1, map.hashCode()); + } + + public void testSingleElement() { + LongIntMap map = new LongIntMap(); + map.put(17, 42); + assertEquals(1, map.size()); + assertEquals("LongLongMap{17=42}", map.toString()); + assertEquals(42, map.get(17)); + assertEquals(42, map.get(17, -1)); + assertEquals(0, map.indexOfKey(17)); + assertEquals(0, map.indexOfValue(42)); + assertEquals(true, map.delete(17)); + } + + public void testAddAndDelete() { + LongIntMap map = new LongIntMap(); + for(int i=0; i < 100; i++) { + map.put(i * 17, i * 42); + } + assertEquals(100, map.size()); + assertEquals(0, map.get(0)); + assertEquals(42, map.get(17)); + assertEquals(42, map.get(17, -1)); + assertEquals(1, map.indexOfKey(17)); + assertEquals(1, map.indexOfValue(42)); + for(int i=0; i < 100; i++) { + assertEquals(true, map.delete(i * 17)); + } + } + + public void testOverwrite() { + LongIntMap map = new LongIntMap(); + map.put(17, 42); + assertEquals(1, map.size()); + assertEquals("LongLongMap{17=42}", map.toString()); + assertEquals(42, map.get(17)); + map.put(17, 23); + assertEquals(1, map.size()); + assertEquals("LongLongMap{17=23}", map.toString()); + assertEquals(23, map.get(17)); + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoProvider.java b/core/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoProvider.java index b6ece6dc8..4f2d5b204 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoProvider.java +++ b/core/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoProvider.java @@ -6,8 +6,12 @@ import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.media.MediaMetadataRetriever; import android.net.Uri; +import android.text.TextUtils; import android.util.Log; +import com.squareup.okhttp.Interceptor; +import com.squareup.okhttp.OkHttpClient; +import com.squareup.okhttp.Response; import com.squareup.picasso.Cache; import com.squareup.picasso.LruCache; import com.squareup.picasso.OkHttpDownloader; @@ -22,13 +26,18 @@ import org.apache.commons.lang3.StringUtils; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.net.HttpURLConnection; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import de.danoeh.antennapod.core.service.download.HttpDownloader; +import de.danoeh.antennapod.core.storage.DBReader; + /** * Provides access to Picasso instances. */ public class PicassoProvider { + private static final String TAG = "PicassoProvider"; private static final boolean DEBUG = false; @@ -56,10 +65,12 @@ public class PicassoProvider { if (picassoSetup) { return; } + OkHttpClient client = new OkHttpClient(); + client.interceptors().add(new BasicAuthenticationInterceptor(appContext)); Picasso picasso = new Picasso.Builder(appContext) .indicatorsEnabled(DEBUG) .loggingEnabled(DEBUG) - .downloader(new OkHttpDownloader(appContext)) + .downloader(new OkHttpDownloader(client)) .addRequestHandler(new MediaRequestHandler(appContext)) .executor(getExecutorService()) .memoryCache(getMemoryCache(appContext)) @@ -75,6 +86,48 @@ public class PicassoProvider { picassoSetup = true; } + private static class BasicAuthenticationInterceptor implements Interceptor { + + private final Context context; + + public BasicAuthenticationInterceptor(Context context) { + this.context = context; + } + + @Override + public Response intercept(Chain chain) throws IOException { + com.squareup.okhttp.Request request = chain.request(); + String url = request.urlString(); + String authentication = DBReader.getImageAuthentication(context, url); + + if(TextUtils.isEmpty(authentication)) { + Log.d(TAG, "no credentials for '" + url + "'"); + return chain.proceed(request); + } + + // add authentication + String[] auth = authentication.split(":"); + String credentials = HttpDownloader.encodeCredentials(auth[0], auth[1], "ISO-8859-1"); + com.squareup.okhttp.Request newRequest = request + .newBuilder() + .addHeader("Authorization", credentials) + .build(); + Log.d(TAG, "Basic authentication with ISO-8859-1 encoding"); + Response response = chain.proceed(newRequest); + if (!response.isSuccessful() && response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) { + credentials = HttpDownloader.encodeCredentials(auth[0], auth[1], "UTF-8"); + newRequest = request + .newBuilder() + .addHeader("Authorization", credentials) + .build(); + Log.d(TAG, "Basic authentication with UTF-8 encoding"); + return chain.proceed(newRequest); + } else { + return response; + } + } + } + private static class MediaRequestHandler extends RequestHandler { final Context context; @@ -90,7 +143,7 @@ public class PicassoProvider { } @Override - public Result load(Request data) throws IOException { + public Result load(Request data, int networkPolicy) throws IOException { Bitmap bitmap = null; MediaMetadataRetriever mmr = null; try { @@ -109,13 +162,7 @@ public class PicassoProvider { } if (bitmap == null) { - // check for fallback Uri - String fallbackParam = data.uri.getQueryParameter(PicassoImageResource.PARAM_FALLBACK); - - if (fallbackParam != null) { - Uri fallback = Uri.parse(fallbackParam); - bitmap = decodeStreamFromFile(data, fallback); - } + Log.wtf(TAG, "THIS SHOULD NEVER EVER HAPPEN!!"); } return new Result(bitmap, Picasso.LoadedFrom.DISK); 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 c0f71ed55..29ba721fe 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 @@ -2,6 +2,7 @@ package de.danoeh.antennapod.core.feed; import android.content.Context; import android.net.Uri; +import android.support.annotation.Nullable; import org.apache.commons.lang3.StringUtils; @@ -10,9 +11,7 @@ import java.util.Date; import java.util.List; import de.danoeh.antennapod.core.asynctask.PicassoImageResource; -import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.storage.DBWriter; -import de.danoeh.antennapod.core.util.EpisodeFilter; import de.danoeh.antennapod.core.util.flattr.FlattrStatus; import de.danoeh.antennapod.core.util.flattr.FlattrThing; @@ -81,12 +80,20 @@ public class Feed extends FeedFile implements FlattrThing, PicassoImageResource */ private String nextPageLink; + private boolean lastUpdateFailed; + + /** + * Contains property strings. If such a property applies to a feed item, it is not shown in the feed list + */ + private FeedItemFilter itemfilter; + /** * This constructor is used for restoring a feed from the database. */ public Feed(long id, Date 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, FlattrStatus status, boolean paged, String nextPageLink) { + String downloadUrl, boolean downloaded, FlattrStatus status, boolean paged, String nextPageLink, + String filter, boolean lastUpdateFailed) { super(fileUrl, downloadUrl, downloaded); this.id = id; this.title = title; @@ -106,8 +113,13 @@ public class Feed extends FeedFile implements FlattrThing, PicassoImageResource this.flattrStatus = status; this.paged = paged; this.nextPageLink = nextPageLink; - - items = new ArrayList<FeedItem>(); + this.items = new ArrayList<FeedItem>(); + if(filter != null) { + this.itemfilter = new FeedItemFilter(filter); + } else { + this.itemfilter = new FeedItemFilter(new String[0]); + } + this.lastUpdateFailed = lastUpdateFailed; } /** @@ -117,7 +129,7 @@ public class Feed extends FeedFile implements FlattrThing, PicassoImageResource 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, - fileUrl, downloadUrl, downloaded, new FlattrStatus(), false, null); + fileUrl, downloadUrl, downloaded, new FlattrStatus(), false, null, null, false); } /** @@ -125,7 +137,6 @@ public class Feed extends FeedFile implements FlattrThing, PicassoImageResource */ public Feed() { super(); - items = new ArrayList<FeedItem>(); lastUpdate = new Date(); this.flattrStatus = new FlattrStatus(); } @@ -159,53 +170,15 @@ public class Feed extends FeedFile implements FlattrThing, PicassoImageResource preferences = new FeedPreferences(0, true, username, password); } - /** - * Returns the number of FeedItems where 'read' is false. If the 'display - * only episodes' - preference is set to true, this method will only count - * items with episodes. - */ - public int getNumOfNewItems() { - int count = 0; - for (FeedItem item : items) { - if (item.getState() == FeedItem.State.NEW) { - if (!UserPreferences.isDisplayOnlyEpisodes() - || item.getMedia() != null) { - count++; - } - } - } - return count; - } - - /** - * Returns the number of FeedItems where the media started to play but - * wasn't finished yet. - */ - public int getNumOfStartedItems() { - int count = 0; - - for (FeedItem item : items) { - FeedItem.State state = item.getState(); - if (state == FeedItem.State.IN_PROGRESS - || state == FeedItem.State.PLAYING) { - count++; - } - } - return count; - } /** * Returns true if at least one item in the itemlist is unread. * - * @param enableEpisodeFilter true if this method should only count items with episodes if - * the 'display only episodes' - preference is set to true by the - * user. */ - public boolean hasNewItems(boolean enableEpisodeFilter) { + public boolean hasNewItems() { for (FeedItem item : items) { - if (item.getState() == FeedItem.State.NEW) { - if (!(enableEpisodeFilter && UserPreferences - .isDisplayOnlyEpisodes()) || item.getMedia() != null) { + if (item.getState() == FeedItem.State.UNREAD) { + if (item.getMedia() != null) { return true; } } @@ -216,30 +189,17 @@ public class Feed extends FeedFile implements FlattrThing, PicassoImageResource /** * Returns the number of FeedItems. * - * @param enableEpisodeFilter true if this method should only count items with episodes if - * the 'display only episodes' - preference is set to true by the - * user. */ - public int getNumOfItems(boolean enableEpisodeFilter) { - if (enableEpisodeFilter && UserPreferences.isDisplayOnlyEpisodes()) { - return EpisodeFilter.countItemsWithEpisodes(items); - } else { - return items.size(); - } + public int getNumOfItems() { + return items.size(); } /** * Returns the item at the specified index. * - * @param enableEpisodeFilter true if this method should ignore items without episdodes if - * the episodes filter has been enabled by the user. */ - public FeedItem getItemAtIndex(boolean enableEpisodeFilter, int position) { - if (enableEpisodeFilter && UserPreferences.isDisplayOnlyEpisodes()) { - return EpisodeFilter.accessEpisodeByIndex(items, position); - } else { - return items.get(position); - } + public FeedItem getItemAtIndex(int position) { + return items.get(position); } /** @@ -350,6 +310,19 @@ public class Feed extends FeedFile implements FlattrThing, PicassoImageResource return false; } + public FeedItem getMostRecentItem() { + // we could sort, but we don't need to, a simple search is fine... + Date mostRecentDate = new Date(0); + FeedItem mostRecentItem = null; + for (FeedItem item : items) { + if (item.getPubDate().after(mostRecentDate)) { + mostRecentDate = item.getPubDate(); + mostRecentItem = item; + } + } + return mostRecentItem; + } + @Override public int getTypeAsInt() { return FEEDFILETYPE_FEED; @@ -503,4 +476,24 @@ public class Feed extends FeedFile implements FlattrThing, PicassoImageResource public void setNextPageLink(String nextPageLink) { this.nextPageLink = nextPageLink; } + + @Nullable + public FeedItemFilter getItemFilter() { + return itemfilter; + } + + public void setHiddenItemProperties(String[] properties) { + if (properties != null) { + this.itemfilter = new FeedItemFilter(properties); + } + } + + public boolean hasLastUpdateFailed() { + return this.lastUpdateFailed; + } + + public void setLastUpdateFailed(boolean lastUpdateFailed) { + this.lastUpdateFailed = lastUpdateFailed; + } + } diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedEvent.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedEvent.java new file mode 100644 index 000000000..d04d236e4 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedEvent.java @@ -0,0 +1,28 @@ +package de.danoeh.antennapod.core.feed; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +public class FeedEvent { + + public enum Action { + FILTER_CHANGED + } + + public final Action action; + public final long feedId; + + public FeedEvent(Action action, long feedId) { + this.action = action; + this.feedId = feedId; + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) + .append("action", action) + .append("feedId", feedId) + .toString(); + } + +} 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 4fd7a184c..11348953e 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 @@ -63,6 +63,8 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr private List<Chapter> chapters; private FeedImage image; + private boolean autoDownload = true; + public FeedItem() { this.read = true; this.flattrStatus = new FlattrStatus(); @@ -74,7 +76,7 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr * */ public FeedItem(long id, String title, String link, Date pubDate, String paymentLink, long feedId, FlattrStatus flattrStatus, boolean hasChapters, FeedImage image, boolean read, - String itemIdentifier) { + String itemIdentifier, boolean autoDownload) { this.id = id; this.title = title; this.link = link; @@ -86,6 +88,7 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr this.image = image; this.read = read; this.itemIdentifier = itemIdentifier; + this.autoDownload = autoDownload; } /** @@ -236,7 +239,7 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr } public boolean isRead() { - return read || isInProgress(); + return read; } public void setRead(boolean read) { @@ -315,10 +318,10 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr @Override public Uri getImageUri() { - if (hasItemImageDownloaded()) { - return image.getImageUri(); - } else if (hasMedia()) { + if(media != null && media.hasEmbeddedPicture()) { return media.getImageUri(); + } else if (hasItemImageDownloaded()) { + return image.getImageUri(); } else if (feed != null) { return feed.getImageUri(); } else { @@ -327,7 +330,7 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr } public enum State { - NEW, IN_PROGRESS, READ, PLAYING + UNREAD, IN_PROGRESS, READ, PLAYING } public State getState() { @@ -339,7 +342,7 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr return State.IN_PROGRESS; } } - return (isRead() ? State.READ : State.NEW); + return (isRead() ? State.READ : State.UNREAD); } public long getFeedId() { @@ -388,6 +391,22 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr return hasChapters; } + public void setAutoDownload(boolean autoDownload) { + this.autoDownload = autoDownload; + } + + public boolean getAutoDownload() { + return this.autoDownload; + } + + public boolean isAutoDownloadable() { + return this.hasMedia() && + false == this.getMedia().isPlaying() && + false == this.getMedia().isDownloaded() && + false == this.isRead() && + this.getAutoDownload(); + } + @Override public String toString() { return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); 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 new file mode 100644 index 000000000..4ad084b39 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilter.java @@ -0,0 +1,82 @@ +package de.danoeh.antennapod.core.feed; + +import android.content.Context; + +import org.apache.commons.lang3.StringUtils; + +import java.util.ArrayList; +import java.util.List; + +import de.danoeh.antennapod.core.storage.DBReader; + +public class FeedItemFilter { + + private final String[] properties; + + private boolean hideUnplayed = false; + private boolean hidePaused = false; + private boolean hidePlayed = false; + private boolean hideQueued = false; + private boolean hideNotQueued = false; + private boolean hideDownloaded = false; + private boolean hideNotDownloaded = false; + + public FeedItemFilter(String properties) { + this(StringUtils.split(properties, ',')); + } + + public FeedItemFilter(String[] properties) { + this.properties = properties; + for(String property : properties) { + // see R.arrays.feed_filter_values + switch(property) { + case "unplayed": + hideUnplayed = true; + break; + case "paused": + hidePaused = true; + break; + case "played": + hidePlayed = true; + break; + case "queued": + hideQueued = true; + break; + case "not_queued": + hideNotQueued = true; + break; + case "downloaded": + hideDownloaded = true; + break; + case "not_downloaded": + hideNotDownloaded = true; + break; + } + } + } + + public List<FeedItem> filter(Context context, List<FeedItem> items) { + if(properties.length == 0) { + return items; + } + List<FeedItem> result = new ArrayList<FeedItem>(); + for(FeedItem item : items) { + if(hideUnplayed && false == item.isRead()) continue; + if(hidePaused && item.getState() == FeedItem.State.IN_PROGRESS) continue; + if(hidePlayed && item.isRead()) continue; + boolean isQueued = DBReader.getQueueIDList(context).contains(item.getId()); + if(hideQueued && isQueued) continue; + if(hideNotQueued && false == isQueued) continue; + boolean isDownloaded = item.getMedia() != null && item.getMedia().isDownloaded(); + if(hideDownloaded && isDownloaded) continue; + if(hideNotDownloaded && false == isDownloaded) continue; + result.add(item); + } + return result; + } + + public String[] getValues() { + return properties.clone(); + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java index 93f826894..f875eb812 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java @@ -2,6 +2,7 @@ package de.danoeh.antennapod.core.feed; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; +import android.media.MediaMetadataRetriever; import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; @@ -34,6 +35,7 @@ public class FeedMedia extends FeedFile implements Playable { private String mime_type; private volatile FeedItem item; private Date playbackCompletionDate; + private boolean hasEmbeddedPicture; /* Used for loading item when restoring from parcel. */ private long itemID; @@ -50,6 +52,7 @@ public class FeedMedia extends FeedFile implements Playable { long size, String mime_type, String file_url, String download_url, boolean downloaded, Date playbackCompletionDate, int played_duration) { super(file_url, download_url, downloaded); + checkEmbeddedPicture(); this.id = id; this.item = item; this.duration = duration; @@ -61,12 +64,6 @@ public class FeedMedia extends FeedFile implements Playable { ? null : (Date) playbackCompletionDate.clone(); } - public FeedMedia(long id, FeedItem item) { - super(); - this.id = id; - this.item = item; - } - @Override public String getHumanReadableIdentifier() { if (item != null && item.getTitle() != null) { @@ -227,18 +224,15 @@ public class FeedMedia extends FeedFile implements Playable { return (this.position > 0); } - public FeedImage getImage() { - if (item != null) { - return (item.hasItemImageDownloaded()) ? item.getImage() : item.getFeed().getImage(); - } - return null; - } - @Override public int describeContents() { return 0; } + public boolean hasEmbeddedPicture() { + return this.hasEmbeddedPicture; + } + @Override public void writeToParcel(Parcel dest, int flags) { dest.writeLong(id); @@ -415,28 +409,45 @@ public class FeedMedia extends FeedFile implements Playable { @Override public Uri getImageUri() { - final Uri feedImgUri = getFeedImageUri(); - - if (localFileAvailable()) { + if (hasEmbeddedPicture) { Uri.Builder builder = new Uri.Builder(); - builder.scheme(SCHEME_MEDIA) - .encodedPath(getLocalMediaUrl()); - if (feedImgUri != null) { - builder.appendQueryParameter(PARAM_FALLBACK, feedImgUri.toString()); - } + builder.scheme(SCHEME_MEDIA).encodedPath(getLocalMediaUrl()); return builder.build(); - } else if (item.hasItemImageDownloaded()) { - return item.getImage().getImageUri(); } else { - return feedImgUri; + return item.getImageUri(); } } - private Uri getFeedImageUri() { - if (item != null && item.getFeed() != null) { - return item.getFeed().getImageUri(); - } else { - return null; + @Override + public void setDownloaded(boolean downloaded) { + super.setDownloaded(downloaded); + checkEmbeddedPicture(); + } + + @Override + public void setFile_url(String file_url) { + super.setFile_url(file_url); + checkEmbeddedPicture(); + } + + private void checkEmbeddedPicture() { + if (!localFileAvailable()) { + hasEmbeddedPicture = false; + return; + } + MediaMetadataRetriever mmr = new MediaMetadataRetriever(); + try { + mmr.setDataSource(getLocalMediaUrl()); + byte[] image = mmr.getEmbeddedPicture(); + if(image != null) { + hasEmbeddedPicture = true; + } + else { + hasEmbeddedPicture = false; + } + } catch (Exception e) { + e.printStackTrace(); + hasEmbeddedPicture = false; } } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/QueueEvent.java b/core/src/main/java/de/danoeh/antennapod/core/feed/QueueEvent.java index 9f1eec754..c8497f509 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/QueueEvent.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/QueueEvent.java @@ -8,7 +8,7 @@ import java.util.List; public class QueueEvent { public enum Action { - ADDED, ADDED_ITEMS, REMOVED, CLEARED, DELETED_MEDIA, SORTED + ADDED, ADDED_ITEMS, REMOVED, CLEARED, DELETED_MEDIA, SORTED, MOVED } public final Action action; 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 db242c3bc..1a40120e2 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 @@ -75,84 +75,9 @@ public class GpodnetService { public GpodnetService() { httpClient = AntennapodHttpClient.getHttpClient(); - if (Build.VERSION.SDK_INT <= 10) { - Log.d(TAG, "Use custom SSL factory"); - SSLSocketFactory factory = getCustomSslSocketFactory(); - httpClient.setSslSocketFactory(factory); - } BASE_HOST = GpodnetPreferences.getHostname(); } - private synchronized static SSLSocketFactory getCustomSslSocketFactory() { - try { - TrustManagerFactory defaultTrustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - defaultTrustManagerFactory.init((KeyStore) null); // use system keystore - final X509TrustManager defaultTrustManager = (X509TrustManager) defaultTrustManagerFactory.getTrustManagers()[0]; - TrustManager[] customTrustManagers = new TrustManager[]{new X509TrustManager() { - @Override - public java.security.cert.X509Certificate[] getAcceptedIssuers() { - return null; - } - @Override - public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException { - } - @Override - public void checkServerTrusted(X509Certificate[] chain, String authType) - throws CertificateException { - // chain may out of order - construct data structures to walk from server certificate to root certificate - Map<Principal, X509Certificate> certificates = new HashMap<Principal, X509Certificate>(chain.length - 1); - X509Certificate subject = null; - for (X509Certificate cert : chain) { - cert.checkValidity(); - if (cert.getSubjectDN().toString().startsWith("CN=" + DEFAULT_BASE_HOST)) { - subject = cert; - } else { - certificates.put(cert.getSubjectDN(), cert); - } - } - if (subject == null) { - throw new CertificateException("Chain does not contain a certificate for " + DEFAULT_BASE_HOST); - } - // follow chain to root CA - while (certificates.get(subject.getIssuerDN()) != null) { - subject.checkValidity(); - X509Certificate issuer = certificates.get(subject.getIssuerDN()); - try { - subject.verify(issuer.getPublicKey()); - } catch (Exception e) { - Log.d(TAG, "failed: " + issuer.getSubjectDN() + " -> " + subject.getSubjectDN()); - throw new CertificateException("Could not verify certificate"); - } - subject = issuer; - } - X500Principal rootAuthority = subject.getIssuerX500Principal(); - boolean accepted = false; - for (X509Certificate cert : - defaultTrustManager.getAcceptedIssuers()) { - if (cert.getSubjectX500Principal().equals(rootAuthority)) { - try { - subject.verify(cert.getPublicKey()); - accepted = true; - } catch (Exception e) { - Log.d(TAG, "failed: " + cert.getSubjectDN() + " -> " + subject.getSubjectDN()); - throw new CertificateException("Could not verify root certificate"); - } - } - } - if (accepted == false) { - throw new CertificateException("Could not verify root certificate"); - } - } - }}; - SSLContext sslContext = SSLContext.getInstance("TLS"); - sslContext.init(null, customTrustManagers, null); - return sslContext.getSocketFactory(); - } catch (Exception e) { - e.printStackTrace(); - } - return null; - } - /** * Returns the [count] most used tags. */ @@ -726,7 +651,12 @@ public class GpodnetService { Validate.notNull(body); ByteArrayOutputStream outputStream; - int contentLength = (int) body.contentLength(); + int contentLength = 0; + try { + contentLength = (int) body.contentLength(); + } catch (IOException ignore) { + // ignore + } if (contentLength > 0) { outputStream = new ByteArrayOutputStream(contentLength); } else { diff --git a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetEpisodeAction.java b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetEpisodeAction.java index 0c431d60b..bd6210d13 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetEpisodeAction.java +++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetEpisodeAction.java @@ -258,7 +258,7 @@ public class GpodnetEpisodeAction { private int total = -1; public Builder(FeedItem item, Action action) { - this(item.getFeed().getDownload_url(), item.getItemIdentifier(), action); + this(item.getFeed().getDownload_url(), item.getMedia().getDownload_url(), action); } public Builder(String podcast, String episode, Action action) { diff --git a/core/src/main/java/de/danoeh/antennapod/core/preferences/GpodnetPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/preferences/GpodnetPreferences.java index cfdd0c5d6..c3c6ce8c5 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/preferences/GpodnetPreferences.java +++ b/core/src/main/java/de/danoeh/antennapod/core/preferences/GpodnetPreferences.java @@ -173,7 +173,7 @@ public class GpodnetPreferences { writePreference(PREF_SYNC_REMOVED, removedFeeds); } feedListLock.unlock(); - GpodnetSyncService.sendSyncIntent(ClientConfig.applicationCallbacks.getApplicationInstance()); + GpodnetSyncService.sendSyncSubscriptionsIntent(ClientConfig.applicationCallbacks.getApplicationInstance()); } public static void addRemovedFeed(String feed) { @@ -186,7 +186,7 @@ public class GpodnetPreferences { writePreference(PREF_SYNC_ADDED, addedFeeds); } feedListLock.unlock(); - GpodnetSyncService.sendSyncIntent(ClientConfig.applicationCallbacks.getApplicationInstance()); + GpodnetSyncService.sendSyncSubscriptionsIntent(ClientConfig.applicationCallbacks.getApplicationInstance()); } public static Set<String> getAddedFeedsCopy() { @@ -225,6 +225,7 @@ public class GpodnetPreferences { ensurePreferencesLoaded(); queuedEpisodeActions.add(action); writePreference(PREF_SYNC_EPISODE_ACTIONS, writeEpisodeActionsToString(queuedEpisodeActions)); + GpodnetSyncService.sendSyncActionsIntent(ClientConfig.applicationCallbacks.getApplicationInstance()); } public static List<GpodnetEpisodeAction> getQueuedEpisodeActions() { 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 022c03ca7..594241573 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 @@ -16,11 +16,12 @@ import org.json.JSONException; import java.io.File; import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; import java.util.LinkedList; import java.util.List; import java.util.concurrent.TimeUnit; -import de.danoeh.antennapod.core.BuildConfig; import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.R; import de.danoeh.antennapod.core.receiver.FeedUpdateReceiver; @@ -33,35 +34,52 @@ import de.danoeh.antennapod.core.receiver.FeedUpdateReceiver; */ public class UserPreferences implements SharedPreferences.OnSharedPreferenceChangeListener { + public static final String IMPORT_DIR = "import/"; + private static final String TAG = "UserPreferences"; + // User Infercasce + public static final String PREF_THEME = "prefTheme"; + public static final String PREF_HIDDEN_DRAWER_ITEMS = "prefHiddenDrawerItems"; + public static final String PREF_EXPANDED_NOTIFICATION = "prefExpandNotify"; + public static final String PREF_PERSISTENT_NOTIFICATION = "prefPersistNotify"; + + // Queue + public static final String PREF_QUEUE_ADD_TO_FRONT = "prefQueueAddToFront"; + + // Playback public static final String PREF_PAUSE_ON_HEADSET_DISCONNECT = "prefPauseOnHeadsetDisconnect"; public static final String PREF_UNPAUSE_ON_HEADSET_RECONNECT = "prefUnpauseOnHeadsetReconnect"; public static final String PREF_FOLLOW_QUEUE = "prefFollowQueue"; - public static final String PREF_DOWNLOAD_MEDIA_ON_WIFI_ONLY = "prefDownloadMediaOnWifiOnly"; - public static final String PREF_UPDATE_INTERVAL = "prefAutoUpdateIntervall"; - public static final String PREF_PARALLEL_DOWNLOADS = "prefParallelDownloads"; - public static final String PREF_MOBILE_UPDATE = "prefMobileUpdate"; - public static final String PREF_DISPLAY_ONLY_EPISODES = "prefDisplayOnlyEpisodes"; public static final String PREF_AUTO_DELETE = "prefAutoDelete"; public static final String PREF_SMART_MARK_AS_PLAYED_SECS = "prefSmartMarkAsPlayedSecs"; + private static final String PREF_PLAYBACK_SPEED_ARRAY = "prefPlaybackSpeedArray"; + public static final String PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS = "prefPauseForFocusLoss"; + public static final String PREF_RESUME_AFTER_CALL = "prefResumeAfterCall"; + + // Network + public static final String PREF_UPDATE_INTERVAL = "prefAutoUpdateIntervall"; + public static final String PREF_MOBILE_UPDATE = "prefMobileUpdate"; + public static final String PREF_PARALLEL_DOWNLOADS = "prefParallelDownloads"; + public static final String PREF_EPISODE_CACHE_SIZE = "prefEpisodeCacheSize"; + public static final String PREF_ENABLE_AUTODL = "prefEnableAutoDl"; + public static final String PREF_ENABLE_AUTODL_ON_BATTERY = "prefEnableAutoDownloadOnBattery"; + public static final String PREF_ENABLE_AUTODL_WIFI_FILTER = "prefEnableAutoDownloadWifiFilter"; + public static final String PREF_AUTODL_SELECTED_NETWORKS = "prefAutodownloadSelectedNetworks"; + + // Services public static final String PREF_AUTO_FLATTR = "pref_auto_flattr"; public static final String PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD = "prefAutoFlattrPlayedDurationThreshold"; - public static final String PREF_THEME = "prefTheme"; + + // Other public static final String PREF_DATA_FOLDER = "prefDataFolder"; - public static final String PREF_ENABLE_AUTODL = "prefEnableAutoDl"; - public static final String PREF_ENABLE_AUTODL_WIFI_FILTER = "prefEnableAutoDownloadWifiFilter"; - public static final String PREF_ENABLE_AUTODL_ON_BATTERY = "prefEnableAutoDownloadOnBattery"; - private static final String PREF_AUTODL_SELECTED_NETWORKS = "prefAutodownloadSelectedNetworks"; - public static final String PREF_EPISODE_CACHE_SIZE = "prefEpisodeCacheSize"; - private static final String PREF_PLAYBACK_SPEED = "prefPlaybackSpeed"; - private static final String PREF_PLAYBACK_SPEED_ARRAY = "prefPlaybackSpeedArray"; - public static final String PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS = "prefPauseForFocusLoss"; - private static final String PREF_SEEK_DELTA_SECS = "prefSeekDeltaSecs"; - private static final String PREF_EXPANDED_NOTIFICATION = "prefExpandNotify"; - private static final String PREF_PERSISTENT_NOTIFICATION = "prefPersistNotify"; - public static final String PREF_QUEUE_ADD_TO_FRONT = "prefQueueAddToFront"; + + // Mediaplayer + public static final String PREF_PLAYBACK_SPEED = "prefPlaybackSpeed"; + private static final String PREF_FAST_FORWARD_SECS = "prefFastForwardSecs"; + private static final String PREF_REWIND_SECS = "prefRewindSecs"; + public static final String PREF_QUEUE_LOCKED = "prefQueueLocked"; // TODO: Make this value configurable private static final float PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD_DEFAULT = 0.8f; @@ -71,32 +89,45 @@ public class UserPreferences implements private static UserPreferences instance; private final Context context; - // Preferences + // User Interface + private int theme; + private List<String> hiddenDrawerItems; + private int notifyPriority; + private boolean persistNotify; + + // Queue + private boolean enqueueAtFront; + + // Playback private boolean pauseOnHeadsetDisconnect; private boolean unpauseOnHeadsetReconnect; private boolean followQueue; - private boolean downloadMediaOnWifiOnly; - private long updateInterval; - private boolean allowMobileUpdate; - private boolean displayOnlyEpisodes; private boolean autoDelete; private int smartMarkAsPlayedSecs; - private boolean autoFlattr; - private float autoFlattrPlayedDurationThreshold; - private int theme; + private String[] playbackSpeedArray; + private boolean pauseForFocusLoss; + private boolean resumeAfterCall; + + // Network + private long updateInterval; + private boolean allowMobileUpdate; + private int parallelDownloads; + private int episodeCacheSize; private boolean enableAutodownload; - private boolean enableAutodownloadWifiFilter; private boolean enableAutodownloadOnBattery; + private boolean enableAutodownloadWifiFilter; private String[] autodownloadSelectedNetworks; - private int parallelDownloads; - private int episodeCacheSize; + + // Services + private boolean autoFlattr; + private float autoFlattrPlayedDurationThreshold; + + // Settings somewhere in the GUI private String playbackSpeed; - private String[] playbackSpeedArray; - private boolean pauseForFocusLoss; - private int seekDeltaSecs; - private boolean isFreshInstall; - private int notifyPriority; - private boolean persistNotify; + private int fastForwardSecs; + private int rewindSecs; + private boolean queueLocked; + private UserPreferences(Context context) { this.context = context; @@ -109,8 +140,7 @@ public class UserPreferences implements * @throws IllegalArgumentException if context is null */ public static void createInstance(Context context) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Creating new instance of UserPreferences"); + Log.d(TAG, "Creating new instance of UserPreferences"); Validate.notNull(context); instance = new UserPreferences(context); @@ -123,48 +153,54 @@ public class UserPreferences implements } private void loadPreferences() { - SharedPreferences sp = PreferenceManager - .getDefaultSharedPreferences(context); - EPISODE_CACHE_SIZE_UNLIMITED = context.getResources().getInteger( - R.integer.episode_cache_size_unlimited); - pauseOnHeadsetDisconnect = sp.getBoolean( - PREF_PAUSE_ON_HEADSET_DISCONNECT, true); - unpauseOnHeadsetReconnect = sp.getBoolean( - PREF_UNPAUSE_ON_HEADSET_RECONNECT, true); + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); + + // User Interface + theme = readThemeValue(sp.getString(PREF_THEME, "0")); + if (sp.getBoolean(PREF_EXPANDED_NOTIFICATION, false)) { + notifyPriority = NotificationCompat.PRIORITY_MAX; + } else { + notifyPriority = NotificationCompat.PRIORITY_DEFAULT; + } + hiddenDrawerItems = Arrays.asList(StringUtils.split(sp.getString(PREF_HIDDEN_DRAWER_ITEMS, ""), ',')); + persistNotify = sp.getBoolean(PREF_PERSISTENT_NOTIFICATION, false); + + // Queue + enqueueAtFront = sp.getBoolean(PREF_QUEUE_ADD_TO_FRONT, false); + + // Playback + pauseOnHeadsetDisconnect = sp.getBoolean(PREF_PAUSE_ON_HEADSET_DISCONNECT, true); + unpauseOnHeadsetReconnect = sp.getBoolean(PREF_UNPAUSE_ON_HEADSET_RECONNECT, true); followQueue = sp.getBoolean(PREF_FOLLOW_QUEUE, false); - downloadMediaOnWifiOnly = sp.getBoolean( - PREF_DOWNLOAD_MEDIA_ON_WIFI_ONLY, true); - updateInterval = readUpdateInterval(sp.getString(PREF_UPDATE_INTERVAL, - "0")); - allowMobileUpdate = sp.getBoolean(PREF_MOBILE_UPDATE, false); - displayOnlyEpisodes = sp.getBoolean(PREF_DISPLAY_ONLY_EPISODES, false); autoDelete = sp.getBoolean(PREF_AUTO_DELETE, false); smartMarkAsPlayedSecs = Integer.valueOf(sp.getString(PREF_SMART_MARK_AS_PLAYED_SECS, "30")); - autoFlattr = sp.getBoolean(PREF_AUTO_FLATTR, false); - autoFlattrPlayedDurationThreshold = sp.getFloat(PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD, - PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD_DEFAULT); - theme = readThemeValue(sp.getString(PREF_THEME, "0")); - enableAutodownloadWifiFilter = sp.getBoolean( - PREF_ENABLE_AUTODL_WIFI_FILTER, false); - autodownloadSelectedNetworks = StringUtils.split( - sp.getString(PREF_AUTODL_SELECTED_NETWORKS, ""), ','); + playbackSpeedArray = readPlaybackSpeedArray(sp.getString( + PREF_PLAYBACK_SPEED_ARRAY, null)); + pauseForFocusLoss = sp.getBoolean(PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS, false); + + // Network + updateInterval = readUpdateInterval(sp.getString(PREF_UPDATE_INTERVAL, "0")); + allowMobileUpdate = sp.getBoolean(PREF_MOBILE_UPDATE, false); parallelDownloads = Integer.valueOf(sp.getString(PREF_PARALLEL_DOWNLOADS, "6")); - episodeCacheSize = readEpisodeCacheSizeInternal(sp.getString( - PREF_EPISODE_CACHE_SIZE, "20")); + EPISODE_CACHE_SIZE_UNLIMITED = context.getResources().getInteger( + R.integer.episode_cache_size_unlimited); + episodeCacheSize = readEpisodeCacheSizeInternal(sp.getString(PREF_EPISODE_CACHE_SIZE, "20")); enableAutodownload = sp.getBoolean(PREF_ENABLE_AUTODL, false); enableAutodownloadOnBattery = sp.getBoolean(PREF_ENABLE_AUTODL_ON_BATTERY, true); + enableAutodownloadWifiFilter = sp.getBoolean(PREF_ENABLE_AUTODL_WIFI_FILTER, false); + autodownloadSelectedNetworks = StringUtils.split( + sp.getString(PREF_AUTODL_SELECTED_NETWORKS, ""), ','); + + // Services + autoFlattr = sp.getBoolean(PREF_AUTO_FLATTR, false); + autoFlattrPlayedDurationThreshold = sp.getFloat(PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD, + PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD_DEFAULT); + + // MediaPlayer playbackSpeed = sp.getString(PREF_PLAYBACK_SPEED, "1.0"); - playbackSpeedArray = readPlaybackSpeedArray(sp.getString( - PREF_PLAYBACK_SPEED_ARRAY, null)); - pauseForFocusLoss = sp.getBoolean(PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS, false); - seekDeltaSecs = Integer.valueOf(sp.getString(PREF_SEEK_DELTA_SECS, "30")); - if (sp.getBoolean(PREF_EXPANDED_NOTIFICATION, false)) { - notifyPriority = NotificationCompat.PRIORITY_MAX; - } - else { - notifyPriority = NotificationCompat.PRIORITY_DEFAULT; - } - persistNotify = sp.getBoolean(PREF_PERSISTENT_NOTIFICATION, false); + fastForwardSecs = sp.getInt(PREF_FAST_FORWARD_SECS, 30); + rewindSecs = sp.getInt(PREF_REWIND_SECS, 30); + queueLocked = sp.getBoolean(PREF_QUEUE_LOCKED, false); } private int readThemeValue(String valueFromPrefs) { @@ -214,8 +250,7 @@ public class UserPreferences implements selectedSpeeds[i] = jsonArray.getString(i); } } catch (JSONException e) { - Log.e(TAG, - "Got JSON error when trying to get speeds from JSONArray"); + Log.e(TAG, "Got JSON error when trying to get speeds from JSONArray"); e.printStackTrace(); } } @@ -224,45 +259,78 @@ public class UserPreferences implements private static void instanceAvailable() { if (instance == null) { - throw new IllegalStateException( - "UserPreferences was used before being set up"); + throw new IllegalStateException("UserPreferences was used before being set up"); } } - public static boolean isPauseOnHeadsetDisconnect() { + /** + * Returns theme as R.style value + * + * @return R.style.Theme_AntennaPod_Light or R.style.Theme_AntennaPod_Dark + */ + public static int getTheme() { instanceAvailable(); - return instance.pauseOnHeadsetDisconnect; + return instance.theme; } - public static boolean isUnpauseOnHeadsetReconnect() { + public static int getNoTitleTheme() { + int theme = getTheme(); + if (theme == R.style.Theme_AntennaPod_Dark) { + return R.style.Theme_AntennaPod_Dark_NoTitle; + } else { + return R.style.Theme_AntennaPod_Light_NoTitle; + } + } + + public static List<String> getHiddenDrawerItems() { instanceAvailable(); - return instance.unpauseOnHeadsetReconnect; + return new ArrayList<String>(instance.hiddenDrawerItems); } - public static boolean isFollowQueue() { + /** + * Returns notification priority. + * + * @return NotificationCompat.PRIORITY_MAX or NotificationCompat.PRIORITY_DEFAULT + */ + public static int getNotifyPriority() { instanceAvailable(); - return instance.followQueue; + return instance.notifyPriority; + } + + /** + * Returns true if notifications are persistent + * + * @return {@code true} if notifications are persistent, {@code false} otherwise + */ + public static boolean isPersistNotify() { + instanceAvailable(); + return instance.persistNotify; } - public static boolean isDownloadMediaOnWifiOnly() { + /** + * 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 enqueueAtFront() { instanceAvailable(); - return instance.downloadMediaOnWifiOnly; + return instance.enqueueAtFront; } - public static long getUpdateInterval() { + public static boolean isPauseOnHeadsetDisconnect() { instanceAvailable(); - return instance.updateInterval; + return instance.pauseOnHeadsetDisconnect; } - public static boolean isAllowMobileUpdate() { + public static boolean isUnpauseOnHeadsetReconnect() { instanceAvailable(); - return instance.allowMobileUpdate; + return instance.unpauseOnHeadsetReconnect; } - public static boolean isDisplayOnlyEpisodes() { + + public static boolean isFollowQueue() { instanceAvailable(); - //return instance.displayOnlyEpisodes; - return false; + return instance.followQueue; } public static boolean isAutoDelete() { @@ -271,7 +339,7 @@ public class UserPreferences implements } public static int getSmartMarkAsPlayedSecs() { - instanceAvailable();; + instanceAvailable(); return instance.smartMarkAsPlayedSecs; } @@ -280,48 +348,29 @@ public class UserPreferences implements return instance.autoFlattr; } - public static int getNotifyPriority() { - instanceAvailable(); - return instance.notifyPriority; - } - - public static boolean isPersistNotify() { + public static String getPlaybackSpeed() { instanceAvailable(); - return instance.persistNotify; + return instance.playbackSpeed; } - - /** - * Returns the time after which an episode should be auto-flattr'd in percent of the episode's - * duration. - */ - public static float getAutoFlattrPlayedDurationThreshold() { + public static String[] getPlaybackSpeedArray() { instanceAvailable(); - return instance.autoFlattrPlayedDurationThreshold; + return instance.playbackSpeedArray; } - public static int getTheme() { + public static boolean shouldPauseForFocusLoss() { instanceAvailable(); - return instance.theme; - } - - public static int getNoTitleTheme() { - int theme = getTheme(); - if (theme == R.style.Theme_AntennaPod_Dark) { - return R.style.Theme_AntennaPod_Dark_NoTitle; - } else { - return R.style.Theme_AntennaPod_Light_NoTitle; - } + return instance.pauseForFocusLoss; } - public static boolean isEnableAutodownloadWifiFilter() { + public static long getUpdateInterval() { instanceAvailable(); - return instance.enableAutodownloadWifiFilter; + return instance.updateInterval; } - public static String[] getAutodownloadSelectedNetworks() { + public static boolean isAllowMobileUpdate() { instanceAvailable(); - return instance.autodownloadSelectedNetworks; + return instance.allowMobileUpdate; } public static int getParallelDownloads() { @@ -333,21 +382,6 @@ public class UserPreferences implements return EPISODE_CACHE_SIZE_UNLIMITED; } - public static String getPlaybackSpeed() { - instanceAvailable(); - return instance.playbackSpeed; - } - - public static String[] getPlaybackSpeedArray() { - instanceAvailable(); - return instance.playbackSpeedArray; - } - - public static int getSeekDeltaMs() { - instanceAvailable(); - return 1000 * instance.seekDeltaSecs; - } - /** * Returns the capacity of the episode cache. This method will return the * negative integer EPISODE_CACHE_SIZE_UNLIMITED if the cache size is set to @@ -368,88 +402,163 @@ public class UserPreferences implements return instance.enableAutodownloadOnBattery; } - public static boolean shouldPauseForFocusLoss() { + public static boolean isEnableAutodownloadWifiFilter() { instanceAvailable(); - return instance.pauseForFocusLoss; + return instance.enableAutodownloadWifiFilter; + } + + public static int getFastFowardSecs() { + instanceAvailable(); + return instance.fastForwardSecs; + } + + public static int getRewindSecs() { + instanceAvailable(); + return instance.rewindSecs; + } + + + /** + * Returns the time after which an episode should be auto-flattr'd in percent of the episode's + * duration. + */ + public static float getAutoFlattrPlayedDurationThreshold() { + instanceAvailable(); + return instance.autoFlattrPlayedDurationThreshold; + } + + public static String[] getAutodownloadSelectedNetworks() { + instanceAvailable(); + return instance.autodownloadSelectedNetworks; } - public static boolean isFreshInstall() { + public static boolean shouldResumeAfterCall() { instanceAvailable(); - return instance.isFreshInstall; + return instance.resumeAfterCall; + } + + public static boolean isQueueLocked() { + instanceAvailable(); + return instance.queueLocked; } @Override public void onSharedPreferenceChanged(SharedPreferences sp, String key) { Log.d(TAG, "Registered change of user preferences. Key: " + key); - - if (key.equals(PREF_DOWNLOAD_MEDIA_ON_WIFI_ONLY)) { - downloadMediaOnWifiOnly = sp.getBoolean( - PREF_DOWNLOAD_MEDIA_ON_WIFI_ONLY, true); - - } else if (key.equals(PREF_MOBILE_UPDATE)) { - allowMobileUpdate = sp.getBoolean(PREF_MOBILE_UPDATE, false); - - } else if (key.equals(PREF_FOLLOW_QUEUE)) { - followQueue = sp.getBoolean(PREF_FOLLOW_QUEUE, false); - - } else if (key.equals(PREF_UPDATE_INTERVAL)) { - updateInterval = readUpdateInterval(sp.getString( - PREF_UPDATE_INTERVAL, "0")); - ClientConfig.applicationCallbacks.setUpdateInterval(updateInterval); - } else if (key.equals(PREF_AUTO_DELETE)) { - autoDelete = sp.getBoolean(PREF_AUTO_DELETE, false); - } else if (key.equals(PREF_SMART_MARK_AS_PLAYED_SECS)) { - smartMarkAsPlayedSecs = Integer.valueOf(sp.getString(PREF_SMART_MARK_AS_PLAYED_SECS, "30")); - } else if (key.equals(PREF_AUTO_FLATTR)) { - autoFlattr = sp.getBoolean(PREF_AUTO_FLATTR, false); - } else if (key.equals(PREF_DISPLAY_ONLY_EPISODES)) { - displayOnlyEpisodes = sp.getBoolean(PREF_DISPLAY_ONLY_EPISODES, - false); - } else if (key.equals(PREF_THEME)) { - theme = readThemeValue(sp.getString(PREF_THEME, "")); - } else if (key.equals(PREF_ENABLE_AUTODL_WIFI_FILTER)) { - enableAutodownloadWifiFilter = sp.getBoolean( - PREF_ENABLE_AUTODL_WIFI_FILTER, false); - } else if (key.equals(PREF_AUTODL_SELECTED_NETWORKS)) { - autodownloadSelectedNetworks = StringUtils.split( - sp.getString(PREF_AUTODL_SELECTED_NETWORKS, ""), ','); - } else if(key.equals(PREF_PARALLEL_DOWNLOADS)) { - parallelDownloads = Integer.valueOf(sp.getString(PREF_PARALLEL_DOWNLOADS, "6")); - } else if (key.equals(PREF_EPISODE_CACHE_SIZE)) { - episodeCacheSize = readEpisodeCacheSizeInternal(sp.getString( - PREF_EPISODE_CACHE_SIZE, "20")); - } else if (key.equals(PREF_ENABLE_AUTODL)) { - enableAutodownload = sp.getBoolean(PREF_ENABLE_AUTODL, false); - } else if (key.equals(PREF_ENABLE_AUTODL_ON_BATTERY)) { - enableAutodownloadOnBattery = sp.getBoolean(PREF_ENABLE_AUTODL_ON_BATTERY, true); - } else if (key.equals(PREF_PLAYBACK_SPEED)) { - playbackSpeed = sp.getString(PREF_PLAYBACK_SPEED, "1.0"); - } else if (key.equals(PREF_PLAYBACK_SPEED_ARRAY)) { - playbackSpeedArray = readPlaybackSpeedArray(sp.getString( - PREF_PLAYBACK_SPEED_ARRAY, null)); - } else if (key.equals(PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS)) { - pauseForFocusLoss = sp.getBoolean(PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS, false); - } else if (key.equals(PREF_SEEK_DELTA_SECS)) { - seekDeltaSecs = Integer.valueOf(sp.getString(PREF_SEEK_DELTA_SECS, "30")); - } else if (key.equals(PREF_PAUSE_ON_HEADSET_DISCONNECT)) { - pauseOnHeadsetDisconnect = sp.getBoolean(PREF_PAUSE_ON_HEADSET_DISCONNECT, true); - } else if (key.equals(PREF_UNPAUSE_ON_HEADSET_RECONNECT)) { - unpauseOnHeadsetReconnect = sp.getBoolean(PREF_UNPAUSE_ON_HEADSET_RECONNECT, true); - } else if (key.equals(PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD)) { - autoFlattrPlayedDurationThreshold = sp.getFloat(PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD, - PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD_DEFAULT); - } else if (key.equals(PREF_EXPANDED_NOTIFICATION)) { - if (sp.getBoolean(PREF_EXPANDED_NOTIFICATION, false)) { - notifyPriority = NotificationCompat.PRIORITY_MAX; - } - else { - notifyPriority = NotificationCompat.PRIORITY_DEFAULT; - } - } else if (key.equals(PREF_PERSISTENT_NOTIFICATION)) { - persistNotify = sp.getBoolean(PREF_PERSISTENT_NOTIFICATION, false); + switch(key) { + // User Interface + case PREF_THEME: + theme = readThemeValue(sp.getString(PREF_THEME, "")); + break; + case PREF_HIDDEN_DRAWER_ITEMS: + hiddenDrawerItems = Arrays.asList(StringUtils.split(sp.getString(PREF_HIDDEN_DRAWER_ITEMS, ""), ',')); + break; + case PREF_EXPANDED_NOTIFICATION: + if (sp.getBoolean(PREF_EXPANDED_NOTIFICATION, false)) { + notifyPriority = NotificationCompat.PRIORITY_MAX; + } else { + notifyPriority = NotificationCompat.PRIORITY_DEFAULT; + } + break; + case PREF_PERSISTENT_NOTIFICATION: + persistNotify = sp.getBoolean(PREF_PERSISTENT_NOTIFICATION, false); + break; + // Queue + case PREF_QUEUE_ADD_TO_FRONT: + enqueueAtFront = sp.getBoolean(PREF_QUEUE_ADD_TO_FRONT, false); + break; + // Playback + case PREF_PAUSE_ON_HEADSET_DISCONNECT: + pauseOnHeadsetDisconnect = sp.getBoolean(PREF_PAUSE_ON_HEADSET_DISCONNECT, true); + break; + case PREF_UNPAUSE_ON_HEADSET_RECONNECT: + unpauseOnHeadsetReconnect = sp.getBoolean(PREF_UNPAUSE_ON_HEADSET_RECONNECT, true); + break; + case PREF_FOLLOW_QUEUE: + followQueue = sp.getBoolean(PREF_FOLLOW_QUEUE, false); + break; + case PREF_AUTO_DELETE: + autoDelete = sp.getBoolean(PREF_AUTO_DELETE, false); + break; + case PREF_SMART_MARK_AS_PLAYED_SECS: + smartMarkAsPlayedSecs = Integer.valueOf(sp.getString(PREF_SMART_MARK_AS_PLAYED_SECS, "30")); + break; + case PREF_PLAYBACK_SPEED_ARRAY: + playbackSpeedArray = readPlaybackSpeedArray(sp.getString(PREF_PLAYBACK_SPEED_ARRAY, null)); + break; + case PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS: + pauseForFocusLoss = sp.getBoolean(PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS, false); + break; + case PREF_RESUME_AFTER_CALL: + resumeAfterCall = sp.getBoolean(PREF_RESUME_AFTER_CALL, true); + break; + // Network + case PREF_UPDATE_INTERVAL: + updateInterval = readUpdateInterval(sp.getString(PREF_UPDATE_INTERVAL, "0")); + ClientConfig.applicationCallbacks.setUpdateInterval(updateInterval); + break; + case PREF_MOBILE_UPDATE: + allowMobileUpdate = sp.getBoolean(PREF_MOBILE_UPDATE, false); + break; + case PREF_PARALLEL_DOWNLOADS: + parallelDownloads = Integer.valueOf(sp.getString(PREF_PARALLEL_DOWNLOADS, "6")); + break; + case PREF_EPISODE_CACHE_SIZE: + episodeCacheSize = readEpisodeCacheSizeInternal(sp.getString(PREF_EPISODE_CACHE_SIZE, "20")); + break; + case PREF_ENABLE_AUTODL: + enableAutodownload = sp.getBoolean(PREF_ENABLE_AUTODL, false); + break; + case PREF_ENABLE_AUTODL_ON_BATTERY: + enableAutodownloadOnBattery = sp.getBoolean(PREF_ENABLE_AUTODL_ON_BATTERY, true); + break; + case PREF_ENABLE_AUTODL_WIFI_FILTER: + enableAutodownloadWifiFilter = sp.getBoolean(PREF_ENABLE_AUTODL_WIFI_FILTER, false); + break; + case PREF_AUTODL_SELECTED_NETWORKS: + autodownloadSelectedNetworks = StringUtils.split( + sp.getString(PREF_AUTODL_SELECTED_NETWORKS, ""), ','); + break; + // Services + case PREF_AUTO_FLATTR: + autoFlattr = sp.getBoolean(PREF_AUTO_FLATTR, false); + break; + case PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD: + autoFlattrPlayedDurationThreshold = sp.getFloat(PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD, + PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD_DEFAULT); + break; + // Mediaplayer + case PREF_PLAYBACK_SPEED: + playbackSpeed = sp.getString(PREF_PLAYBACK_SPEED, "1.0"); + break; + case PREF_FAST_FORWARD_SECS: + fastForwardSecs = sp.getInt(PREF_FAST_FORWARD_SECS, 30); + break; + case PREF_REWIND_SECS: + rewindSecs = sp.getInt(PREF_REWIND_SECS, 30); + break; + case PREF_QUEUE_LOCKED: + queueLocked = sp.getBoolean(PREF_QUEUE_LOCKED, false); + break; + default: + Log.w(TAG, "Unhandled key: " + key); } } + public static void setPrefFastForwardSecs(int secs) { + Log.d(TAG, "setPrefFastForwardSecs(" + secs +")"); + SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(instance.context).edit(); + editor.putInt(PREF_FAST_FORWARD_SECS, secs); + editor.commit(); + } + + public static void setPrefRewindSecs(int secs) { + Log.d(TAG, "setPrefRewindSecs(" + secs +")"); + SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(instance.context).edit(); + editor.putInt(PREF_REWIND_SECS, secs); + editor.commit(); + } + public static void setPlaybackSpeed(String speed) { PreferenceManager.getDefaultSharedPreferences(instance.context).edit() .putString(PREF_PLAYBACK_SPEED, speed).apply(); @@ -509,6 +618,26 @@ public class UserPreferences implements instance.autoFlattrPlayedDurationThreshold = autoFlattrThreshold; } + public static void setHiddenDrawerItems(Context context, List<String> items) { + instanceAvailable(); + instance.hiddenDrawerItems = items; + String str = StringUtils.join(items, ','); + PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()) + .edit() + .putString(PREF_HIDDEN_DRAWER_ITEMS, str) + .commit(); + } + + public static void setQueueLocked(boolean locked) { + instanceAvailable(); + instance.queueLocked = locked; + PreferenceManager.getDefaultSharedPreferences(instance.context) + .edit() + .putBoolean(PREF_QUEUE_LOCKED, locked) + .commit(); + } + + /** * Return the folder where the app stores all of its data. This method will * return the standard data folder if none has been set by the user. @@ -524,8 +653,7 @@ public class UserPreferences implements .getDefaultSharedPreferences(context.getApplicationContext()); String strDir = prefs.getString(PREF_DATA_FOLDER, null); if (strDir == null) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Using default data folder"); + Log.d(TAG, "Using default data folder"); return context.getExternalFilesDir(type); } else { File dataDir = new File(strDir); @@ -556,21 +684,18 @@ public class UserPreferences implements if (!typeDir.exists()) { if (dataDir.canWrite()) { if (!typeDir.mkdir()) { - Log.e(TAG, "Could not create data folder named " - + type); + Log.e(TAG, "Could not create data folder named " + type); return null; } } } return typeDir; } - } } public static void setDataFolder(String dir) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Result from DirectoryChooser: " + dir); + Log.d(TAG, "Result from DirectoryChooser: " + dir); instanceAvailable(); SharedPreferences prefs = PreferenceManager .getDefaultSharedPreferences(instance.context); @@ -593,8 +718,7 @@ public class UserPreferences implements Log.e(TAG, "Could not create .nomedia file"); e.printStackTrace(); } - if (BuildConfig.DEBUG) - Log.d(TAG, ".nomedia file created"); + Log.d(TAG, ".nomedia file created"); } } @@ -607,16 +731,13 @@ public class UserPreferences implements IMPORT_DIR); if (importDir != null) { if (importDir.exists()) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Import directory already exists"); + Log.d(TAG, "Import directory already exists"); } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "Creating import directory"); + Log.d(TAG, "Creating import directory"); importDir.mkdir(); } } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "Could not access external storage."); + Log.d(TAG, "Could not access external storage."); } } @@ -625,8 +746,7 @@ public class UserPreferences implements */ public static void restartUpdateAlarm(long triggerAtMillis, long intervalMillis) { instanceAvailable(); - if (BuildConfig.DEBUG) - Log.d(TAG, "Restarting update alarm."); + Log.d(TAG, "Restarting update alarm."); AlarmManager alarmManager = (AlarmManager) instance.context .getSystemService(Context.ALARM_SERVICE); PendingIntent updateIntent = PendingIntent.getBroadcast( @@ -635,11 +755,9 @@ public class UserPreferences implements if (intervalMillis != 0) { alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, triggerAtMillis, intervalMillis, updateIntent); - if (BuildConfig.DEBUG) - Log.d(TAG, "Changed alarm to new interval"); + Log.d(TAG, "Changed alarm to new interval"); } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "Automatic update was deactivated"); + Log.d(TAG, "Automatic update was deactivated"); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java b/core/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java index 95dc4fb07..d37f97a5f 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java +++ b/core/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java @@ -3,41 +3,26 @@ package de.danoeh.antennapod.core.receiver; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; import android.util.Log; -import de.danoeh.antennapod.core.BuildConfig; -import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.storage.DBTasks; +import de.danoeh.antennapod.core.util.NetworkUtils; /** * Refreshes all feeds when it receives an intent */ public class FeedUpdateReceiver extends BroadcastReceiver { + private static final String TAG = "FeedUpdateReceiver"; @Override public void onReceive(Context context, Intent intent) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Received intent"); - boolean mobileUpdate = UserPreferences.isAllowMobileUpdate(); - if (mobileUpdate || connectedToWifi(context)) { + Log.d(TAG, "Received intent"); + if (NetworkUtils.isDownloadAllowed(context)) { DBTasks.refreshExpiredFeeds(context); } else { - if (BuildConfig.DEBUG) - Log.d(TAG, - "Blocking automatic update: no wifi available / no mobile updates allowed"); + Log.d(TAG, "Blocking automatic update: no wifi available / no mobile updates allowed"); } } - private boolean connectedToWifi(Context context) { - ConnectivityManager connManager = (ConnectivityManager) context - .getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo mWifi = connManager - .getNetworkInfo(ConnectivityManager.TYPE_WIFI); - - return mWifi.isConnected(); - } - } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/GpodnetSyncService.java b/core/src/main/java/de/danoeh/antennapod/core/service/GpodnetSyncService.java index e39197387..3f2222f42 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/GpodnetSyncService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/GpodnetSyncService.java @@ -12,7 +12,6 @@ import android.util.Log; import android.util.Pair; import java.util.Collection; -import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; @@ -22,6 +21,7 @@ import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.R; 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.gpoddernet.GpodnetService; import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceAuthenticationException; import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException; @@ -50,17 +50,38 @@ public class GpodnetSyncService extends Service { public static final String ARG_ACTION = "action"; public static final String ACTION_SYNC = "de.danoeh.antennapod.intent.action.sync"; + public static final String ACTION_SYNC_SUBSCRIPTIONS = "de.danoeh.antennapod.intent.action.sync_subscriptions"; + public static final String ACTION_SYNC_ACTIONS = "de.danoeh.antennapod.intent.action.sync_ACTIONS"; private GpodnetService service; + private boolean syncSubscriptions = false; + private boolean syncActions = false; + @Override public int onStartCommand(Intent intent, int flags, int startId) { final String action = (intent != null) ? intent.getStringExtra(ARG_ACTION) : null; - if (action != null && action.equals(ACTION_SYNC)) { - Log.d(TAG, String.format("Waiting %d milliseconds before uploading changes", WAIT_INTERVAL)); - syncWaiterThread.restart(); + if (action != null) { + switch(action) { + case ACTION_SYNC: + syncSubscriptions = true; + syncActions = true; + break; + case ACTION_SYNC_SUBSCRIPTIONS: + syncSubscriptions = true; + break; + case ACTION_SYNC_ACTIONS: + syncActions = true; + break; + default: + Log.e(TAG, "Received invalid intent: action argument is invalid"); + } + if(syncSubscriptions || syncActions) { + Log.d(TAG, String.format("Waiting %d milliseconds before uploading changes", WAIT_INTERVAL)); + syncWaiterThread.restart(); + } } else { - Log.e(TAG, "Received invalid intent: action argument is null or invalid"); + Log.e(TAG, "Received invalid intent: action argument is null"); } return START_FLAG_REDELIVERY; } @@ -91,8 +112,14 @@ public class GpodnetSyncService extends Service { stopSelf(); return; } - syncSubscriptionChanges(); - syncEpisodeActions(); + if(syncSubscriptions) { + syncSubscriptionChanges(); + syncSubscriptions = false; + } + if(syncActions) { + syncEpisodeActions(); + syncActions = false; + } stopSelf(); } @@ -100,37 +127,36 @@ public class GpodnetSyncService extends Service { final long timestamp = GpodnetPreferences.getLastSubscriptionSyncTimestamp(); try { final List<String> localSubscriptions = DBReader.getFeedListDownloadUrls(this); + Collection<String> localAdded = GpodnetPreferences.getAddedFeedsCopy(); + Collection<String> localRemoved = GpodnetPreferences.getRemovedFeedsCopy(); GpodnetService service = tryLogin(); // first sync: download all subscriptions... GpodnetSubscriptionChange subscriptionChanges = service.getSubscriptionChanges(GpodnetPreferences.getUsername(), GpodnetPreferences.getDeviceID(), timestamp); - long lastUpdate = subscriptionChanges.getTimestamp(); + long newTimeStamp = subscriptionChanges.getTimestamp(); Log.d(TAG, "Downloaded subscription changes: " + subscriptionChanges); - processSubscriptionChanges(localSubscriptions, subscriptionChanges); - - Collection<String> added; - Collection<String> removed; - if (timestamp == 0) { - added = localSubscriptions; - GpodnetPreferences.removeRemovedFeeds(GpodnetPreferences.getRemovedFeedsCopy()); - removed = Collections.emptyList(); - } else { - added = GpodnetPreferences.getAddedFeedsCopy(); - removed = GpodnetPreferences.getRemovedFeedsCopy(); + processSubscriptionChanges(localSubscriptions, localAdded, localRemoved, subscriptionChanges); + + if(timestamp == 0) { + // this is this apps first sync with gpodder: + // only submit changes gpodder has not just sent us + localAdded = localSubscriptions; + localAdded.removeAll(subscriptionChanges.getAdded()); + localRemoved.removeAll(subscriptionChanges.getRemoved()); } - if(added.size() > 0 || removed.size() > 0) { + if(localAdded.size() > 0 || localRemoved.size() > 0) { Log.d(TAG, String.format("Uploading subscriptions, Added: %s\nRemoved: %s", - added, removed)); + localAdded, localRemoved)); GpodnetUploadChangesResponse uploadResponse = service.uploadChanges(GpodnetPreferences.getUsername(), - GpodnetPreferences.getDeviceID(), added, removed); - lastUpdate = uploadResponse.timestamp; + GpodnetPreferences.getDeviceID(), localAdded, localRemoved); + newTimeStamp = uploadResponse.timestamp; Log.d(TAG, "Upload changes response: " + uploadResponse); - GpodnetPreferences.removeAddedFeeds(added); - GpodnetPreferences.removeRemovedFeeds(removed); + GpodnetPreferences.removeAddedFeeds(localAdded); + GpodnetPreferences.removeRemovedFeeds(localRemoved); } - GpodnetPreferences.setLastSubscriptionSyncTimestamp(lastUpdate); + GpodnetPreferences.setLastSubscriptionSyncTimestamp(newTimeStamp); clearErrorNotifications(); } catch (GpodnetServiceException e) { e.printStackTrace(); @@ -140,6 +166,27 @@ public class GpodnetSyncService extends Service { } } + private synchronized void processSubscriptionChanges(List<String> localSubscriptions, + Collection<String> localAdded, + Collection<String> localRemoved, + GpodnetSubscriptionChange changes) throws DownloadRequestException { + // local changes are always superior to remote changes! + // add subscription if (1) not already subscribed and (2) not just unsubscribed + for (String downloadUrl : changes.getAdded()) { + if (false == localSubscriptions.contains(downloadUrl) && + false == localRemoved.contains(downloadUrl)) { + Feed feed = new Feed(downloadUrl, new Date(0)); + DownloadRequester.getInstance().downloadFeed(this, feed); + } + } + // remove subscription if not just subscribed (again) + for (String downloadUrl : changes.getRemoved()) { + if(false == localAdded.contains(downloadUrl)) { + DBTasks.removeFeedWithDownloadUrl(GpodnetSyncService.this, downloadUrl); + } + } + } + private synchronized void syncEpisodeActions() { final long timestamp = GpodnetPreferences.getLastEpisodeActionsSyncTimestamp(); Log.d(TAG, "last episode actions sync timestamp: " + timestamp); @@ -173,25 +220,13 @@ public class GpodnetSyncService extends Service { } } - private synchronized void processSubscriptionChanges(List<String> localSubscriptions, GpodnetSubscriptionChange changes) throws DownloadRequestException { - for (String downloadUrl : changes.getAdded()) { - if (!localSubscriptions.contains(downloadUrl)) { - Feed feed = new Feed(downloadUrl, new Date()); - DownloadRequester.getInstance().downloadFeed(this, feed); - } - } - for (String downloadUrl : changes.getRemoved()) { - DBTasks.removeFeedWithDownloadUrl(GpodnetSyncService.this, downloadUrl); - } - } - private synchronized void processEpisodeActions(List<GpodnetEpisodeAction> localActions, List<GpodnetEpisodeAction> remoteActions) throws DownloadRequestException { + private synchronized void processEpisodeActions(List<GpodnetEpisodeAction> localActions, + List<GpodnetEpisodeAction> remoteActions) throws DownloadRequestException { if(remoteActions.size() == 0) { return; } Map<Pair<String, String>, GpodnetEpisodeAction> localMostRecentPlayAction = new HashMap<Pair<String, String>, GpodnetEpisodeAction>(); - Map<Pair<String, String>, GpodnetEpisodeAction> remoteMostRecentPlayAction = new HashMap<Pair<String, String>, GpodnetEpisodeAction>(); - // make sure more recent local actions are not overwritten by older remote actions for(GpodnetEpisodeAction action : localActions) { Pair key = new Pair(action.getPodcast(), action.getEpisode()); GpodnetEpisodeAction mostRecent = localMostRecentPlayAction.get(key); @@ -201,6 +236,9 @@ public class GpodnetSyncService extends Service { localMostRecentPlayAction.put(key, action); } } + + // make sure more recent local actions are not overwritten by older remote actions + Map<Pair<String, String>, GpodnetEpisodeAction> mostRecentPlayAction = new HashMap<Pair<String, String>, GpodnetEpisodeAction>(); for (GpodnetEpisodeAction action : remoteActions) { switch (action.getAction()) { case NEW: @@ -218,11 +256,11 @@ public class GpodnetSyncService extends Service { GpodnetEpisodeAction localMostRecent = localMostRecentPlayAction.get(key); if(localMostRecent == null || localMostRecent.getTimestamp().before(action.getTimestamp())) { - GpodnetEpisodeAction mostRecent = remoteMostRecentPlayAction.get(key); + GpodnetEpisodeAction mostRecent = mostRecentPlayAction.get(key); if (mostRecent == null) { - remoteMostRecentPlayAction.put(key, action); + mostRecentPlayAction.put(key, action); } else if (mostRecent.getTimestamp().before(action.getTimestamp())) { - remoteMostRecentPlayAction.put(key, action); + mostRecentPlayAction.put(key, action); } } break; @@ -231,10 +269,12 @@ public class GpodnetSyncService extends Service { break; } } - for (GpodnetEpisodeAction action : remoteMostRecentPlayAction.values()) { + for (GpodnetEpisodeAction action : mostRecentPlayAction.values()) { FeedItem playItem = DBReader.getFeedItem(this, action.getPodcast(), action.getEpisode()); if (playItem != null) { - playItem.getMedia().setPosition(action.getPosition() * 1000); + FeedMedia media = playItem.getMedia(); + media.setPosition(action.getPosition() * 1000); + DBWriter.setFeedMedia(this, media); if(playItem.getMedia().hasAlmostEnded()) { DBWriter.markItemRead(this, playItem, true, true); DBWriter.addItemToPlaybackHistory(this, playItem.getMedia()); @@ -342,4 +382,20 @@ public class GpodnetSyncService extends Service { context.startService(intent); } } + + public static void sendSyncSubscriptionsIntent(Context context) { + if (GpodnetPreferences.loggedIn()) { + Intent intent = new Intent(context, GpodnetSyncService.class); + intent.putExtra(ARG_ACTION, ACTION_SYNC_SUBSCRIPTIONS); + context.startService(intent); + } + } + + public static void sendSyncActionsIntent(Context context) { + if (GpodnetPreferences.loggedIn()) { + Intent intent = new Intent(context, GpodnetSyncService.class); + intent.putExtra(ARG_ACTION, ACTION_SYNC_ACTIONS); + context.startService(intent); + } + } } 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 d5f17c099..e7b226eca 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 @@ -520,9 +520,7 @@ public class DownloadService extends Service { /** * Creates a notification at the end of the service lifecycle to notify the * user about the number of completed downloads. A report will only be - * created if the number of successfully downloaded feeds is bigger than 1 - * or if there is at least one failed download which is not an image or if - * there is at least one downloaded media file. + * created if there is at least one failed download excluding images */ private void updateReport() { // check if report should be created @@ -550,16 +548,16 @@ public class DownloadService extends Service { .setTicker( getString(R.string.download_report_title)) .setContentTitle( - getString(R.string.download_report_title)) + getString(R.string.download_report_content_title)) .setContentText( String.format( getString(R.string.download_report_content), successfulDownloads, failedDownloads) ) - .setSmallIcon(R.drawable.stat_notify_sync) + .setSmallIcon(R.drawable.stat_notify_sync_error) .setLargeIcon( BitmapFactory.decodeResource(getResources(), - R.drawable.stat_notify_sync) + R.drawable.stat_notify_sync_error) ) .setContentIntent( ClientConfig.downloadServiceCallbacks.getReportNotificationContentIntent(this) @@ -939,6 +937,13 @@ public class DownloadService extends Service { if (successful) { + // we create a 'successful' download log if the feed's last refresh failed + List<DownloadStatus> log = DBReader.getFeedDownloadLog(DownloadService.this, feed); + if(log.size() > 0 && log.get(0).isSuccessful() == false) { + saveDownloadStatus(new DownloadStatus(feed, + feed.getHumanReadableIdentifier(), DownloadError.SUCCESS, successful, + reasonDetailed)); + } return Pair.create(request, result); } else { numberOfDownloads.decrementAndGet(); @@ -1055,9 +1060,11 @@ public class DownloadService extends Service { @Override public void run() { - if (request.isDeleteOnFailure()) { + if(request.getFeedfileType() == Feed.FEEDFILETYPE_FEED) { + DBWriter.setFeedLastUpdateFailed(DownloadService.this, request.getFeedfileId(), true); + } else if (request.isDeleteOnFailure()) { Log.d(TAG, "Ignoring failed download, deleteOnFailure=true"); - } else { + } else { File dest = new File(request.getDestination()); if (dest.exists() && request.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { Log.d(TAG, "File has been partially downloaded. Writing file url"); 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 7abb6df5e..ac0fe8036 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,7 +2,6 @@ package de.danoeh.antennapod.core.service.download; import android.util.Log; -import com.squareup.okhttp.Credentials; import com.squareup.okhttp.OkHttpClient; import com.squareup.okhttp.Request; import com.squareup.okhttp.Response; @@ -18,19 +17,20 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.RandomAccessFile; +import java.io.UnsupportedEncodingException; import java.net.HttpURLConnection; import java.net.SocketTimeoutException; import java.net.URI; import java.net.UnknownHostException; import java.util.Date; -import de.danoeh.antennapod.core.BuildConfig; import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.R; import de.danoeh.antennapod.core.feed.FeedImage; import de.danoeh.antennapod.core.util.DownloadError; import de.danoeh.antennapod.core.util.StorageUtils; import de.danoeh.antennapod.core.util.URIUtil; +import okio.ByteString; public class HttpDownloader extends Downloader { private static final String TAG = "HttpDownloader"; @@ -81,11 +81,12 @@ public class HttpDownloader extends Downloader { if (userInfo != null) { String[] parts = userInfo.split(":"); if (parts.length == 2) { - String credentials = Credentials.basic(parts[0], parts[1]); + String credentials = encodeCredentials(parts[0], parts[1], "ISO-8859-1"); httpReq.header("Authorization", credentials); } } else if (!StringUtils.isEmpty(request.getUsername()) && request.getPassword() != null) { - String credentials = Credentials.basic(request.getUsername(), request.getPassword()); + String credentials = encodeCredentials(request.getUsername(), request.getPassword(), + "ISO-8859-1"); httpReq.header("Authorization", credentials); } @@ -99,13 +100,29 @@ public class HttpDownloader extends Downloader { Response response = httpClient.newCall(httpReq.build()).execute(); responseBody = response.body(); - String contentEncodingHeader = response.header("Content-Encoding"); - - final boolean isGzip = StringUtils.equalsIgnoreCase(contentEncodingHeader, "gzip"); - - if (BuildConfig.DEBUG) - Log.d(TAG, "Response code is " + response.code()); + boolean isGzip = StringUtils.equalsIgnoreCase(contentEncodingHeader, "gzip"); + + Log.d(TAG, "Response code is " + response.code()); + + if(!response.isSuccessful() && response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) { + Log.d(TAG, "Authorization failed, re-trying with UTF-8 encoding"); + if (userInfo != null) { + String[] parts = userInfo.split(":"); + if (parts.length == 2) { + String credentials = encodeCredentials(parts[0], parts[1], "UTF-8"); + httpReq.header("Authorization", credentials); + } + } else if (!StringUtils.isEmpty(request.getUsername()) && request.getPassword() != null) { + String credentials = encodeCredentials(request.getUsername(), request.getPassword(), + "UTF-8"); + httpReq.header("Authorization", credentials); + } + response = httpClient.newCall(httpReq.build()).execute(); + responseBody = response.body(); + contentEncodingHeader = response.header("Content-Encoding"); + isGzip = StringUtils.equalsIgnoreCase(contentEncodingHeader, "gzip"); + } if(!response.isSuccessful() && response.code() == HttpURLConnection.HTTP_NOT_MODIFIED) { Log.d(TAG, "Feed '" + request.getSource() + "' not modified since last update, Download canceled"); @@ -151,22 +168,18 @@ public class HttpDownloader extends Downloader { out = new RandomAccessFile(destination, "rw"); } - byte[] buffer = new byte[BUFFER_SIZE]; int count = 0; request.setStatusMsg(R.string.download_running); - if (BuildConfig.DEBUG) - Log.d(TAG, "Getting size of download"); + Log.d(TAG, "Getting size of download"); request.setSize(responseBody.contentLength() + request.getSoFar()); - if (BuildConfig.DEBUG) - Log.d(TAG, "Size is " + request.getSize()); + Log.d(TAG, "Size is " + request.getSize()); if (request.getSize() < 0) { request.setSize(DownloadStatus.SIZE_UNKNOWN); } long freeSpace = StorageUtils.getFreeSpaceAvailable(); - if (BuildConfig.DEBUG) - Log.d(TAG, "Free space is " + freeSpace); + Log.d(TAG, "Free space is " + freeSpace); if (request.getSize() != DownloadStatus.SIZE_UNKNOWN && request.getSize() > freeSpace) { @@ -174,8 +187,7 @@ public class HttpDownloader extends Downloader { return; } - if (BuildConfig.DEBUG) - Log.d(TAG, "Starting download"); + Log.d(TAG, "Starting download"); while (!cancelled && (count = connection.read(buffer)) != -1) { out.write(buffer, 0, count); @@ -226,15 +238,12 @@ public class HttpDownloader extends Downloader { } private void onSuccess() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Download was successful"); + Log.d(TAG, "Download was successful"); result.setSuccessful(); } private void onFail(DownloadError reason, String reasonDetailed) { - if (BuildConfig.DEBUG) { - Log.d(TAG, "Download failed"); - } + Log.d(TAG, "Download failed"); result.setFailed(reason, reasonDetailed); if (request.isDeleteOnFailure()) { cleanup(); @@ -242,8 +251,7 @@ public class HttpDownloader extends Downloader { } private void onCancelled() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Download was cancelled"); + Log.d(TAG, "Download was cancelled"); result.setCancelled(); cleanup(); } @@ -256,14 +264,23 @@ public class HttpDownloader extends Downloader { File dest = new File(request.getDestination()); if (dest.exists()) { boolean rc = dest.delete(); - if (BuildConfig.DEBUG) - Log.d(TAG, "Deleted file " + dest.getName() + "; Result: " + Log.d(TAG, "Deleted file " + dest.getName() + "; Result: " + rc); } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "cleanup() didn't delete file: does not exist."); + Log.d(TAG, "cleanup() didn't delete file: does not exist."); } } } + public static String encodeCredentials(String username, String password, String charset) { + try { + String credentials = username + ":" + password; + byte[] bytes = credentials.getBytes(charset); + String encoded = ByteString.of(bytes).base64(); + return "Basic " + encoded; + } catch (UnsupportedEncodingException e) { + throw new AssertionError(); + } + } + } 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 43c345fec..3f6769ee4 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 @@ -254,6 +254,8 @@ public class PlaybackService extends Service { unregisterReceiver(bluetoothStateUpdated); unregisterReceiver(audioBecomingNoisy); unregisterReceiver(skipCurrentEpisodeReceiver); + unregisterReceiver(pausePlayCurrentEpisodeReceiver); + unregisterReceiver(pauseResumeCurrentEpisodeReceiver); mediaPlayer.shutdown(); taskManager.shutdown(); } @@ -344,11 +346,11 @@ public class PlaybackService extends Service { break; case KeyEvent.KEYCODE_MEDIA_NEXT: case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: - mediaPlayer.seekDelta(UserPreferences.getSeekDeltaMs()); + mediaPlayer.seekDelta(UserPreferences.getFastFowardSecs() * 1000); break; case KeyEvent.KEYCODE_MEDIA_PREVIOUS: case KeyEvent.KEYCODE_MEDIA_REWIND: - mediaPlayer.seekDelta(-UserPreferences.getSeekDeltaMs()); + mediaPlayer.seekDelta(-UserPreferences.getRewindSecs() * 1000); break; case KeyEvent.KEYCODE_MEDIA_STOP: if (status == PlayerStatus.PLAYING) { @@ -481,9 +483,8 @@ public class PlaybackService extends Service { } Intent statusUpdate = new Intent(ACTION_PLAYER_STATUS_CHANGED); - statusUpdate.putExtra(EXTRA_NEW_PLAYER_STATUS, newInfo.playerStatus.ordinal()); + // statusUpdate.putExtra(EXTRA_NEW_PLAYER_STATUS, newInfo.playerStatus.ordinal()); sendBroadcast(statusUpdate); - sendBroadcast(new Intent(ACTION_PLAYER_STATUS_CHANGED)); updateWidget(); refreshRemoteControlClientState(newInfo); bluetoothNotifyChange(newInfo, AVRCP_ACTION_PLAYER_STATUS_CHANGED); @@ -626,7 +627,6 @@ public class PlaybackService extends Service { prepareImmediately = startWhenPrepared = true; } else { Log.d(TAG, "No more episodes available to play"); - prepareImmediately = startWhenPrepared = false; stopForeground(true); stopWidgetUpdater(); @@ -933,7 +933,6 @@ public class PlaybackService extends Service { // Auto flattr if (isAutoFlattrable(media) && (media.getPlayedDuration() > UserPreferences.getAutoFlattrPlayedDurationThreshold() * duration)) { - Log.d(TAG, "saveCurrentPosition: performing auto flattr since played duration " + Integer.toString(media.getPlayedDuration()) + " is " + UserPreferences.getAutoFlattrPlayedDurationThreshold() * 100 + "% of file duration " + Integer.toString(duration)); DBTasks.flattrItemIfLoggedIn(this, item); 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 f0acc3531..243ee78e4 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 @@ -40,7 +40,7 @@ import de.danoeh.antennapod.core.util.playback.VideoPlayer; * Manages the MediaPlayer object of the PlaybackService. */ public class PlaybackServiceMediaPlayer { - public static final String TAG = "PlaybackServiceMediaPlayer"; + public static final String TAG = "PlaybackSvcMediaPlayer"; /** * Return value of some PSMP methods if the method call failed. @@ -449,15 +449,15 @@ public class PlaybackServiceMediaPlayer { if (playerStatus == PlayerStatus.PLAYING || playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PREPARED) { - if (stream) { - // statusBeforeSeeking = playerStatus; - // setPlayerStatus(PlayerStatus.SEEKING, media); + if (!stream) { + statusBeforeSeeking = playerStatus; + setPlayerStatus(PlayerStatus.SEEKING, media); } mediaPlayer.seekTo(t); } else if (playerStatus == PlayerStatus.INITIALIZED) { media.setPosition(t); - startWhenPrepared.set(true); + startWhenPrepared.set(false); prepare(); } playerLock.unlock(); @@ -534,20 +534,20 @@ public class PlaybackServiceMediaPlayer { * Returns the position of the current media object or INVALID_TIME if the position could not be retrieved. */ public int getPosition() { - if (!playerLock.tryLock()) { - return INVALID_TIME; - } + playerLock.lock(); int retVal = INVALID_TIME; if (playerStatus == PlayerStatus.PLAYING || playerStatus == PlayerStatus.PAUSED - || playerStatus == PlayerStatus.PREPARED) { + || playerStatus == PlayerStatus.PREPARED + || playerStatus == PlayerStatus.SEEKING) { retVal = mediaPlayer.getCurrentPosition(); } else if (media != null && media.getPosition() > 0) { retVal = media.getPosition(); } playerLock.unlock(); + Log.d(TAG, "getPosition() -> " + retVal); return retVal; } @@ -735,6 +735,7 @@ public class PlaybackServiceMediaPlayer { int state; if (playerStatus != null) { + Log.d(TAG, "playerStatus: " + playerStatus.toString()); switch (playerStatus) { case PLAYING: state = PlaybackStateCompat.STATE_PLAYING; @@ -798,10 +799,10 @@ public class PlaybackServiceMediaPlayer { // If there is an incoming call, playback should be paused permanently TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); final int callState = (tm != null) ? tm.getCallState() : 0; - Log.d(TAG, "Call state: " + callState); Log.i(TAG, "Call state:" + callState); - if (focusChange == AudioManager.AUDIOFOCUS_LOSS || callState != TelephonyManager.CALL_STATE_IDLE) { + if (focusChange == AudioManager.AUDIOFOCUS_LOSS || + (!UserPreferences.shouldResumeAfterCall() && callState != TelephonyManager.CALL_STATE_IDLE)) { Log.d(TAG, "Lost audio focus"); pause(true, false); callback.shouldStop(); @@ -1095,13 +1096,13 @@ public class PlaybackServiceMediaPlayer { @Override public void onFastForward() { super.onFastForward(); - seekDelta(UserPreferences.getSeekDeltaMs()); + seekDelta(UserPreferences.getFastFowardSecs() * 1000); } @Override public void onRewind() { super.onRewind(); - seekDelta(-UserPreferences.getSeekDeltaMs()); + seekDelta(-UserPreferences.getRewindSecs() * 1000); } @Override diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java index cde03adea..fc73c9446 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java @@ -19,8 +19,10 @@ import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.QueueEvent; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.util.playback.Playable; + import de.greenrobot.event.EventBus; + /** * Manages the background tasks of PlaybackSerivce, i.e. * the sleep timer, the position saver, the widget updater and @@ -147,9 +149,9 @@ public class PlaybackServiceTaskManager { positionSaverFuture = schedExecutor.scheduleWithFixedDelay(positionSaver, POSITION_SAVER_WAITING_INTERVAL, POSITION_SAVER_WAITING_INTERVAL, TimeUnit.MILLISECONDS); - if (BuildConfig.DEBUG) Log.d(TAG, "Started PositionSaver"); + Log.d(TAG, "Started PositionSaver"); } else { - if (BuildConfig.DEBUG) Log.d(TAG, "Call to startPositionSaver was ignored."); + Log.d(TAG, "Call to startPositionSaver was ignored."); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/APCleanupAlgorithm.java b/core/src/main/java/de/danoeh/antennapod/core/storage/APCleanupAlgorithm.java index de6c02de7..f647fd537 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/APCleanupAlgorithm.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/APCleanupAlgorithm.java @@ -41,10 +41,10 @@ public class APCleanupAlgorithm implements EpisodeCleanupAlgorithm<Integer> { Date r = rhs.getMedia().getPlaybackCompletionDate(); if (l == null) { - l = new Date(0); + l = new Date(); } if (r == null) { - r = new Date(0); + r = new Date(); } return l.compareTo(r); } 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 c5f871f48..92de1eee7 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 @@ -4,10 +4,9 @@ import android.content.Context; import android.util.Log; import java.util.ArrayList; -import java.util.Arrays; +import java.util.Iterator; import java.util.List; -import de.danoeh.antennapod.core.BuildConfig; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.util.NetworkUtils; @@ -53,75 +52,53 @@ public class APDownloadAlgorithm implements AutomaticDownloadAlgorithm { Log.d(TAG, "Performing auto-dl of undownloaded episodes"); - final List<FeedItem> queue = DBReader.getQueue(context); - final List<FeedItem> unreadItems = DBReader - .getUnreadItemsList(context); + List<FeedItem> candidates; + if(mediaIds.length > 0) { + candidates = DBReader.getFeedItems(context, mediaIds); + } else { + final List<FeedItem> queue = DBReader.getQueue(context); + final List<FeedItem> unreadItems = DBReader.getUnreadItemsList(context); + candidates = new ArrayList<FeedItem>(queue.size() + unreadItems.size()); + candidates.addAll(queue); + for(FeedItem unreadItem : unreadItems) { + if(candidates.contains(unreadItem) == false) { + candidates.add(unreadItem); + } + } + } - int undownloadedEpisodes = DBTasks.getNumberOfUndownloadedEpisodes(queue, - unreadItems); - int downloadedEpisodes = DBReader - .getNumberOfDownloadedEpisodes(context); + // filter items that are not auto downloadable + Iterator<FeedItem> it = candidates.iterator(); + while(it.hasNext()) { + FeedItem item = it.next(); + if(item.isAutoDownloadable() == false) { + it.remove(); + } + } + + int autoDownloadableEpisodes = candidates.size(); + int downloadedEpisodes = DBReader.getNumberOfDownloadedEpisodes(context); int deletedEpisodes = cleanupAlgorithm.performCleanup(context, - APCleanupAlgorithm.getPerformAutoCleanupArgs(context, undownloadedEpisodes)); - int episodeSpaceLeft = undownloadedEpisodes; + APCleanupAlgorithm.getPerformAutoCleanupArgs(context, autoDownloadableEpisodes)); boolean cacheIsUnlimited = UserPreferences.getEpisodeCacheSize() == UserPreferences .getEpisodeCacheSizeUnlimited(); - - if (!cacheIsUnlimited - && UserPreferences.getEpisodeCacheSize() < downloadedEpisodes - + undownloadedEpisodes) { - episodeSpaceLeft = UserPreferences.getEpisodeCacheSize() - - (downloadedEpisodes - deletedEpisodes); + int episodeCacheSize = UserPreferences.getEpisodeCacheSize(); + + int episodeSpaceLeft; + if (cacheIsUnlimited || + episodeCacheSize >= downloadedEpisodes + autoDownloadableEpisodes) { + episodeSpaceLeft = autoDownloadableEpisodes; + } else { + episodeSpaceLeft = episodeCacheSize - (downloadedEpisodes - deletedEpisodes); } - Arrays.sort(mediaIds); // sort for binary search - final boolean ignoreMediaIds = mediaIds.length == 0; - List<FeedItem> itemsToDownload = new ArrayList<FeedItem>(); - - if (episodeSpaceLeft > 0 && undownloadedEpisodes > 0) { - for (int i = 0; i < queue.size(); i++) { // ignore playing item - FeedItem item = queue.get(i); - long mediaId = (item.hasMedia()) ? item.getMedia().getId() : -1; - if ((ignoreMediaIds || Arrays.binarySearch(mediaIds, mediaId) >= 0) - && item.hasMedia() - && !item.getMedia().isDownloaded() - && !item.getMedia().isPlaying() - && item.getFeed().getPreferences().getAutoDownload()) { - itemsToDownload.add(item); - episodeSpaceLeft--; - undownloadedEpisodes--; - if (episodeSpaceLeft == 0 || undownloadedEpisodes == 0) { - break; - } - } - } - } + FeedItem[] itemsToDownload = candidates.subList(0, episodeSpaceLeft) + .toArray(new FeedItem[episodeSpaceLeft]); - if (episodeSpaceLeft > 0 && undownloadedEpisodes > 0) { - for (FeedItem item : unreadItems) { - long mediaId = (item.hasMedia()) ? item.getMedia().getId() : -1; - if ((ignoreMediaIds || Arrays.binarySearch(mediaIds, mediaId) >= 0) - && item.hasMedia() - && !item.getMedia().isDownloaded() - && item.getFeed().getPreferences().getAutoDownload()) { - itemsToDownload.add(item); - episodeSpaceLeft--; - undownloadedEpisodes--; - if (episodeSpaceLeft == 0 || undownloadedEpisodes == 0) { - break; - } - } - } - } - if (BuildConfig.DEBUG) - Log.d(TAG, "Enqueueing " + itemsToDownload.size() - + " items for download"); + Log.d(TAG, "Enqueueing " + itemsToDownload.length + " items for download"); try { - DBTasks.downloadFeedItems(false, context, - itemsToDownload.toArray(new FeedItem[itemsToDownload - .size()]) - ); + DBTasks.downloadFeedItems(false, context, itemsToDownload); } catch (DownloadRequestException e) { e.printStackTrace(); } @@ -130,4 +107,5 @@ public class APDownloadAlgorithm implements AutomaticDownloadAlgorithm { } }; } + } 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 a7c98c7c6..cc20b3d37 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 @@ -4,8 +4,11 @@ import android.content.Context; import android.database.Cursor; import android.util.Log; +import org.apache.commons.lang3.StringUtils; + import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.Date; import java.util.List; @@ -21,6 +24,7 @@ import de.danoeh.antennapod.core.feed.SimpleChapter; import de.danoeh.antennapod.core.feed.VorbisCommentChapter; import de.danoeh.antennapod.core.service.download.DownloadStatus; import de.danoeh.antennapod.core.util.DownloadError; +import de.danoeh.antennapod.core.util.LongIntMap; import de.danoeh.antennapod.core.util.LongList; import de.danoeh.antennapod.core.util.comparator.DownloadStatusComparator; import de.danoeh.antennapod.core.util.comparator.FeedItemPubdateComparator; @@ -174,8 +178,7 @@ public final class DBReader { */ public static List<FeedItem> getFeedItemList(Context context, final Feed feed) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Extracting Feeditems of feed " + feed.getTitle()); + Log.d(TAG, "Extracting Feeditems of feed " + feed.getTitle()); PodDBAdapter adapter = new PodDBAdapter(context); adapter.open(); @@ -228,7 +231,9 @@ public final class DBReader { itemlistCursor.getInt(PodDBAdapter.IDX_FI_SMALL_HAS_CHAPTERS) > 0, image, (itemlistCursor.getInt(PodDBAdapter.IDX_FI_SMALL_READ) > 0), - itemlistCursor.getString(PodDBAdapter.IDX_FI_SMALL_ITEM_IDENTIFIER)); + itemlistCursor.getString(PodDBAdapter.IDX_FI_SMALL_ITEM_IDENTIFIER), + itemlistCursor.getInt(itemlistCursor.getColumnIndex(PodDBAdapter.KEY_AUTO_DOWNLOAD)) > 0 + ); itemIds.add(String.valueOf(item.getId())); @@ -312,7 +317,10 @@ public final class DBReader { cursor.getInt(PodDBAdapter.IDX_FEED_SEL_STD_DOWNLOADED) > 0, new FlattrStatus(cursor.getLong(PodDBAdapter.IDX_FEED_SEL_STD_FLATTR_STATUS)), cursor.getInt(PodDBAdapter.IDX_FEED_SEL_STD_IS_PAGED) > 0, - cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_NEXT_PAGE_LINK)); + cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_NEXT_PAGE_LINK), + cursor.getString(cursor.getColumnIndex(PodDBAdapter.KEY_HIDE)), + cursor.getInt(cursor.getColumnIndex(PodDBAdapter.KEY_LAST_UPDATE_FAILED)) > 0 + ); if (image != null) { image.setOwner(feed); @@ -327,6 +335,21 @@ public final class DBReader { return feed; } + private static DownloadStatus extractDownloadStatusFromCursorRow(final Cursor cursor) { + long id = cursor.getLong(PodDBAdapter.KEY_ID_INDEX); + long feedfileId = cursor.getLong(PodDBAdapter.KEY_FEEDFILE_INDEX); + int feedfileType = cursor.getInt(PodDBAdapter.KEY_FEEDFILETYPE_INDEX); + boolean successful = cursor.getInt(PodDBAdapter.KEY_SUCCESSFUL_INDEX) > 0; + int reason = cursor.getInt(PodDBAdapter.KEY_REASON_INDEX); + String reasonDetailed = cursor.getString(PodDBAdapter.KEY_REASON_DETAILED_INDEX); + String title = cursor.getString(PodDBAdapter.KEY_DOWNLOADSTATUS_TITLE_INDEX); + Date completionDate = new Date(cursor.getLong(PodDBAdapter.KEY_COMPLETION_DATE_INDEX)); + + return new DownloadStatus(id, title, feedfileId, + feedfileType, successful, DownloadError.fromCode(reason), completionDate, + reasonDetailed); + } + private static FeedItem getMatchingItemForMedia(long itemId, List<FeedItem> items) { @@ -468,21 +491,45 @@ public final class DBReader { } /** + * Loads a list of FeedItems that are considered new. + * + * @param context A context that is used for opening a database connection. + * @return A list of FeedItems that are considered new. + */ + public static List<FeedItem> getNewItemsList(Context context) { + Log.d(TAG, "getNewItemsList()"); + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + + Cursor itemlistCursor = adapter.getNewItemsCursor(); + List<FeedItem> items = extractItemlistFromCursor(adapter, itemlistCursor); + itemlistCursor.close(); + + loadFeedDataOfFeedItemlist(context, items); + + adapter.close(); + + return items; + } + + /** * Loads the IDs of the FeedItems whose 'read'-attribute is set to false. * * @param context A context that is used for opening a database connection. * @return A list of IDs of the FeedItems whose 'read'-attribute is set to false. This method should be preferred * over {@link #getUnreadItemsList(android.content.Context)} if the FeedItems in the UnreadItems list are not used. */ - public static long[] getUnreadItemIds(Context context) { + public static LongList getNewItemIds(Context context) { PodDBAdapter adapter = new PodDBAdapter(context); adapter.open(); - Cursor cursor = adapter.getUnreadItemIdsCursor(); - long[] itemIds = new long[cursor.getCount()]; + Cursor cursor = adapter.getNewItemIdsCursor(); + LongList itemIds = new LongList(cursor.getCount()); int i = 0; if (cursor.moveToFirst()) { do { - itemIds[i] = cursor.getLong(PodDBAdapter.KEY_ID_INDEX); + long id = cursor.getLong(PodDBAdapter.KEY_ID_INDEX); + itemIds.add(id); i++; } while (cursor.moveToNext()); } @@ -565,27 +612,7 @@ public final class DBReader { if (logCursor.moveToFirst()) { do { - long id = logCursor.getLong(PodDBAdapter.KEY_ID_INDEX); - - long feedfileId = logCursor - .getLong(PodDBAdapter.KEY_FEEDFILE_INDEX); - int feedfileType = logCursor - .getInt(PodDBAdapter.KEY_FEEDFILETYPE_INDEX); - boolean successful = logCursor - .getInt(PodDBAdapter.KEY_SUCCESSFUL_INDEX) > 0; - int reason = logCursor.getInt(PodDBAdapter.KEY_REASON_INDEX); - String reasonDetailed = logCursor - .getString(PodDBAdapter.KEY_REASON_DETAILED_INDEX); - String title = logCursor - .getString(PodDBAdapter.KEY_DOWNLOADSTATUS_TITLE_INDEX); - Date completionDate = new Date( - logCursor - .getLong(PodDBAdapter.KEY_COMPLETION_DATE_INDEX) - ); - downloadLog.add(new DownloadStatus(id, title, feedfileId, - feedfileType, successful, DownloadError.fromCode(reason), completionDate, - reasonDetailed)); - + downloadLog.add(extractDownloadStatusFromCursorRow(logCursor)); } while (logCursor.moveToNext()); } logCursor.close(); @@ -594,6 +621,60 @@ public final class DBReader { } /** + * Loads the download log for a particular feed from the database. + * + * @param context A context that is used for opening a database connection. + * @param feed Feed for which the download log is loaded + * @return A list with DownloadStatus objects that represent the feed's download log, + * newest events first. + */ + public static List<DownloadStatus> getFeedDownloadLog(Context context, Feed feed) { + Log.d(TAG, "getFeedDownloadLog(CONTEXT, " + feed.toString() + ")"); + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + Cursor cursor = adapter.getDownloadLog(Feed.FEEDFILETYPE_FEED, feed.getId()); + List<DownloadStatus> downloadLog = new ArrayList<DownloadStatus>( + cursor.getCount()); + + if (cursor.moveToFirst()) { + do { + downloadLog.add(extractDownloadStatusFromCursorRow(cursor)); + } while (cursor.moveToNext()); + } + cursor.close(); + Collections.sort(downloadLog, new DownloadStatusComparator()); + return downloadLog; + } + + /** + * Loads the download log for a particular feed media from the database. + * + * @param context A context that is used for opening a database connection. + * @param media Feed media for which the download log is loaded + * @return A list with DownloadStatus objects that represent the feed media's download log, + * newest events first. + */ + public static List<DownloadStatus> getFeedMediaDownloadLog(Context context, FeedMedia media) { + Log.d(TAG, "getFeedDownloadLog(CONTEXT, " + media.toString() + ")"); + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + Cursor cursor = adapter.getDownloadLog(FeedMedia.FEEDFILETYPE_FEEDMEDIA, media.getId()); + List<DownloadStatus> downloadLog = new ArrayList<DownloadStatus>( + cursor.getCount()); + + if (cursor.moveToFirst()) { + do { + downloadLog.add(extractDownloadStatusFromCursorRow(cursor)); + } while (cursor.moveToNext()); + } + cursor.close(); + Collections.sort(downloadLog, new DownloadStatusComparator()); + return downloadLog; + } + + /** * Loads the FeedItemStatistics objects of all Feeds in the database. This method should be preferred over * {@link #getFeedItemList(android.content.Context, de.danoeh.antennapod.core.feed.Feed)} if only metadata about * the FeedItems is needed. @@ -670,6 +751,31 @@ public final class DBReader { } } return item; + } + + static List<FeedItem> getFeedItems(final Context context, PodDBAdapter adapter, final long... itemIds) { + + String[] ids = new String[itemIds.length]; + for(int i = 0; i < itemIds.length; i++) { + long itemId = itemIds[i]; + ids[i] = Long.toString(itemId); + } + + List<FeedItem> result; + + Cursor itemCursor = adapter.getFeedItemCursor(ids); + if (itemCursor.moveToFirst()) { + result = extractItemlistFromCursor(adapter, itemCursor); + loadFeedDataOfFeedItemlist(context, result); + for(FeedItem item : result) { + if (item.hasChapters()) { + loadChaptersOfFeedItem(adapter, item); + } + } + } else { + result = Collections.emptyList(); + } + return result; } @@ -691,7 +797,6 @@ public final class DBReader { FeedItem item = getFeedItem(context, itemId, adapter); adapter.close(); return item; - } static FeedItem getFeedItem(final Context context, final String podcastUrl, final String episodeUrl, PodDBAdapter adapter) { @@ -712,6 +817,55 @@ public final class DBReader { } /** + * Loads specific FeedItems from the database. This method canbe used for loading more + * than one FeedItem + * + * @param context A context that is used for opening a database connection. + * @param itemIds The IDs of the FeedItems + * @return The FeedItems or an empty list if none of the FeedItems could be found. All FeedComponent-attributes + * as well as chapter marks of the FeedItems will also be loaded from the database. + */ + public static List<FeedItem> getFeedItems(final Context context, final long... itemIds) { + Log.d(TAG, "Loading feeditem with ids: " + StringUtils.join(itemIds, ",")); + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + List<FeedItem> items = getFeedItems(context, adapter, itemIds); + adapter.close(); + return items; + } + + + /** + * Returns credentials based on image URL + * + * @param context A context that is used for opening a database connection. + * @param imageUrl The URL of the image + * @return Credentials in format "<Username>:<Password>", empty String if no authorization given + */ + public static String getImageAuthentication(final Context context, final String imageUrl) { + Log.d(TAG, "Loading credentials for image with URL " + imageUrl); + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + String credentials = getImageAuthentication(context, imageUrl, adapter); + adapter.close(); + return credentials; + + } + + static String getImageAuthentication(final Context context, final String imageUrl, PodDBAdapter adapter) { + String credentials = null; + Cursor cursor = adapter.getImageAuthenticationCursor(imageUrl); + if (cursor.moveToFirst()) { + String username = cursor.getString(0); + String password = cursor.getString(1); + return username + ":" + password; + } + return ""; + } + + /** * Loads a specific FeedItem from the database. * * @param context A context that is used for opening a database connection. @@ -828,10 +982,24 @@ public final class DBReader { * @param context A context that is used for opening a database connection. * @return The number of unread items. */ - public static int getNumberOfUnreadItems(final Context context) { + public static int getNumberOfNewItems(final Context context) { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + final int result = adapter.getNumberOfNewItems(); + adapter.close(); + return result; + } + + /** + * Returns a map containing the number of unread items per feed + * + * @param context A context that is used for opening a database connection. + * @return The number of unread items per feed. + */ + public static LongIntMap getNumberOfUnreadFeedItems(final Context context, long... feedIds) { PodDBAdapter adapter = new PodDBAdapter(context); adapter.open(); - final int result = adapter.getNumberOfUnreadItems(); + final LongIntMap result = adapter.getNumberOfUnreadFeedItems(feedIds); adapter.close(); return result; } @@ -960,9 +1128,31 @@ public final class DBReader { PodDBAdapter adapter = new PodDBAdapter(context); adapter.open(); List<Feed> feeds = getFeedList(adapter); + long[] feedIds = new long[feeds.size()]; + for(int i=0; i < feeds.size(); i++) { + feedIds[i] = feeds.get(i).getId(); + } + final LongIntMap numUnreadFeedItems = adapter.getNumberOfUnreadFeedItems(feedIds); + Collections.sort(feeds, new Comparator<Feed>() { + @Override + public int compare(Feed lhs, Feed rhs) { + long numUnreadLhs = numUnreadFeedItems.get(lhs.getId()); + Log.d(TAG, "feed with id " + lhs.getId() + " has " + numUnreadLhs + " unread items"); + long numUnreadRhs = numUnreadFeedItems.get(rhs.getId()); + Log.d(TAG, "feed with id " + rhs.getId() + " has " + numUnreadRhs + " unread items"); + if(numUnreadLhs > numUnreadRhs) { + // reverse natural order: podcast with most unplayed episodes first + return -1; + } else if(numUnreadLhs == numUnreadRhs) { + return lhs.getTitle().compareTo(rhs.getTitle()); + } else { + return 1; + } + } + }); int queueSize = adapter.getQueueSize(); - int numUnreadItems = adapter.getNumberOfUnreadItems(); - NavDrawerData result = new NavDrawerData(feeds, queueSize, numUnreadItems); + int numNewItems = adapter.getNumberOfNewItems(); + NavDrawerData result = new NavDrawerData(feeds, queueSize, numNewItems, numUnreadFeedItems); adapter.close(); return result; } @@ -970,12 +1160,15 @@ public final class DBReader { public static class NavDrawerData { public List<Feed> feeds; public int queueSize; - public int numUnreadItems; + public int numNewItems; + public LongIntMap numUnreadFeedItems; - public NavDrawerData(List<Feed> feeds, int queueSize, int numUnreadItems) { + public NavDrawerData(List<Feed> feeds, int queueSize, int numNewItems, + LongIntMap numUnreadFeedItems) { this.feeds = feeds; this.queueSize = queueSize; - this.numUnreadItems = numUnreadItems; + this.numNewItems = numNewItems; + this.numUnreadFeedItems = numUnreadFeedItems; } } } 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 9fa17bf72..e570ee709 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 @@ -20,7 +20,6 @@ import java.util.concurrent.FutureTask; import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicBoolean; -import de.danoeh.antennapod.core.BuildConfig; import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.asynctask.FlattrClickWorker; import de.danoeh.antennapod.core.asynctask.FlattrStatusFetcher; @@ -171,10 +170,10 @@ public final class DBTasks { isRefreshing.set(false); if (FlattrUtils.hasToken()) { - if (BuildConfig.DEBUG) Log.d(TAG, "Flattring all pending things."); + Log.d(TAG, "Flattring all pending things."); new FlattrClickWorker(context).executeAsync(); // flattr pending things - if (BuildConfig.DEBUG) Log.d(TAG, "Fetching flattr status."); + Log.d(TAG, "Fetching flattr status."); new FlattrStatusFetcher(context).start(); } @@ -185,9 +184,7 @@ public final class DBTasks { } }.start(); } else { - if (BuildConfig.DEBUG) - Log.d(TAG, - "Ignoring request to refresh all feeds: Refresh lock is locked"); + Log.d(TAG, "Ignoring request to refresh all feeds: Refresh lock is locked"); } } @@ -223,8 +220,7 @@ public final class DBTasks { * @param context Used for DB access. */ public static void refreshExpiredFeeds(final Context context) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Refreshing expired feeds"); + Log.d(TAG, "Refreshing expired feeds"); new Thread() { public void run() { @@ -306,6 +302,7 @@ public final class DBTasks { */ public static void refreshFeed(Context context, Feed feed) throws DownloadRequestException { + Log.d(TAG, "id " + feed.getId()); refreshFeed(context, feed, false); } @@ -428,25 +425,6 @@ public final class DBTasks { } } - static int getNumberOfUndownloadedEpisodes( - final List<FeedItem> queue, final List<FeedItem> unreadItems) { - int counter = 0; - for (FeedItem item : queue) { - if (item.hasMedia() && !item.getMedia().isDownloaded() - && !item.getMedia().isPlaying() - && item.getFeed().getPreferences().getAutoDownload()) { - counter++; - } - } - for (FeedItem item : unreadItems) { - if (item.hasMedia() && !item.getMedia().isDownloaded() - && item.getFeed().getPreferences().getAutoDownload()) { - counter++; - } - } - return counter; - } - /** * Looks for undownloaded episodes in the queue or list of unread items and request a download if * 1. Network is available @@ -479,14 +457,6 @@ public final class DBTasks { } /** - * Adds all FeedItem objects whose 'read'-attribute is false to the queue in a separate thread. - */ - public static void enqueueAllNewItems(final Context context) { - long[] unreadItems = DBReader.getUnreadItemIds(context); - DBWriter.addQueueItem(context, unreadItems); - } - - /** * Returns the successor of a FeedItem in the queue. * * @param context Used for accessing the DB. @@ -560,7 +530,7 @@ public final class DBTasks { /** * Adds new Feeds to the database or updates the old versions if they already exists. If another Feed with the same * identifying value already exists, this method will add new FeedItems from the new Feed to the existing Feed. - * These FeedItems will be marked as unread. + * These FeedItems will be marked as unread with the exception of the most recent FeedItem. * <p/> * This method can update multiple feeds at once. Submitting a feed twice in the same method call can result in undefined behavior. * <p/> @@ -586,17 +556,20 @@ public final class DBTasks { final Feed savedFeed = searchFeedByIdentifyingValueOrID(context, adapter, newFeed); if (savedFeed == null) { - if (BuildConfig.DEBUG) - Log.d(TAG, - "Found no existing Feed with title " - + newFeed.getTitle() + ". Adding as new one." - ); + Log.d(TAG, "Found no existing Feed with title " + + newFeed.getTitle() + ". Adding as new one."); + // Add a new Feed + // all new feeds will have the most recent item marked as unplayed + FeedItem mostRecent = newFeed.getMostRecentItem(); + if (mostRecent != null) { + mostRecent.setRead(false); + } + newFeedsList.add(newFeed); resultFeeds[feedIdx] = newFeed; } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "Feed with title " + newFeed.getTitle() + Log.d(TAG, "Feed with title " + newFeed.getTitle() + " already exists. Syncing new with existing one."); Collections.sort(newFeed.getItems(), new FeedItemPubdateComparator()); @@ -604,21 +577,17 @@ public final class DBTasks { final boolean markNewItemsAsUnread; if (newFeed.getPageNr() == savedFeed.getPageNr()) { if (savedFeed.compareWithOther(newFeed)) { - if (BuildConfig.DEBUG) - Log.d(TAG, - "Feed has updated attribute values. Updating old feed's attributes"); + Log.d(TAG, "Feed has updated attribute values. Updating old feed's attributes"); savedFeed.updateFromOther(newFeed); } markNewItemsAsUnread = true; } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "New feed has a higher page number. Merging without marking as unread"); + Log.d(TAG, "New feed has a higher page number. Merging without marking as unread"); markNewItemsAsUnread = false; savedFeed.setNextPageLink(newFeed.getNextPageLink()); } if (savedFeed.getPreferences().compareWithOther(newFeed.getPreferences())) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Feed has updated preferences. Updating old feed's preferences"); + Log.d(TAG, "Feed has updated preferences. Updating old feed's preferences"); savedFeed.getPreferences().updateFromOther(newFeed.getPreferences()); } // Look for new or updated Items @@ -630,6 +599,7 @@ public final class DBTasks { // item is new final int i = idx; item.setFeed(savedFeed); + item.setAutoDownload(savedFeed.getPreferences().getAutoDownload()); savedFeed.getItems().add(i, item); if (markNewItemsAsUnread) { item.setRead(false); @@ -641,6 +611,7 @@ public final class DBTasks { // update attributes savedFeed.setLastUpdate(newFeed.getLastUpdate()); savedFeed.setType(newFeed.getType()); + savedFeed.setLastUpdateFailed(false); updatedFeedsList.add(savedFeed); resultFeeds[feedIdx] = savedFeed; 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 bd0cfee5b..fe5d0dfd3 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 @@ -29,6 +29,7 @@ import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.asynctask.FlattrClickWorker; import de.danoeh.antennapod.core.feed.EventDistributor; import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedEvent; import de.danoeh.antennapod.core.feed.FeedImage; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; @@ -353,30 +354,16 @@ public class DBWriter { FeedItem item = null; if (queue != null) { - boolean queueModified = false; - boolean unreadItemsModified = false; - if (!itemListContains(queue, itemId)) { item = DBReader.getFeedItem(context, itemId); if (item != null) { queue.add(index, item); - queueModified = true; - if (!item.isRead()) { - item.setRead(true); - unreadItemsModified = true; - } + adapter.setQueue(queue); + EventBus.getDefault().post(new QueueEvent(QueueEvent.Action.ADDED, item, index)); } } - if (queueModified) { - adapter.setQueue(queue); - EventBus.getDefault().post(new QueueEvent(QueueEvent.Action.ADDED, item, index)); - } - if (unreadItemsModified && item != null) { - adapter.setSingleFeedItem(item); - EventDistributor.getInstance() - .sendUnreadItemsUpdateBroadcast(); - } } + adapter.close(); if (performAutoDownload) { DBTasks.autodownloadUndownloadedItems(context); @@ -417,21 +404,15 @@ public class DBWriter { if (item != null) { // add item to either front ot back of queue - boolean addToFront = PreferenceManager.getDefaultSharedPreferences(context) - .getBoolean(UserPreferences.PREF_QUEUE_ADD_TO_FRONT, false); + boolean addToFront = UserPreferences.enqueueAtFront(); if(addToFront){ queue.add(0, item); - }else{ + } else { queue.add(item); } queueModified = true; - if (!item.isRead()) { - item.setRead(true); - itemsToSave.add(item); - unreadItemsModified = true; - } } } } @@ -439,11 +420,6 @@ public class DBWriter { adapter.setQueue(queue); EventBus.getDefault().post(new QueueEvent(QueueEvent.Action.ADDED_ITEMS, queue)); } - if (unreadItemsModified) { - adapter.setFeedItemlist(itemsToSave); - EventDistributor.getInstance() - .sendUnreadItemsUpdateBroadcast(); - } } adapter.close(); DBTasks.autodownloadUndownloadedItems(context); @@ -608,8 +584,7 @@ public class DBWriter { adapter.setQueue(queue); if (broadcastUpdate) { - EventBus.getDefault().post(new QueueEvent(QueueEvent.Action.REMOVED, item)); - EventBus.getDefault().post(new QueueEvent(QueueEvent.Action.ADDED, item, to)); + EventBus.getDefault().post(new QueueEvent(QueueEvent.Action.MOVED, item, to)); } } @@ -937,6 +912,26 @@ public class DBWriter { } /** + * Saves if a feed's last update failed + * + * @param lastUpdateFailed true if last update failed + */ + public static Future<?> setFeedLastUpdateFailed(final Context context, + final long feedId, + final boolean lastUpdateFailed) { + return dbExec.submit(new Runnable() { + + @Override + public void run() { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.setFeedLastUpdateFailed(feedId, lastUpdateFailed); + adapter.close(); + } + }); + } + + /** * format an url for querying the database * (postfix a / and apply percent-encoding) */ @@ -1040,4 +1035,52 @@ public class DBWriter { } }); } + + /** + * Sets the 'auto_download'-attribute of specific FeedItem. + * + * @param context A context that is used for opening a database connection. + * @param feedItem FeedItem. + */ + public static Future<?> setFeedItemAutoDownload(final Context context, final FeedItem feedItem, + final boolean autoDownload) { + Log.d(TAG, "FeedItem[id=" + feedItem.getId() + "] SET auto_download " + autoDownload); + return dbExec.submit(new Runnable() { + + @Override + public void run() { + final PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.setFeedItemAutoDownload(feedItem, autoDownload); + adapter.close(); + + EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast(); + } + }); + + } + + /** + * Set filter of the feed + * + * @param context Used for opening a database connection. + * @param feedId The feed's ID + * @param filterValues Values that represent properties to filter by + */ + public static Future<?> setFeedItemsFilter(final Context context, final long feedId, + final List<String> filterValues) { + Log.d(TAG, "setFeedFilter"); + + return dbExec.submit(new Runnable() { + @Override + public void run() { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.setFeedItemFilter(feedId, filterValues); + adapter.close(); + EventBus.getDefault().post(new FeedEvent(FeedEvent.Action.FILTER_CHANGED, feedId)); + } + }); + } + } 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 f518a4f5f..4780098e0 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 @@ -11,6 +11,7 @@ import android.database.sqlite.SQLiteDatabase.CursorFactory; import android.database.sqlite.SQLiteOpenHelper; import android.util.Log; +import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; import java.util.Arrays; @@ -26,8 +27,11 @@ 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.service.download.DownloadStatus; +import de.danoeh.antennapod.core.util.LongIntMap; import de.danoeh.antennapod.core.util.flattr.FlattrStatus; +; + // TODO Remove media column from feeditem table /** @@ -149,6 +153,8 @@ public class PodDBAdapter { public static final String KEY_PASSWORD = "password"; public static final String KEY_IS_PAGED = "is_paged"; public static final String KEY_NEXT_PAGE_LINK = "next_page_link"; + public static final String KEY_HIDE = "hide"; + public static final String KEY_LAST_UPDATE_FAILED = "last_update_failed"; // Table names public static final String TABLE_NAME_FEEDS = "Feeds"; @@ -175,8 +181,9 @@ public class PodDBAdapter { + KEY_USERNAME + " TEXT," + KEY_PASSWORD + " TEXT," + KEY_IS_PAGED + " INTEGER DEFAULT 0," - + KEY_NEXT_PAGE_LINK + " TEXT)"; - + + KEY_NEXT_PAGE_LINK + " TEXT," + + KEY_HIDE + " TEXT," + + KEY_LAST_UPDATE_FAILED + " INTEGER DEFAULT 0)"; public static final String CREATE_TABLE_FEED_ITEMS = "CREATE TABLE " + TABLE_NAME_FEED_ITEMS + " (" + TABLE_PRIMARY_KEY + KEY_TITLE @@ -186,7 +193,8 @@ public class PodDBAdapter { + KEY_MEDIA + " INTEGER," + KEY_FEED + " INTEGER," + KEY_HAS_CHAPTERS + " INTEGER," + KEY_ITEM_IDENTIFIER + " TEXT," + KEY_FLATTR_STATUS + " INTEGER," - + KEY_IMAGE + " INTEGER)"; + + KEY_IMAGE + " INTEGER," + + KEY_AUTO_DOWNLOAD + " INTEGER)"; public static final String CREATE_TABLE_FEED_IMAGES = "CREATE TABLE " + TABLE_NAME_FEED_IMAGES + " (" + TABLE_PRIMARY_KEY + KEY_TITLE @@ -200,7 +208,8 @@ public class PodDBAdapter { + " INTEGER," + KEY_SIZE + " INTEGER," + KEY_MIME_TYPE + " TEXT," + KEY_PLAYBACK_COMPLETION_DATE + " INTEGER," + KEY_FEEDITEM + " INTEGER," - + KEY_PLAYED_DURATION + " INTEGER)"; + + KEY_PLAYED_DURATION + " INTEGER," + + KEY_AUTO_DOWNLOAD + " INTEGER)"; public static final String CREATE_TABLE_DOWNLOAD_LOG = "CREATE TABLE " + TABLE_NAME_DOWNLOAD_LOG + " (" + TABLE_PRIMARY_KEY + KEY_FEEDFILE @@ -218,6 +227,28 @@ public class PodDBAdapter { + " TEXT," + KEY_START + " INTEGER," + KEY_FEEDITEM + " INTEGER," + KEY_LINK + " TEXT," + KEY_CHAPTER_TYPE + " INTEGER)"; + // SQL Statements for creating indexes + public static final String CREATE_INDEX_FEEDITEMS_FEED = "CREATE INDEX " + + TABLE_NAME_FEED_ITEMS + "_" + KEY_FEED + " ON " + TABLE_NAME_FEED_ITEMS + " (" + + KEY_FEED + ")"; + + public static final String CREATE_INDEX_FEEDITEMS_IMAGE = "CREATE INDEX " + + TABLE_NAME_FEED_ITEMS + "_" + KEY_IMAGE + " ON " + TABLE_NAME_FEED_ITEMS + " (" + + KEY_IMAGE + ")"; + + public static final String CREATE_INDEX_QUEUE_FEEDITEM = "CREATE INDEX " + + TABLE_NAME_QUEUE + "_" + KEY_FEEDITEM + " ON " + TABLE_NAME_QUEUE + " (" + + KEY_FEEDITEM + ")"; + + public static final String CREATE_INDEX_FEEDMEDIA_FEEDITEM = "CREATE INDEX " + + TABLE_NAME_FEED_MEDIA + "_" + KEY_FEEDITEM + " ON " + TABLE_NAME_FEED_MEDIA + " (" + + KEY_FEEDITEM + ")"; + + public static final String CREATE_INDEX_SIMPLECHAPTERS_FEEDITEM = "CREATE INDEX " + + TABLE_NAME_SIMPLECHAPTERS + "_" + KEY_FEEDITEM + " ON " + TABLE_NAME_SIMPLECHAPTERS + " (" + + KEY_FEEDITEM + ")"; + + private SQLiteDatabase db; private final Context context; private PodDBHelper helper; @@ -246,6 +277,8 @@ public class PodDBAdapter { TABLE_NAME_FEEDS + "." + KEY_NEXT_PAGE_LINK, TABLE_NAME_FEEDS + "." + KEY_USERNAME, TABLE_NAME_FEEDS + "." + KEY_PASSWORD, + TABLE_NAME_FEEDS + "." + KEY_HIDE, + TABLE_NAME_FEEDS + "." + KEY_LAST_UPDATE_FAILED, }; // column indices for FEED_SEL_STD @@ -270,7 +303,6 @@ public class PodDBAdapter { public static final int IDX_FEED_SEL_PREFERENCES_USERNAME = 18; public static final int IDX_FEED_SEL_PREFERENCES_PASSWORD = 19; - /** * Select all columns from the feeditems-table except description and * content-encoded. @@ -286,7 +318,9 @@ public class PodDBAdapter { TABLE_NAME_FEED_ITEMS + "." + KEY_HAS_CHAPTERS, TABLE_NAME_FEED_ITEMS + "." + KEY_ITEM_IDENTIFIER, TABLE_NAME_FEED_ITEMS + "." + KEY_FLATTR_STATUS, - TABLE_NAME_FEED_ITEMS + "." + KEY_IMAGE}; + TABLE_NAME_FEED_ITEMS + "." + KEY_IMAGE, + TABLE_NAME_FEED_ITEMS + "." + KEY_AUTO_DOWNLOAD + }; /** * Contains FEEDITEM_SEL_FI_SMALL as comma-separated list. Useful for raw queries. @@ -400,17 +434,20 @@ public class PodDBAdapter { values.put(KEY_FLATTR_STATUS, feed.getFlattrStatus().toLong()); values.put(KEY_IS_PAGED, feed.isPaged()); values.put(KEY_NEXT_PAGE_LINK, feed.getNextPageLink()); + if(feed.getItemFilter() != null && feed.getItemFilter().getValues().length > 0) { + values.put(KEY_HIDE, StringUtils.join(feed.getItemFilter().getValues(), ",")); + } else { + values.put(KEY_HIDE, ""); + } + values.put(KEY_LAST_UPDATE_FAILED, feed.hasLastUpdateFailed()); if (feed.getId() == 0) { // Create new entry - if (BuildConfig.DEBUG) - Log.d(this.toString(), "Inserting new Feed into db"); + Log.d(this.toString(), "Inserting new Feed into db"); feed.setId(db.insert(TABLE_NAME_FEEDS, null, values)); } else { - if (BuildConfig.DEBUG) - Log.d(this.toString(), "Updating existing Feed in db"); + Log.d(this.toString(), "Updating existing Feed in db"); db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?", new String[]{String.valueOf(feed.getId())}); - } return feed.getId(); } @@ -426,6 +463,13 @@ public class PodDBAdapter { db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?", new String[]{String.valueOf(prefs.getFeedID())}); } + public void setFeedItemFilter(long feedId, List<String> filterValues) { + ContentValues values = new ContentValues(); + values.put(KEY_HIDE, StringUtils.join(filterValues, ",")); + Log.d(TAG, StringUtils.join(filterValues, ",")); + db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?", new String[]{String.valueOf(feedId)}); + } + /** * Inserts or updates an image entry * @@ -696,6 +740,7 @@ public class PodDBAdapter { values.put(KEY_HAS_CHAPTERS, item.getChapters() != null || item.hasChapters()); values.put(KEY_ITEM_IDENTIFIER, item.getItemIdentifier()); values.put(KEY_FLATTR_STATUS, item.getFlattrStatus().toLong()); + values.put(KEY_AUTO_DOWNLOAD, item.getAutoDownload()); if (item.hasItemImage()) { if (item.getImage().getId() == 0) { setImage(item.getImage()); @@ -766,6 +811,13 @@ public class PodDBAdapter { } } + public void setFeedLastUpdateFailed(long feedId, boolean failed) { + final String sql = "UPDATE " + TABLE_NAME_FEEDS + + " SET " + KEY_LAST_UPDATE_FAILED+ "=" + (failed ? "1" : "0") + + " WHERE " + KEY_ID + "="+ feedId; + db.execSQL(sql); + } + /** * Inserts or updates a download status. */ @@ -787,6 +839,13 @@ public class PodDBAdapter { return status.getId(); } + public void setFeedItemAutoDownload(FeedItem feedItem, boolean autoDownload) { + ContentValues values = new ContentValues(); + values.put(KEY_AUTO_DOWNLOAD, autoDownload); + db.update(TABLE_NAME_FEED_ITEMS, values, KEY_ID + "=?", + new String[] { String.valueOf(feedItem.getId()) } ); + } + public long getDownloadLogSize() { final String query = String.format("SELECT COUNT(%s) FROM %s", KEY_ID, TABLE_NAME_DOWNLOAD_LOG); Cursor result = db.rawQuery(query, null); @@ -977,6 +1036,14 @@ public class PodDBAdapter { return c; } + public final Cursor getDownloadLog(final int feedFileType, final long feedFileId) { + final String query = "SELECT * FROM " + TABLE_NAME_DOWNLOAD_LOG + + " WHERE " + KEY_FEEDFILE + "=" + feedFileId + " AND " + KEY_FEEDFILETYPE + "=" + feedFileType + + " ORDER BY " + KEY_ID + " DESC"; + Cursor c = db.rawQuery(query, null); + return c; + } + public final Cursor getDownloadLogCursor(final int limit) { Cursor c = db.query(TABLE_NAME_DOWNLOAD_LOG, null, null, null, null, null, KEY_COMPLETION_DATE + " DESC LIMIT " + limit); @@ -1021,11 +1088,43 @@ public class PodDBAdapter { return c; } - public final Cursor getUnreadItemIdsCursor() { - Cursor c = db.query(TABLE_NAME_FEED_ITEMS, new String[]{KEY_ID}, - KEY_READ + "=0", null, null, null, KEY_PUBDATE + " DESC"); - return c; + public final Cursor getNewItemIdsCursor() { + final String query = "SELECT " + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + + " FROM " + TABLE_NAME_FEED_ITEMS + + " INNER JOIN " + TABLE_NAME_FEED_MEDIA + " ON " + + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + "=" + + TABLE_NAME_FEED_MEDIA + "." + KEY_FEEDITEM + + " LEFT OUTER JOIN " + TABLE_NAME_QUEUE + " ON " + + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + "=" + + TABLE_NAME_QUEUE + "." + KEY_FEEDITEM + + " WHERE " + + TABLE_NAME_FEED_ITEMS + "." + KEY_READ + " = 0 AND " // unplayed + + TABLE_NAME_FEED_MEDIA + "." + KEY_DOWNLOADED + " = 0 AND " // undownloaded + + TABLE_NAME_FEED_MEDIA + "." + KEY_POSITION + " = 0 AND " // not partially played + + TABLE_NAME_QUEUE + "." + KEY_ID + " IS NULL"; // not in queue + return db.rawQuery(query, null); + } + /** + * Returns a cursor which contains all feed items that are considered new. + * The returned cursor uses the FEEDITEM_SEL_FI_SMALL selection. + */ + public final Cursor getNewItemsCursor() { + final String query = "SELECT " + SEL_FI_SMALL_STR + " FROM " + TABLE_NAME_FEED_ITEMS + + " INNER JOIN " + TABLE_NAME_FEED_MEDIA + " ON " + + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + "=" + + TABLE_NAME_FEED_MEDIA + "." + KEY_FEEDITEM + + " LEFT OUTER JOIN " + TABLE_NAME_QUEUE + " ON " + + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + "=" + + TABLE_NAME_QUEUE + "." + KEY_FEEDITEM + + " WHERE " + + TABLE_NAME_FEED_ITEMS + "." + KEY_READ + " = 0 AND " // unplayed + + TABLE_NAME_FEED_MEDIA + "." + KEY_DOWNLOADED + " = 0 AND " // undownloaded + + TABLE_NAME_FEED_MEDIA + "." + KEY_POSITION + " = 0 AND " // not partially played + + TABLE_NAME_QUEUE + "." + KEY_ID + " IS NULL" // not in queue + + " ORDER BY " + KEY_PUBDATE + " DESC"; + Cursor c = db.rawQuery(query, null); + return c; } public final Cursor getRecentlyPublishedItemsCursor(int limit) { @@ -1121,7 +1220,7 @@ public class PodDBAdapter { } public final Cursor getFeedItemCursor(final String id) { - return getFeedItemCursor(new String[] { id }); + return getFeedItemCursor(new String[]{id}); } public final Cursor getFeedItemCursor(final String[] ids) { @@ -1146,6 +1245,20 @@ public class PodDBAdapter { return db.rawQuery(query, null); } + public Cursor getImageAuthenticationCursor(final String imageUrl) { + final String query = "SELECT " + KEY_USERNAME + "," + KEY_PASSWORD + " FROM " + + TABLE_NAME_FEED_IMAGES + " INNER JOIN " + TABLE_NAME_FEEDS + " ON " + + TABLE_NAME_FEED_IMAGES + "." + KEY_ID + "=" + TABLE_NAME_FEEDS + "." + KEY_IMAGE + " WHERE " + + TABLE_NAME_FEED_IMAGES + "." + KEY_DOWNLOAD_URL + "='" + imageUrl + "' UNION SELECT " + + KEY_USERNAME + "," + KEY_PASSWORD + " FROM " + TABLE_NAME_FEED_IMAGES + " INNER JOIN " + + TABLE_NAME_FEED_ITEMS + " ON " + TABLE_NAME_FEED_IMAGES + "." + KEY_ID + "=" + + TABLE_NAME_FEED_ITEMS + "." + KEY_IMAGE + " INNER JOIN " + TABLE_NAME_FEEDS + " ON " + + TABLE_NAME_FEED_ITEMS + "." + KEY_FEED + "=" + TABLE_NAME_FEEDS + "." + KEY_ID + " WHERE " + + TABLE_NAME_FEED_IMAGES + "." + KEY_DOWNLOAD_URL + "='" + imageUrl + "'"; + Log.d(TAG, "Query: " + query); + return db.rawQuery(query, null); + } + public int getQueueSize() { final String query = String.format("SELECT COUNT(%s) FROM %s", KEY_ID, TABLE_NAME_QUEUE); Cursor c = db.rawQuery(query, null); @@ -1157,9 +1270,20 @@ public class PodDBAdapter { return result; } - public final int getNumberOfUnreadItems() { - final String query = "SELECT COUNT(DISTINCT " + KEY_ID + ") AS count FROM " + TABLE_NAME_FEED_ITEMS + - " WHERE " + KEY_READ + " = 0"; + public final int getNumberOfNewItems() { + final String query = "SELECT COUNT(" + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + ")" + +" FROM " + TABLE_NAME_FEED_ITEMS + + " LEFT JOIN " + TABLE_NAME_FEED_MEDIA + " ON " + + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + "=" + + TABLE_NAME_FEED_MEDIA + "." + KEY_FEEDITEM + + " LEFT JOIN " + TABLE_NAME_QUEUE + " ON " + + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + "=" + + TABLE_NAME_QUEUE + "." + KEY_FEEDITEM + + " WHERE " + + TABLE_NAME_FEED_ITEMS + "." + KEY_READ + " = 0 AND " // unplayed + + TABLE_NAME_FEED_MEDIA + "." + KEY_DOWNLOADED + " = 0 AND " // undownloaded + + TABLE_NAME_FEED_MEDIA + "." + KEY_POSITION + " = 0 AND " // not partially played + + TABLE_NAME_QUEUE + "." + KEY_ID + " IS NULL"; // not in queue Cursor c = db.rawQuery(query, null); int result = 0; if (c.moveToFirst()) { @@ -1169,6 +1293,25 @@ public class PodDBAdapter { return result; } + public final LongIntMap getNumberOfUnreadFeedItems(long... feedIds) { + final String query = "SELECT " + KEY_FEED + ", COUNT(" + KEY_ID + ") AS count " + + " FROM " + TABLE_NAME_FEED_ITEMS + + " WHERE " + KEY_FEED + " IN (" + StringUtils.join(feedIds, ',') + ") " + + " AND " + KEY_READ + " = 0" + + " GROUP BY " + KEY_FEED; + Cursor c = db.rawQuery(query, null); + LongIntMap result = new LongIntMap(c.getCount()); + if (c.moveToFirst()) { + do { + long feedId = c.getLong(0); + int count = c.getInt(1); + result.put(feedId, count); + } while(c.moveToNext()); + } + c.close(); + return result; + } + public final int getNumberOfDownloadedEpisodes() { final String query = "SELECT COUNT(DISTINCT " + KEY_ID + ") AS count FROM " + TABLE_NAME_FEED_MEDIA + " WHERE " + KEY_DOWNLOADED + " > 0"; @@ -1330,6 +1473,13 @@ public class PodDBAdapter { db.execSQL(CREATE_TABLE_DOWNLOAD_LOG); db.execSQL(CREATE_TABLE_QUEUE); db.execSQL(CREATE_TABLE_SIMPLECHAPTERS); + + db.execSQL(CREATE_INDEX_FEEDITEMS_FEED); + db.execSQL(CREATE_INDEX_FEEDITEMS_IMAGE); + db.execSQL(CREATE_INDEX_FEEDMEDIA_FEEDITEM); + db.execSQL(CREATE_INDEX_QUEUE_FEEDITEM); + db.execSQL(CREATE_INDEX_SIMPLECHAPTERS_FEEDITEM); + } @Override diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/DateUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/DateUtils.java index 6622eab73..b6df2dc85 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/DateUtils.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/DateUtils.java @@ -1,5 +1,7 @@ package de.danoeh.antennapod.core.util; +import android.util.Log; + import org.apache.commons.lang3.StringUtils; import java.text.ParsePosition; @@ -11,45 +13,14 @@ import java.util.Locale; * Parses several date formats. */ public class DateUtils { - private static final String TAG = "DateUtils"; - - private static final String[] RFC822DATES = {"dd MMM yy HH:mm:ss Z", - "dd MMM yy HH:mm Z"}; - - /** - * RFC 3339 date format for UTC dates. - */ - public static final String RFC3339UTC = "yyyy-MM-dd'T'HH:mm:ss'Z'"; - - /** - * RFC 3339 date format for localtime dates with offset. - */ - public static final String RFC3339LOCAL = "yyyy-MM-dd'T'HH:mm:ssZ"; + + private static final String TAG = "DateUtils"; - public static final String ISO8601_SHORT = "yyyy-MM-dd"; - - private static ThreadLocal<SimpleDateFormat> RFC822Formatter = new ThreadLocal<SimpleDateFormat>() { - @Override - protected SimpleDateFormat initialValue() { - return new SimpleDateFormat("dd MMM yy HH:mm:ss Z", Locale.US); - } - - }; - - private static ThreadLocal<SimpleDateFormat> RFC3339Formatter = new ThreadLocal<SimpleDateFormat>() { - @Override - protected SimpleDateFormat initialValue() { - return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US); - } - - }; - - public static Date parse(String date) { - if(date == null) { + public static Date parse(final String input) { + if(input == null) { throw new IllegalArgumentException("Date most not be null"); } - date = date.replace('/', ' '); - date = date.replace('-', ' '); + String date = input.replace('/', '-'); if(date.contains(".")) { int start = date.indexOf('.'); int current = start+1; @@ -75,15 +46,16 @@ public class DateUtils { "dd MMM yy HH:mm:ss Z", "dd MMM yy HH:mm Z", "EEE, dd MMM yyyy HH:mm:ss Z", + "EEE, dd MMMM yyyy HH:mm:ss Z", "EEEE, dd MMM yy HH:mm:ss Z", "EEE MMM d HH:mm:ss yyyy", - "yyyy MM dd'T'HH:mm:ss", - "yyyy MM dd'T'HH:mm:ss.SSS", - "yyyy MM dd'T'HH:mm:ss.SSS Z", - "yyyy MM dd'T'HH:mm:ssZ", - "yyyy MM dd'T'HH:mm:ss'Z'", - "yyyy MM ddZ", - "yyyy MM dd" + "yyyy-MM-dd'T'HH:mm:ss", + "yyyy-MM-dd'T'HH:mm:ss.SSS", + "yyyy-MM-dd'T'HH:mm:ss.SSS Z", + "yyyy-MM-dd'T'HH:mm:ssZ", + "yyyy-MM-dd'T'HH:mm:ss'Z'", + "yyyy-MM-ddZ", + "yyyy-MM-dd" }; SimpleDateFormat parser = new SimpleDateFormat("", Locale.US); parser.setLenient(false); @@ -96,6 +68,8 @@ public class DateUtils { return result; } } + + Log.d(TAG, "Could not parse date string \"" + input + "\""); return null; } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/EpisodeFilter.java b/core/src/main/java/de/danoeh/antennapod/core/util/EpisodeFilter.java index 4c23b161b..029e7fe84 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/EpisodeFilter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/EpisodeFilter.java @@ -1,11 +1,12 @@ package de.danoeh.antennapod.core.util; -import de.danoeh.antennapod.core.feed.FeedItem; - import java.util.ArrayList; import java.util.List; +import de.danoeh.antennapod.core.feed.FeedItem; + public class EpisodeFilter { + private EpisodeFilter() { } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/IntList.java b/core/src/main/java/de/danoeh/antennapod/core/util/IntList.java new file mode 100644 index 000000000..673c81235 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/IntList.java @@ -0,0 +1,240 @@ +package de.danoeh.antennapod.core.util; + +import java.util.Arrays; + +/** + * Fast and memory efficient int list + */ +public final class IntList { + + private int[] values; + protected int size; + + /** + * Constructs an empty instance with a default initial capacity. + */ + public IntList() { + this(4); + } + + /** + * Constructs an empty instance. + * + * @param initialCapacity {@code >= 0;} initial capacity of the list + */ + public IntList(int initialCapacity) { + if(initialCapacity < 0) { + throw new IllegalArgumentException("initial capacity must be 0 or higher"); + } + values = new int[initialCapacity]; + size = 0; + } + + @Override + public int hashCode() { + int hashCode = 1; + for (int i = 0; i < size; i++) { + int value = values[i]; + hashCode = 31 * hashCode + (int)(value ^ (value >>> 32)); + } + return hashCode; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (! (other instanceof IntList)) { + return false; + } + IntList otherList = (IntList) other; + if (size != otherList.size) { + return false; + } + for (int i = 0; i < size; i++) { + if (values[i] != otherList.values[i]) { + return false; + } + } + return true; + } + + @Override + public String toString() { + StringBuffer sb = new StringBuffer(size * 5 + 10); + sb.append("IntList{"); + for (int i = 0; i < size; i++) { + if (i != 0) { + sb.append(", "); + } + sb.append(values[i]); + } + sb.append("}"); + return sb.toString(); + } + + /** + * Gets the number of elements in this list. + */ + public int size() { + return size; + } + + /** + * Gets the indicated value. + * + * @param n {@code >= 0, < size();} which element + * @return the indicated element's value + */ + public int get(int n) { + if (n >= size) { + throw new IndexOutOfBoundsException("n >= size()"); + } else if(n < 0) { + throw new IndexOutOfBoundsException("n < 0"); + } + return values[n]; + } + + /** + * Sets the value at the given index. + * + * @param index the index at which to put the specified object. + * @param value the object to add. + * @return the previous element at the index. + */ + public int set(int index, int value) { + if (index >= size) { + throw new IndexOutOfBoundsException("n >= size()"); + } else if(index < 0) { + throw new IndexOutOfBoundsException("n < 0"); + } + int result = values[index]; + values[index] = value; + return result; + } + + /** + * Adds an element to the end of the list. This will increase the + * list's capacity if necessary. + * + * @param value the value to add + */ + public void add(int value) { + growIfNeeded(); + values[size++] = value; + } + + /** + * Inserts element into specified index, moving elements at and above + * that index up one. May not be used to insert at an index beyond the + * current size (that is, insertion as a last element is legal but + * no further). + * + * @param n {@code >= 0, <=size();} index of where to insert + * @param value value to insert + */ + public void insert(int n, int value) { + if (n > size) { + throw new IndexOutOfBoundsException("n > size()"); + } else if(n < 0) { + throw new IndexOutOfBoundsException("n < 0"); + } + + growIfNeeded(); + + System.arraycopy (values, n, values, n+1, size - n); + values[n] = value; + size++; + } + + /** + * Removes value from this list. + * + * @param value value to remove + * return {@code true} if the value was removed, {@code false} otherwise + */ + public boolean remove(int value) { + for (int i = 0; i < size; i++) { + if (values[i] == value) { + size--; + System.arraycopy(values, i+1, values, i, size-i); + return true; + } + } + return false; + } + + /** + * Removes an element at a given index, shifting elements at greater + * indicies down one. + * + * @param index index of element to remove + */ + public void removeIndex(int index) { + if (index >= size) { + throw new IndexOutOfBoundsException("n >= size()"); + } else if(index < 0) { + throw new IndexOutOfBoundsException("n < 0"); + } + size--; + System.arraycopy (values, index + 1, values, index, size - index); + } + + /** + * Increases size of array if needed + */ + private void growIfNeeded() { + if (size == values.length) { + // Resize. + int[] newArray = new int[size * 3 / 2 + 10]; + System.arraycopy(values, 0, newArray, 0, size); + values = newArray; + } + } + + /** + * Returns the index of the given value, or -1 if the value does not + * appear in the list. + * + * @param value value to find + * @return index of value or -1 + */ + public int indexOf(int value) { + for (int i = 0; i < size; i++) { + if (values[i] == value) { + return i; + } + } + return -1; + } + + /** + * Removes all values from this list. + */ + public void clear() { + values = new int[4]; + size = 0; + } + + + /** + * Returns true if the given value is contained in the list + * + * @param value value to look for + * @return {@code true} if this list contains {@code value}, {@code false} otherwise + */ + public boolean contains(int value) { + return indexOf(value) >= 0; + } + + /** + * Returns an array with a copy of this list's values + * + * @return array with a copy of this list's values + */ + public int[] toArray() { + return Arrays.copyOf(values, size); + + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/LongIntMap.java b/core/src/main/java/de/danoeh/antennapod/core/util/LongIntMap.java new file mode 100644 index 000000000..33fd252eb --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/LongIntMap.java @@ -0,0 +1,252 @@ +package de.danoeh.antennapod.core.util; + + +/** + * Fast and memory efficient long to long map + */ +public class LongIntMap { + + private long[] keys; + private int[] values; + private int size; + + /** + * Creates a new LongLongMap containing no mappings. + */ + public LongIntMap() { + this(10); + } + + /** + * Creates a new SparseLongArray containing no mappings that will not + * require any additional memory allocation to store the specified + * number of mappings. If you supply an initial capacity of 0, the + * sparse array will be initialized with a light-weight representation + * not requiring any additional array allocations. + */ + public LongIntMap(int initialCapacity) { + if(initialCapacity < 0) { + throw new IllegalArgumentException("initial capacity must be 0 or higher"); + } + keys = new long[initialCapacity]; + values = new int[initialCapacity]; + size = 0; + } + + /** + * Increases size of array if needed + */ + private void growIfNeeded() { + if (size == keys.length) { + // Resize. + long[] newKeysArray = new long[size * 3 / 2 + 10]; + int[] newValuesArray = new int[size * 3 / 2 + 10]; + System.arraycopy(keys, 0, newKeysArray, 0, size); + System.arraycopy(values, 0, newValuesArray, 0, size); + keys = newKeysArray; + values = newValuesArray; + } + } + + /** + * Gets the long mapped from the specified key, or <code>0</code> + * if no such mapping has been made. + */ + public int get(long key) { + return get(key, 0); + } + + /** + * Gets the long mapped from the specified key, or the specified value + * if no such mapping has been made. + */ + public int get(long key, int valueIfKeyNotFound) { + int index = indexOfKey(key); + if(index >= 0) { + return values[index]; + } else { + return valueIfKeyNotFound; + } + } + + /** + * Removes the mapping from the specified key, if there was any. + */ + public boolean delete(long key) { + int index = indexOfKey(key); + + if (index >= 0) { + removeAt(index); + return true; + } else { + return false; + } + } + + /** + * Removes the mapping at the given index. + */ + public void removeAt(int index) { + System.arraycopy(keys, index + 1, keys, index, size - (index + 1)); + System.arraycopy(values, index + 1, values, index, size - (index + 1)); + size--; + } + + /** + * Adds a mapping from the specified key to the specified value, + * replacing the previous mapping from the specified key if there + * was one. + */ + public void put(long key, int value) { + int index = indexOfKey(key); + + if (index >= 0) { + values[index] = value; + } else { + growIfNeeded(); + keys[size] = key; + values[size] = value; + size++; + } + } + + /** + * Returns the number of key-value mappings that this SparseIntArray + * currently stores. + */ + public int size() { + return size; + } + + /** + * Given an index in the range <code>0...size()-1</code>, returns + * the key from the <code>index</code>th key-value mapping that this + * SparseLongArray stores. + * + * <p>The keys corresponding to indices in ascending order are guaranteed to + * be in ascending order, e.g., <code>keyAt(0)</code> will return the + * smallest key and <code>keyAt(size()-1)</code> will return the largest + * key.</p> + */ + public long keyAt(int index) { + if (index >= size) { + throw new IndexOutOfBoundsException("n >= size()"); + } else if(index < 0) { + throw new IndexOutOfBoundsException("n < 0"); + } + return keys[index]; + } + + /** + * Given an index in the range <code>0...size()-1</code>, returns + * the value from the <code>index</code>th key-value mapping that this + * SparseLongArray stores. + * + * <p>The values corresponding to indices in ascending order are guaranteed + * to be associated with keys in ascending order, e.g., + * <code>valueAt(0)</code> will return the value associated with the + * smallest key and <code>valueAt(size()-1)</code> will return the value + * associated with the largest key.</p> + */ + public int valueAt(int index) { + if (index >= size) { + throw new IndexOutOfBoundsException("n >= size()"); + } else if(index < 0) { + throw new IndexOutOfBoundsException("n < 0"); + } + return values[index]; + } + + /** + * Returns the index for which {@link #keyAt} would return the + * specified key, or a negative number if the specified + * key is not mapped. + */ + public int indexOfKey(long key) { + for(int i=0; i < size; i++) { + if(keys[i] == key) { + return i; + } + } + return -1; + } + + /** + * Returns an index for which {@link #valueAt} would return the + * specified key, or a negative number if no keys map to the + * specified value. + * Beware that this is a linear search, unlike lookups by key, + * and that multiple keys can map to the same value and this will + * find only one of them. + */ + public int indexOfValue(long value) { + for (int i = 0; i < size; i++) { + if (values[i] == value) { + return i; + } + } + return -1; + } + + /** + * Removes all key-value mappings from this SparseIntArray. + */ + public void clear() { + keys = new long[10]; + values = new int[10]; + size = 0; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (! (other instanceof LongIntMap)) { + return false; + } + LongIntMap otherMap = (LongIntMap) other; + if (size != otherMap.size) { + return false; + } + for (int i = 0; i < size; i++) { + if (keys[i] != otherMap.keys[i] || + values[i] != otherMap.values[i]) { + return false; + } + } + return true; + } + + @Override + public int hashCode() { + int hashCode = 1; + for (int i = 0; i < size; i++) { + long value = values[i]; + hashCode = 31 * hashCode + (int)(value ^ (value >>> 32)); + } + return hashCode; + } + + @Override + public String toString() { + if (size() <= 0) { + return "LongLongMap{}"; + } + + StringBuilder buffer = new StringBuilder(size * 28); + buffer.append("LongLongMap{"); + for (int i=0; i < size; i++) { + if (i > 0) { + buffer.append(", "); + } + long key = keyAt(i); + buffer.append(key); + buffer.append('='); + long value = valueAt(i); + buffer.append(value); + } + buffer.append('}'); + return buffer.toString(); + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/LongList.java b/core/src/main/java/de/danoeh/antennapod/core/util/LongList.java index f5d0cab0c..8934f3272 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/LongList.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/LongList.java @@ -32,7 +32,6 @@ public final class LongList { @Override public int hashCode() { - Arrays.hashCode(values); int hashCode = 1; for (int i = 0; i < size; i++) { long value = values[i]; 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 b321536a3..3a349e221 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 @@ -6,12 +6,13 @@ import android.net.NetworkInfo; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; import android.util.Log; -import de.danoeh.antennapod.core.BuildConfig; -import de.danoeh.antennapod.core.preferences.UserPreferences; import java.util.Arrays; import java.util.List; +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.preferences.UserPreferences; + public class NetworkUtils { private static final String TAG = "NetworkUtils"; @@ -66,4 +67,18 @@ public class NetworkUtils { NetworkInfo info = cm.getActiveNetworkInfo(); return info != null && info.isConnected(); } + + public static boolean isDownloadAllowed(Context context) { + return UserPreferences.isAllowMobileUpdate() || NetworkUtils.connectedToWifi(context); + } + + public static boolean connectedToWifi(Context context) { + ConnectivityManager connManager = (ConnectivityManager) context + .getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo mWifi = connManager + .getNetworkInfo(ConnectivityManager.TYPE_WIFI); + + return mWifi.isConnected(); + } + } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrUtils.java index 90caad946..50792ae26 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrUtils.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrUtils.java @@ -26,7 +26,6 @@ import java.util.EnumSet; import java.util.List; import java.util.TimeZone; -import de.danoeh.antennapod.core.BuildConfig; import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.R; import de.danoeh.antennapod.core.asynctask.FlattrTokenFetcher; @@ -65,18 +64,15 @@ public class FlattrUtils { private static AccessToken retrieveToken() { if (cachedToken == null) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Retrieving access token"); + Log.d(TAG, "Retrieving access token"); String token = PreferenceManager.getDefaultSharedPreferences( ClientConfig.applicationCallbacks.getApplicationInstance()) .getString(PREF_ACCESS_TOKEN, null); if (token != null) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Found access token. Caching."); + Log.d(TAG, "Found access token. Caching."); cachedToken = new AccessToken(token); } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "No access token found"); + Log.d(TAG, "No access token found"); return null; } } @@ -97,8 +93,7 @@ public class FlattrUtils { } public static void storeToken(AccessToken token) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Storing token"); + Log.d(TAG, "Storing token"); SharedPreferences.Editor editor = PreferenceManager .getDefaultSharedPreferences(ClientConfig.applicationCallbacks.getApplicationInstance()).edit(); if (token != null) { @@ -111,8 +106,7 @@ public class FlattrUtils { } public static void deleteToken() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Deleting flattr token"); + Log.d(TAG, "Deleting flattr token"); storeToken(null); } @@ -169,15 +163,11 @@ public class FlattrUtils { } } - if (BuildConfig.DEBUG) { - Log.d(TAG, "Got my flattrs list of length " + Integer.toString(myFlattrs.size()) + " comparison date" + firstOfMonthDate); - - for (Flattr fl : myFlattrs) { - Thing thing = fl.getThing(); - Log.d(TAG, "Flattr thing: " + fl.getThingId() + " name: " + thing.getTitle() + " url: " + thing.getUrl() + " on: " + fl.getCreated()); - } + Log.d(TAG, "Got my flattrs list of length " + Integer.toString(myFlattrs.size()) + " comparison date" + firstOfMonthDate); + for (Flattr fl : myFlattrs) { + Thing thing = fl.getThing(); + Log.d(TAG, "Flattr thing: " + fl.getThingId() + " name: " + thing.getTitle() + " url: " + thing.getUrl() + " on: " + fl.getCreated()); } - } else { Log.e(TAG, "retrieveFlattrdThings was called with null access token"); } @@ -191,8 +181,7 @@ public class FlattrUtils { } public static void revokeAccessToken(Context context) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Revoking access token"); + Log.d(TAG, "Revoking access token"); deleteToken(); FlattrServiceCreator.deleteFlattrService(); showRevokeDialog(context); diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/gui/UndoBarController.java b/core/src/main/java/de/danoeh/antennapod/core/util/gui/UndoBarController.java index 23d8cf7c7..26c712af3 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/gui/UndoBarController.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/gui/UndoBarController.java @@ -88,7 +88,8 @@ public class UndoBarController<T> { } public void close() { - mHideHandler.post(mHideRunnable); + hideUndoBar(true); + mUndoListener.onHide(mUndoToken); } public void hideUndoBar(boolean immediate) { 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 17c752bb6..a0d12d3e7 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 @@ -32,13 +32,11 @@ import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; -import de.danoeh.antennapod.core.BuildConfig; import de.danoeh.antennapod.core.R; import de.danoeh.antennapod.core.feed.Chapter; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.MediaType; import de.danoeh.antennapod.core.preferences.PlaybackPreferences; -import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.core.service.playback.PlaybackServiceMediaPlayer; import de.danoeh.antennapod.core.service.playback.PlayerStatus; @@ -174,8 +172,7 @@ public abstract class PlaybackController { * as the arguments of the launch intent. */ private void bindToService() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Trying to connect to service"); + Log.d(TAG, "Trying to connect to service"); AsyncTask<Void, Void, Intent> intentLoader = new AsyncTask<Void, Void, Intent>() { @Override protected Intent doInBackground(Void... voids) { @@ -211,8 +208,7 @@ public abstract class PlaybackController { * played media or null if no last played media could be found. */ private Intent getPlayLastPlayedMediaIntent() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Trying to restore last played media"); + Log.d(TAG, "Trying to restore last played media"); SharedPreferences prefs = PreferenceManager .getDefaultSharedPreferences(activity.getApplicationContext()); long currentlyPlayingMedia = PlaybackPreferences @@ -240,8 +236,7 @@ public abstract class PlaybackController { return serviceIntent; } } - if (BuildConfig.DEBUG) - Log.d(TAG, "No last played media found"); + Log.d(TAG, "No last played media found"); return null; } @@ -253,8 +248,7 @@ public abstract class PlaybackController { || (positionObserverFuture != null && positionObserverFuture .isDone()) || positionObserverFuture == null) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Setting up position observer"); + Log.d(TAG, "Setting up position observer"); positionObserver = new MediaPositionObserver(); positionObserverFuture = schedExecutor.scheduleWithFixedDelay( positionObserver, MediaPositionObserver.WAITING_INTERVALL, @@ -266,8 +260,7 @@ public abstract class PlaybackController { private void cancelPositionObserver() { if (positionObserverFuture != null) { boolean result = positionObserverFuture.cancel(true); - if (BuildConfig.DEBUG) - Log.d(TAG, "PositionObserver cancelled. Result: " + result); + Log.d(TAG, "PositionObserver cancelled. Result: " + result); } } @@ -295,8 +288,7 @@ public abstract class PlaybackController { protected BroadcastReceiver statusUpdate = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Received statusUpdate Intent."); + Log.d(TAG, "Received statusUpdate Intent."); if (isConnectedToPlaybackService()) { PlaybackServiceMediaPlayer.PSMPInfo info = playbackService.getPSMPInfo(); status = info.playerStatus; @@ -353,8 +345,7 @@ public abstract class PlaybackController { } } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "Bad arguments. Won't handle intent"); + Log.d(TAG, "Bad arguments. Won't handle intent"); } } else { bindToService(); @@ -425,6 +416,7 @@ public abstract class PlaybackController { pauseResource = R.drawable.ic_av_pause_circle_outline_80dp; } + Log.d(TAG, "status: " + status.toString()); switch (status) { case ERROR: @@ -470,6 +462,7 @@ public abstract class PlaybackController { updatePlayButtonAppearance(playResource, playText); break; case SEEKING: + onPositionObserverUpdate(); postStatusMsg(R.string.player_seeking_msg); break; case INITIALIZED: @@ -505,8 +498,7 @@ public abstract class PlaybackController { * information has to be refreshed */ void queryService() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Querying service info"); + Log.d(TAG, "Querying service info"); if (playbackService != null) { status = playbackService.getStatus(); media = playbackService.getPlayable(); @@ -614,28 +606,6 @@ public abstract class PlaybackController { }; } - public OnClickListener newOnRevButtonClickListener() { - return new OnClickListener() { - @Override - public void onClick(View v) { - if (status == PlayerStatus.PLAYING) { - playbackService.seekDelta(-UserPreferences.getSeekDeltaMs()); - } - } - }; - } - - public OnClickListener newOnFFButtonClickListener() { - return new OnClickListener() { - @Override - public void onClick(View v) { - if (status == PlayerStatus.PLAYING) { - playbackService.seekDelta(UserPreferences.getSeekDeltaMs()); - } - } - }; - } - public boolean serviceAvailable() { return playbackService != null; } diff --git a/core/src/main/res/drawable-hdpi/ic_feed_grey600_24dp.png b/core/src/main/res/drawable-hdpi/ic_feed_grey600_24dp.png Binary files differindex 46be3e14e..0c3bb0757 100755 --- a/core/src/main/res/drawable-hdpi/ic_feed_grey600_24dp.png +++ b/core/src/main/res/drawable-hdpi/ic_feed_grey600_24dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_feed_white_24dp.png b/core/src/main/res/drawable-hdpi/ic_feed_white_24dp.png Binary files differindex 3d57127f5..667300129 100755 --- a/core/src/main/res/drawable-hdpi/ic_feed_white_24dp.png +++ b/core/src/main/res/drawable-hdpi/ic_feed_white_24dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_filter_grey600_24dp.png b/core/src/main/res/drawable-hdpi/ic_filter_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..83c564377 --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_filter_grey600_24dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_filter_white_24dp.png b/core/src/main/res/drawable-hdpi/ic_filter_white_24dp.png Binary files differnew file mode 100644 index 000000000..e3517a57d --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_filter_white_24dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_lock_closed_grey600_24dp.png b/core/src/main/res/drawable-hdpi/ic_lock_closed_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..b6dba1002 --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_lock_closed_grey600_24dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_lock_closed_white_24dp.png b/core/src/main/res/drawable-hdpi/ic_lock_closed_white_24dp.png Binary files differnew file mode 100644 index 000000000..5c60ab08a --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_lock_closed_white_24dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_lock_open_grey600_24dp.png b/core/src/main/res/drawable-hdpi/ic_lock_open_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..a99e9f2b6 --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_lock_open_grey600_24dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_lock_open_white_24dp.png b/core/src/main/res/drawable-hdpi/ic_lock_open_white_24dp.png Binary files differnew file mode 100644 index 000000000..61c623ce2 --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_lock_open_white_24dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_feed_grey600_24dp.png b/core/src/main/res/drawable-mdpi/ic_feed_grey600_24dp.png Binary files differindex 79f082610..d46b325d8 100755 --- a/core/src/main/res/drawable-mdpi/ic_feed_grey600_24dp.png +++ b/core/src/main/res/drawable-mdpi/ic_feed_grey600_24dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_feed_white_24dp.png b/core/src/main/res/drawable-mdpi/ic_feed_white_24dp.png Binary files differindex 15a4b16bf..ac94476c2 100755 --- a/core/src/main/res/drawable-mdpi/ic_feed_white_24dp.png +++ b/core/src/main/res/drawable-mdpi/ic_feed_white_24dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_filter_grey600_24dp.png b/core/src/main/res/drawable-mdpi/ic_filter_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..e1e55d72b --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_filter_grey600_24dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_filter_white_24dp.png b/core/src/main/res/drawable-mdpi/ic_filter_white_24dp.png Binary files differnew file mode 100644 index 000000000..7d72e7562 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_filter_white_24dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_lock_closed_grey600_24dp.png b/core/src/main/res/drawable-mdpi/ic_lock_closed_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..f1627ce34 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_lock_closed_grey600_24dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_lock_closed_white_24dp.png b/core/src/main/res/drawable-mdpi/ic_lock_closed_white_24dp.png Binary files differnew file mode 100644 index 000000000..8f18d11e6 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_lock_closed_white_24dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_lock_open_grey600_24dp.png b/core/src/main/res/drawable-mdpi/ic_lock_open_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..ada8d3be4 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_lock_open_grey600_24dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_lock_open_white_24dp.png b/core/src/main/res/drawable-mdpi/ic_lock_open_white_24dp.png Binary files differnew file mode 100644 index 000000000..72d01c406 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_lock_open_white_24dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_feed_grey600_24dp.png b/core/src/main/res/drawable-xhdpi/ic_feed_grey600_24dp.png Binary files differindex 5cb0262ee..b25d64863 100755 --- a/core/src/main/res/drawable-xhdpi/ic_feed_grey600_24dp.png +++ b/core/src/main/res/drawable-xhdpi/ic_feed_grey600_24dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_feed_white_24dp.png b/core/src/main/res/drawable-xhdpi/ic_feed_white_24dp.png Binary files differindex 5f34b0492..3c3e74c1d 100755 --- a/core/src/main/res/drawable-xhdpi/ic_feed_white_24dp.png +++ b/core/src/main/res/drawable-xhdpi/ic_feed_white_24dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_filter_grey600_24dp.png b/core/src/main/res/drawable-xhdpi/ic_filter_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..fdbb8eb4e --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_filter_grey600_24dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_filter_white_24dp.png b/core/src/main/res/drawable-xhdpi/ic_filter_white_24dp.png Binary files differnew file mode 100644 index 000000000..7e14f7fbf --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_filter_white_24dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_lock_closed_grey600_24dp.png b/core/src/main/res/drawable-xhdpi/ic_lock_closed_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..ca35f6d0a --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_lock_closed_grey600_24dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_lock_closed_white_24dp.png b/core/src/main/res/drawable-xhdpi/ic_lock_closed_white_24dp.png Binary files differnew file mode 100644 index 000000000..01fb55ca1 --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_lock_closed_white_24dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_lock_open_grey600_24dp.png b/core/src/main/res/drawable-xhdpi/ic_lock_open_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..11d9a4b8b --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_lock_open_grey600_24dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_lock_open_white_24dp.png b/core/src/main/res/drawable-xhdpi/ic_lock_open_white_24dp.png Binary files differnew file mode 100644 index 000000000..01ca4b56c --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_lock_open_white_24dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_feed_grey600_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_feed_grey600_24dp.png Binary files differindex 01ef2ee4d..aacf24d28 100755 --- a/core/src/main/res/drawable-xxhdpi/ic_feed_grey600_24dp.png +++ b/core/src/main/res/drawable-xxhdpi/ic_feed_grey600_24dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_feed_white_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_feed_white_24dp.png Binary files differindex 6dd465852..625dbaa1f 100755 --- a/core/src/main/res/drawable-xxhdpi/ic_feed_white_24dp.png +++ b/core/src/main/res/drawable-xxhdpi/ic_feed_white_24dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_filter_grey600_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_filter_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..43ec90ea5 --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_filter_grey600_24dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_filter_white_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_filter_white_24dp.png Binary files differnew file mode 100644 index 000000000..d3923efee --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_filter_white_24dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_lock_closed_grey600_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_lock_closed_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..311a7fa13 --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_lock_closed_grey600_24dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_lock_closed_white_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_lock_closed_white_24dp.png Binary files differnew file mode 100644 index 000000000..39a163843 --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_lock_closed_white_24dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_lock_open_grey600_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_lock_open_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..c0552d564 --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_lock_open_grey600_24dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_lock_open_white_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_lock_open_white_24dp.png Binary files differnew file mode 100644 index 000000000..46852d54f --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_lock_open_white_24dp.png diff --git a/core/src/main/res/drawable-xxxhdpi/ic_filter_grey600_24dp.png b/core/src/main/res/drawable-xxxhdpi/ic_filter_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..5d14b5b25 --- /dev/null +++ b/core/src/main/res/drawable-xxxhdpi/ic_filter_grey600_24dp.png diff --git a/core/src/main/res/drawable-xxxhdpi/ic_filter_white_24dp.png b/core/src/main/res/drawable-xxxhdpi/ic_filter_white_24dp.png Binary files differnew file mode 100644 index 000000000..e8b865e4a --- /dev/null +++ b/core/src/main/res/drawable-xxxhdpi/ic_filter_white_24dp.png diff --git a/core/src/main/res/drawable-xxxhdpi/ic_lock_closed_grey600_24dp.png b/core/src/main/res/drawable-xxxhdpi/ic_lock_closed_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..e41d5b9ee --- /dev/null +++ b/core/src/main/res/drawable-xxxhdpi/ic_lock_closed_grey600_24dp.png diff --git a/core/src/main/res/drawable-xxxhdpi/ic_lock_closed_white_24dp.png b/core/src/main/res/drawable-xxxhdpi/ic_lock_closed_white_24dp.png Binary files differnew file mode 100644 index 000000000..2376b7334 --- /dev/null +++ b/core/src/main/res/drawable-xxxhdpi/ic_lock_closed_white_24dp.png diff --git a/core/src/main/res/drawable-xxxhdpi/ic_lock_open_grey600_24dp.png b/core/src/main/res/drawable-xxxhdpi/ic_lock_open_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..c281784dd --- /dev/null +++ b/core/src/main/res/drawable-xxxhdpi/ic_lock_open_grey600_24dp.png diff --git a/core/src/main/res/drawable-xxxhdpi/ic_lock_open_white_24dp.png b/core/src/main/res/drawable-xxxhdpi/ic_lock_open_white_24dp.png Binary files differnew file mode 100644 index 000000000..25ea3ab99 --- /dev/null +++ b/core/src/main/res/drawable-xxxhdpi/ic_lock_open_white_24dp.png diff --git a/core/src/main/res/values-az/strings.xml b/core/src/main/res/values-az/strings.xml index 9640412c1..b52ecf4a4 100644 --- a/core/src/main/res/values-az/strings.xml +++ b/core/src/main/res/values-az/strings.xml @@ -76,7 +76,7 @@ <string name="download_error_connection_error">Əlaqə xətasi</string> <string name="download_error_unknown_host">Naməlum xost</string> <string name="cancel_all_downloads_label">Yükləmələrin hamısını ləğv et</string> - <string name="download_cancelled_msg">Yükləmə ləğv olundu</string> + <string name="download_canceled_msg">Yükləmə ləğv olundu</string> <string name="download_report_title">Yükləmə başa çatdı</string> <string name="download_error_malformed_url">Yanlış URL</string> <string name="download_error_io_error">IO xətasi</string> diff --git a/core/src/main/res/values-ca/strings.xml b/core/src/main/res/values-ca/strings.xml index 0fe3ae415..be7a73e6d 100644 --- a/core/src/main/res/values-ca/strings.xml +++ b/core/src/main/res/values-ca/strings.xml @@ -108,7 +108,7 @@ <string name="download_error_unknown_host">Amfitrió desconegut</string> <string name="download_error_unauthorized">Error d\'autenticació</string> <string name="cancel_all_downloads_label">Cancel·la totes les baixades</string> - <string name="download_cancelled_msg">S\'ha cancel·lat la baixada</string> + <string name="download_canceled_msg">S\'ha cancel·lat la baixada</string> <string name="download_report_title">Baixades completades</string> <string name="download_error_malformed_url">URL mal formatada</string> <string name="download_error_io_error">Error d\'E/S</string> diff --git a/core/src/main/res/values-cs-rCZ/strings.xml b/core/src/main/res/values-cs-rCZ/strings.xml index a67a6d10b..661faefbe 100644 --- a/core/src/main/res/values-cs-rCZ/strings.xml +++ b/core/src/main/res/values-cs-rCZ/strings.xml @@ -108,7 +108,7 @@ <string name="download_error_unknown_host">Neznámý host</string> <string name="download_error_unauthorized">Chyba přihlášení</string> <string name="cancel_all_downloads_label">Zrušit všechna stahování</string> - <string name="download_cancelled_msg">Stahování zrušeno</string> + <string name="download_canceled_msg">Stahování zrušeno</string> <string name="download_report_title">Všechna stahování dokončena</string> <string name="download_error_malformed_url">Chybné URL</string> <string name="download_error_io_error">IO chyba</string> diff --git a/core/src/main/res/values-da/strings.xml b/core/src/main/res/values-da/strings.xml index d31c65614..ba7fafca6 100644 --- a/core/src/main/res/values-da/strings.xml +++ b/core/src/main/res/values-da/strings.xml @@ -108,7 +108,7 @@ <string name="download_error_unknown_host">Ukendt vært</string> <string name="download_error_unauthorized">Godkendelses fejl</string> <string name="cancel_all_downloads_label">Annuller alle downloads</string> - <string name="download_cancelled_msg">Download afbrudt</string> + <string name="download_canceled_msg">Download afbrudt</string> <string name="download_report_title">Downloads afsluttet</string> <string name="download_error_malformed_url">Misdannet URL</string> <string name="download_error_io_error">IO fejl</string> diff --git a/core/src/main/res/values-de/strings.xml b/core/src/main/res/values-de/strings.xml index 1f1b519ab..f2d0a66ad 100644 --- a/core/src/main/res/values-de/strings.xml +++ b/core/src/main/res/values-de/strings.xml @@ -112,7 +112,7 @@ <string name="download_error_unknown_host">Unbekannter Host</string> <string name="download_error_unauthorized">Authentifizierungsfehler</string> <string name="cancel_all_downloads_label">Alle Downloads abbrechen</string> - <string name="download_cancelled_msg">Download abgebrochen</string> + <string name="download_canceled_msg">Download abgebrochen</string> <string name="download_report_title">Download abgeschlossen</string> <string name="download_error_malformed_url">Fehler in URL</string> <string name="download_error_io_error">E/A Error</string> diff --git a/core/src/main/res/values-es-rES/strings.xml b/core/src/main/res/values-es-rES/strings.xml index 48ff1570b..d05c34876 100644 --- a/core/src/main/res/values-es-rES/strings.xml +++ b/core/src/main/res/values-es-rES/strings.xml @@ -72,7 +72,7 @@ <string name="download_error_connection_error">Error de conexión</string> <string name="download_error_unknown_host">Host desconocido</string> <string name="cancel_all_downloads_label">Cancelar todas las descargas</string> - <string name="download_cancelled_msg">Descarga cancelada</string> + <string name="download_canceled_msg">Descarga cancelada</string> <string name="download_report_title">Descargas completadas</string> <string name="download_error_malformed_url">URL malformada</string> <string name="download_error_io_error">Error de E/S</string> diff --git a/core/src/main/res/values-es/strings.xml b/core/src/main/res/values-es/strings.xml index 44421240b..eb1d1631b 100644 --- a/core/src/main/res/values-es/strings.xml +++ b/core/src/main/res/values-es/strings.xml @@ -88,6 +88,7 @@ <string name="remove_episode_lable">Quitar episodio</string> <string name="mark_read_label">Marcar como leído</string> <string name="mark_unread_label">Marcar como no leído</string> + <string name="marked_as_read_label">Marcado como leído</string> <string name="add_to_queue_label">Añadir a la cola</string> <string name="remove_from_queue_label">Quitar de la cola</string> <string name="visit_website_label">Visitar el sitio web</string> @@ -111,7 +112,7 @@ <string name="download_error_unknown_host">Host desconocido</string> <string name="download_error_unauthorized">Error de autenticación</string> <string name="cancel_all_downloads_label">Cancelar todas las descargas</string> - <string name="download_cancelled_msg">Descarga cancelada</string> + <string name="download_canceled_msg">Descarga cancelada</string> <string name="download_report_title">Descargas completadas</string> <string name="download_error_malformed_url">URL con formato incorrecto</string> <string name="download_error_io_error">Error de E/S</string> diff --git a/core/src/main/res/values-fr/strings.xml b/core/src/main/res/values-fr/strings.xml index 617df3f8f..141b84bf7 100644 --- a/core/src/main/res/values-fr/strings.xml +++ b/core/src/main/res/values-fr/strings.xml @@ -112,7 +112,7 @@ <string name="download_error_unknown_host">Hôte inconnu</string> <string name="download_error_unauthorized">Erreur d\'authentification</string> <string name="cancel_all_downloads_label">Annuler tous les téléchargements</string> - <string name="download_cancelled_msg">Téléchargement annulé</string> + <string name="download_canceled_msg">Téléchargement annulé</string> <string name="download_report_title">Téléchargements terminés</string> <string name="download_error_malformed_url">URL incorrecte</string> <string name="download_error_io_error">Erreur d\'E/S</string> diff --git a/core/src/main/res/values-hi-rIN/strings.xml b/core/src/main/res/values-hi-rIN/strings.xml index 7a43ba15b..f32c7c02f 100644 --- a/core/src/main/res/values-hi-rIN/strings.xml +++ b/core/src/main/res/values-hi-rIN/strings.xml @@ -91,7 +91,7 @@ <string name="download_error_connection_error">कनेक्शन त्रुटि</string> <string name="download_error_unknown_host">अज्ञात होस्ट</string> <string name="cancel_all_downloads_label">सभी डाउनलोड रद्द करें</string> - <string name="download_cancelled_msg">डाउनलोड रद्द</string> + <string name="download_canceled_msg">डाउनलोड रद्द</string> <string name="download_report_title">डाउनलोड पूरा हो गया है</string> <string name="download_error_malformed_url">गलत URL</string> <string name="download_error_io_error">आईओ त्रुटि</string> diff --git a/core/src/main/res/values-it-rIT/strings.xml b/core/src/main/res/values-it-rIT/strings.xml index 52873d892..361657b6b 100644 --- a/core/src/main/res/values-it-rIT/strings.xml +++ b/core/src/main/res/values-it-rIT/strings.xml @@ -60,7 +60,7 @@ <string name="auto_download_label">Includi nei download automatici</string> <!--'Add Feed' Activity labels--> <string name="feedurl_label">URL del feed</string> - <string name="etxtFeedurlHint">URL del feed o del sito web</string> + <string name="etxtFeedurlHint">www.example.com/feed</string> <string name="txtvfeedurl_label">Aggiungi un Podcast tramite URL</string> <string name="podcastdirectories_label">Trova un podcast nella directory</string> <string name="podcastdirectories_descr">Puoi cercare dei nuovi podcast in base al nome, alla categoria o alla popolarità nella directory di gpodder.net.</string> @@ -85,6 +85,7 @@ <string name="remove_episode_lable">Rimuovi l\'episodio</string> <string name="mark_read_label">Segna come letto</string> <string name="mark_unread_label">Segna come non letto</string> + <string name="marked_as_read_label">Segnato come letto</string> <string name="add_to_queue_label">Aggiungi alla coda</string> <string name="remove_from_queue_label">Rimuovi dalla coda</string> <string name="visit_website_label">Visita il sito</string> @@ -108,7 +109,7 @@ <string name="download_error_unknown_host">Host sconosciuto</string> <string name="download_error_unauthorized">Errore di autenticazione</string> <string name="cancel_all_downloads_label">Annulla tutti i download</string> - <string name="download_cancelled_msg">Download annullato</string> + <string name="download_canceled_msg">Download annullato</string> <string name="download_report_title">Download completati</string> <string name="download_error_malformed_url">URL malformato</string> <string name="download_error_io_error">Errore IO</string> @@ -225,6 +226,7 @@ <string name="pref_autodl_wifi_filter_sum">Abilita il download automatico solo per alcune reti Wi-Fi selezionate.</string> <string name="pref_automatic_download_on_battery_title">Scarica quando la batteria non è in carica</string> <string name="pref_automatic_download_on_battery_sum">Permetti il download automatico quando la batteria non è in carica</string> + <string name="pref_parallel_downloads_title">Download paralleli</string> <string name="pref_episode_cache_title">Cache degli episodi</string> <string name="pref_theme_title_light">Light</string> <string name="pref_theme_title_dark">Dark</string> @@ -356,4 +358,5 @@ <string name="authentication_descr">Cambia il tuo nome utente e la tua password per questo podcast e i suoi episodi.</string> <!--AntennaPodSP--> <string name="sp_apps_importing_feeds_msg">Importazione di sottoscrizioni da applicazioni monouso in corso...</string> + <string name="search_itunes_label">Cerca su iTunes</string> </resources> diff --git a/core/src/main/res/values-iw-rIL/strings.xml b/core/src/main/res/values-iw-rIL/strings.xml index dd129457d..9e9c0e6bc 100644 --- a/core/src/main/res/values-iw-rIL/strings.xml +++ b/core/src/main/res/values-iw-rIL/strings.xml @@ -58,6 +58,7 @@ <string name="close_label">סגור</string> <string name="retry_label">נסה שוב</string> <string name="auto_download_label">כלול בהורדות אוטומטיות</string> + <string name="parallel_downloads_suffix">\u0020הורדות במקביל</string> <!--'Add Feed' Activity labels--> <string name="feedurl_label">כתובת הזנה</string> <string name="etxtFeedurlHint">כתובת של הזנה או אתר אינטרנט</string> @@ -68,6 +69,8 @@ <!--Actions on feeds--> <string name="mark_all_read_label">סמן הכל כנקרא</string> <string name="mark_all_read_msg">סמן את כל הפרקים כנקרא</string> + <string name="mark_all_read_confirmation_msg">אנא אשר שאתה רוצה לסמן את כל פרקים כנקראים.</string> + <string name="mark_all_read_feed_confirmation_msg">אנא אשר שאתה רוצה לסמן את כל פרקים בהזנה זו כנקראים.</string> <string name="show_info_label">הצג מידע</string> <string name="remove_feed_label">הסר פודקאסט</string> <string name="share_link_label">שתף קישור אתר</string> @@ -85,6 +88,7 @@ <string name="remove_episode_lable">הסר פרק</string> <string name="mark_read_label">סמן כנקרא</string> <string name="mark_unread_label">סמן כלא נקרא</string> + <string name="marked_as_read_label">סומן כנקרא</string> <string name="add_to_queue_label">הוסף לתור</string> <string name="remove_from_queue_label">הסר מהתור</string> <string name="visit_website_label">בקר באתר</string> @@ -108,7 +112,7 @@ <string name="download_error_unknown_host">שרת לא ידוע</string> <string name="download_error_unauthorized">שגיאת אימות</string> <string name="cancel_all_downloads_label">בטל את כל ההורדות</string> - <string name="download_cancelled_msg">הורדה בוטלה</string> + <string name="download_canceled_msg">הורדה בוטלה</string> <string name="download_report_title">הורדות הושלמו</string> <string name="download_error_malformed_url">כתובת אתר שגויה</string> <string name="download_error_io_error">שגיאת קלט פלט</string> @@ -150,6 +154,7 @@ <string name="duration">משך</string> <string name="ascending">בסדר עולה</string> <string name="descending">בסדר יורד</string> + <string name="clear_queue_confirmation_msg">אנא אשר שאתה רוצה לנקות את התור מכל הפרקים שבו</string> <!--Flattr--> <string name="flattr_auth_label">כניסה ל-Fattr</string> <string name="flattr_auth_explanation">לחץ על הכפתור למטה כדי להתחיל את תהליך האימות. אתה תועבר למסך כניסת flattr בדפדפן שלך ותתבקש לתת לאנטנה-פוד רשות לתרום באמצעות flattr. לאחר שקבלת אישור, תוכל לחזור למסך זה באופן אוטומטי.</string> @@ -226,6 +231,7 @@ <string name="pref_autodl_wifi_filter_sum">אפשר הורדה אוטומטית דרך רשתות אלחוטייות נבחרות.</string> <string name="pref_automatic_download_on_battery_title">הורדה כשלא טוען</string> <string name="pref_automatic_download_on_battery_sum">אפשר הורדה אוטומטית כשהסוללה אינה נטענת</string> + <string name="pref_parallel_downloads_title">הורדות במקביל</string> <string name="pref_episode_cache_title">מטמון פרקים</string> <string name="pref_theme_title_light">בהיר</string> <string name="pref_theme_title_dark">כהה</string> @@ -250,6 +256,8 @@ <string name="pref_persistNotify_title">פקדי הפעלה קבועים</string> <string name="pref_persistNotify_sum">שמר בקרי הודעה ומסך נעילה בעת השהיית השמעה.</string> <string name="pref_expand_notify_unsupport_toast">גרסאות אנדרויד לפני 4.1 לא תומכות בהודעות מורחבות.</string> + <string name="pref_queueAddToFront_sum">הוסף פרקים חדשים לראש התור.</string> + <string name="pref_queueAddToFront_title">הוסף לראש התור.</string> <!--Auto-Flattr dialog--> <string name="auto_flattr_enable">הפעל תרומות flattr אוטומטיות</string> <string name="auto_flattr_after_percent">תרום באמצעות flattr כשנוגן %d אחוזים מהפרק</string> @@ -264,6 +272,9 @@ <string name="found_in_title_label">נמצא בכותרת</string> <!--OPML import and export--> <string name="opml_import_txtv_button_lable">קבצי OPML יאפשרו לכך לנייד פודקאסטים מלוכד פודקאסטים אחד למשנו.</string> + <string name="opml_import_explanation_1">בחר נתיב קובץ ספציפי במערכת הקבצים המקומית.</string> + <string name="opml_import_explanation_2">השתמש ביישומים חיצוניים כמו Dropbox, Google Drive או מנהל הקבצים האהוב עליך לפתוח קובץ OPML.</string> + <string name="opml_import_explanation_3">יישומים רבים כמו Google Mail, Dropbox, Google Drive ורוב מנהלי הקבצים יכולים <i>לפתוח</i> קבצי OPML <i>עם</i> אנטנה-פוד.</string> <string name="start_import_label">התחל יבוא</string> <string name="opml_import_label">יבוא OPML</string> <string name="opml_directory_error">שגיאה!</string> @@ -272,6 +283,8 @@ <string name="opml_import_error_dir_empty">ספריית היבוא ריקה.</string> <string name="select_all_label">בחר הכל</string> <string name="deselect_all_label">בטל בחירות</string> + <string name="choose_file_from_filesystem">ממערכת הקבצים המקומית</string> + <string name="choose_file_from_external_application">השתמש באפליקציה חיצונית</string> <string name="opml_export_label">יצוא OPML</string> <string name="exporting_label">מייצא...</string> <string name="export_error_label">שגיאת יצוא</string> @@ -357,4 +370,5 @@ <string name="authentication_descr">שנה את שם המשתמש והסיסמה שלך לפודקאסט ופרקים שלו.</string> <!--AntennaPodSP--> <string name="sp_apps_importing_feeds_msg">מייבא רישום מאפליקציות יעודיות...</string> + <string name="search_itunes_label">חפש בiTunes</string> </resources> diff --git a/core/src/main/res/values-ja/strings.xml b/core/src/main/res/values-ja/strings.xml index df73db23e..9021b2b42 100644 --- a/core/src/main/res/values-ja/strings.xml +++ b/core/src/main/res/values-ja/strings.xml @@ -112,7 +112,7 @@ <string name="download_error_unknown_host">ホスト不明</string> <string name="download_error_unauthorized">認証エラー</string> <string name="cancel_all_downloads_label">すべてのダウンロードをキャンセル</string> - <string name="download_cancelled_msg">ダウンロードをキャンセルしました</string> + <string name="download_canceled_msg">ダウンロードをキャンセルしました</string> <string name="download_report_title">ダウンロードが完了しました</string> <string name="download_error_malformed_url">不正な形式のURL</string> <string name="download_error_io_error">IOエラー</string> diff --git a/core/src/main/res/values-ko/strings.xml b/core/src/main/res/values-ko/strings.xml index 8e8a264f4..7830be329 100644 --- a/core/src/main/res/values-ko/strings.xml +++ b/core/src/main/res/values-ko/strings.xml @@ -58,16 +58,19 @@ <string name="close_label">닫기</string> <string name="retry_label">다시 시도</string> <string name="auto_download_label">자동 다운로드에 포함</string> + <string name="parallel_downloads_suffix">\u0020동시 다운로드</string> <!--'Add Feed' Activity labels--> <string name="feedurl_label">피드 URL</string> <string name="etxtFeedurlHint">피드의 URL 또는 홈페이지</string> <string name="txtvfeedurl_label">URL로 팟캐스트를 추가</string> <string name="podcastdirectories_label">디렉터리에서 팟캐스트 찾기</string> - <string name="podcastdirectories_descr">gpodder.net 디렉터리에서 이름, 분류, 인기에 따라 새 팟캐스트를 검색할 수 있습니다</string> + <string name="podcastdirectories_descr">gpodder.net 디렉터리에서 이름, 분류, 인기에 따라 새 팟캐스트를 검색할 수 있고, iTunes 스토어에서 검색할 수도 있습니다.</string> <string name="browse_gpoddernet_label">gpodder.net 둘러보기</string> <!--Actions on feeds--> <string name="mark_all_read_label">모두 읽은 것으로 표시</string> <string name="mark_all_read_msg">모든 에피소드 읽은 것으로 표시</string> + <string name="mark_all_read_confirmation_msg">에피소드 모두를 읽은 것으로 표시하는지 확인하십시오.</string> + <string name="mark_all_read_feed_confirmation_msg">이 피드의 에피소드 모두를 읽은 것으로 표시하는지 확인하십시오.</string> <string name="show_info_label">정보 표시</string> <string name="remove_feed_label">팟캐스트 제거</string> <string name="share_link_label">홈페이지 링크 공유</string> @@ -85,6 +88,7 @@ <string name="remove_episode_lable">에피소드 제거</string> <string name="mark_read_label">읽은 것으로 표시</string> <string name="mark_unread_label">읽지 않은 것으로 표시</string> + <string name="marked_as_read_label">읽은 것으로 표시</string> <string name="add_to_queue_label">대기열에 추가</string> <string name="remove_from_queue_label">대기열에서 제거</string> <string name="visit_website_label">홈페이지 보기</string> @@ -108,7 +112,7 @@ <string name="download_error_unknown_host">알 수 없는 호스트</string> <string name="download_error_unauthorized">인증 오류</string> <string name="cancel_all_downloads_label">모든 다운로드 취소</string> - <string name="download_cancelled_msg">다운로드 취소됨</string> + <string name="download_canceled_msg">다운로드 취소됨</string> <string name="download_report_title">다운로드 마침</string> <string name="download_error_malformed_url">URL 형식 틀림</string> <string name="download_error_io_error">입출력 오류</string> @@ -150,6 +154,7 @@ <string name="duration">기간</string> <string name="ascending">오름차순</string> <string name="descending">내림차순</string> + <string name="clear_queue_confirmation_msg">내부의 모든 에피소드 대기열을 지울지 확인하십시오.</string> <!--Flattr--> <string name="flattr_auth_label">Flattr 로그인</string> <string name="flattr_auth_explanation">인증 절차를 시작하려면 아래 버튼을 누르십시오. 브라우저의 Flattr 로그인 화면으로 이동하고, 안테나팟에 Flattr를 사용을 허락 여부를 물어봅니다. 허락을 하면 자동으로 이 화면으로 돌아옵니다.</string> @@ -225,6 +230,7 @@ <string name="pref_autodl_wifi_filter_sum">선택한 Wi-Fi 네트워크에 대해서만 자동 다운로드를 허용합니다.</string> <string name="pref_automatic_download_on_battery_title">충전하지 않을 때 다운로드</string> <string name="pref_automatic_download_on_battery_sum">배터리 충전 중이 아닐 때 자동 다운로드 허용</string> + <string name="pref_parallel_downloads_title">동시 다운로드</string> <string name="pref_episode_cache_title">에피소드 임시 저장</string> <string name="pref_theme_title_light">밝게</string> <string name="pref_theme_title_dark">어둡게</string> @@ -249,6 +255,8 @@ <string name="pref_persistNotify_title">재생 조작 고정</string> <string name="pref_persistNotify_sum">재생이 일시 중지했을 때에도 알림과 잠금 화면의 조작 기능 유지</string> <string name="pref_expand_notify_unsupport_toast">안드로이드 4.1 전 버전에서는 알림 확장을 지원하지 않습니다.</string> + <string name="pref_queueAddToFront_sum">새 에피소드를 대기열 앞에 추가합니다.</string> + <string name="pref_queueAddToFront_title">대기열 앞에 추가</string> <!--Auto-Flattr dialog--> <string name="auto_flattr_enable">자동 flattr 사용</string> <string name="auto_flattr_after_percent">%d 퍼센트를 재생하면 에피소드에 flattr합니다</string> @@ -263,6 +271,9 @@ <string name="found_in_title_label">제목에서 발견</string> <!--OPML import and export--> <string name="opml_import_txtv_button_lable">OPML 파일을 이용하면 팟캐스트 목록을 한 팟캐스트 프로그램에서 다른 팟캐스트 프로그램으로 옮길 수 있습니다.</string> + <string name="opml_import_explanation_1">로컬 파일시스템의 특정 파일 경로를 선택하십시오.</string> + <string name="opml_import_explanation_2">OPML 파일을 여는데 Dropbox, Google Drive, 또는 파일 관리자 와 같은 외부 앱을 사용합니다.</string> + <string name="opml_import_explanation_3">Google Mail, Dropbox, Google Drive 및 대부분의 파일 관리자는 OPML 파일을 안테나팟<i>으로</i> <i>열 수</i> 있습니다.</string> <string name="start_import_label">가져오기 시작</string> <string name="opml_import_label">OPML 가져오기</string> <string name="opml_directory_error">오류!</string> @@ -271,6 +282,8 @@ <string name="opml_import_error_dir_empty">가져오기 디렉터리가 비어 있습니다.</string> <string name="select_all_label">모두 선택</string> <string name="deselect_all_label">모두 선택 해제</string> + <string name="choose_file_from_filesystem">로컬 파일시스템에서</string> + <string name="choose_file_from_external_application">외부 앱 사용</string> <string name="opml_export_label">OPML 내보내기</string> <string name="exporting_label">내보내는 중...</string> <string name="export_error_label">내보내기 오류</string> @@ -356,4 +369,5 @@ <string name="authentication_descr">이 팟캐스트와 에피소드에 대한 사용자 이름과 비밀번호를 바꿉니다.</string> <!--AntennaPodSP--> <string name="sp_apps_importing_feeds_msg">단일 용도 앱에서 구독 정보를 가져옵니다...</string> + <string name="search_itunes_label">iTunes 검색</string> </resources> diff --git a/core/src/main/res/values-nl/strings.xml b/core/src/main/res/values-nl/strings.xml index 66ffaaf87..0f447d54a 100644 --- a/core/src/main/res/values-nl/strings.xml +++ b/core/src/main/res/values-nl/strings.xml @@ -86,7 +86,7 @@ <string name="download_error_unknown_host">Onbekende host</string> <string name="download_error_unauthorized">Authenticatie fout</string> <string name="cancel_all_downloads_label">Alle downloads annuleren</string> - <string name="download_cancelled_msg">Download geannuleerd</string> + <string name="download_canceled_msg">Download geannuleerd</string> <string name="download_report_title">Downloads afgerond</string> <string name="download_error_malformed_url">Misvormde URL</string> <string name="download_error_io_error">IO fout</string> diff --git a/core/src/main/res/values-pl-rPL/strings.xml b/core/src/main/res/values-pl-rPL/strings.xml index 6e5c2ce44..ba1a0bb91 100644 --- a/core/src/main/res/values-pl-rPL/strings.xml +++ b/core/src/main/res/values-pl-rPL/strings.xml @@ -108,7 +108,7 @@ <string name="download_error_unknown_host">Nieznany host</string> <string name="download_error_unauthorized">Błąd autoryzacji</string> <string name="cancel_all_downloads_label">Anuluj wszystkie pobierania</string> - <string name="download_cancelled_msg">Pobieranie anulowane</string> + <string name="download_canceled_msg">Pobieranie anulowane</string> <string name="download_report_title">Pobieranie ukończone</string> <string name="download_error_malformed_url">Niepoprawny adres</string> <string name="download_error_io_error">Błąd wejścia/wyjścia</string> diff --git a/core/src/main/res/values-pt-rBR/strings.xml b/core/src/main/res/values-pt-rBR/strings.xml index aba186c1a..c3523acfb 100644 --- a/core/src/main/res/values-pt-rBR/strings.xml +++ b/core/src/main/res/values-pt-rBR/strings.xml @@ -85,7 +85,7 @@ <string name="download_error_connection_error">Erro de conexão</string> <string name="download_error_unknown_host">Host desconhecido</string> <string name="cancel_all_downloads_label">Cancelar todos os downloads</string> - <string name="download_cancelled_msg">Download cancelado</string> + <string name="download_canceled_msg">Download cancelado</string> <string name="download_report_title">Downloads finalizados</string> <string name="download_error_malformed_url">URL inválida</string> <string name="download_error_io_error">Erro de IO</string> diff --git a/core/src/main/res/values-pt/strings.xml b/core/src/main/res/values-pt/strings.xml index 9ef8474ee..d9e201e21 100644 --- a/core/src/main/res/values-pt/strings.xml +++ b/core/src/main/res/values-pt/strings.xml @@ -112,7 +112,7 @@ <string name="download_error_unknown_host">Servidor desconhecido</string> <string name="download_error_unauthorized">Erro de autenticação</string> <string name="cancel_all_downloads_label">Cancelar transferências</string> - <string name="download_cancelled_msg">Transferência cancelada</string> + <string name="download_canceled_msg">Transferência cancelada</string> <string name="download_report_title">Transferências terminadas</string> <string name="download_error_malformed_url">URL inválido</string> <string name="download_error_io_error">Erro I/O</string> diff --git a/core/src/main/res/values-ro-rRO/strings.xml b/core/src/main/res/values-ro-rRO/strings.xml index 7bfb99f9d..390f50767 100644 --- a/core/src/main/res/values-ro-rRO/strings.xml +++ b/core/src/main/res/values-ro-rRO/strings.xml @@ -80,7 +80,7 @@ <string name="download_error_connection_error">Eroare de conexiune</string> <string name="download_error_unknown_host">Host necunoscut</string> <string name="cancel_all_downloads_label">Anulează toate descărcările</string> - <string name="download_cancelled_msg">Descărcare anulată</string> + <string name="download_canceled_msg">Descărcare anulată</string> <string name="download_report_title">Descărcări terminate</string> <string name="download_error_malformed_url">URL malformat</string> <string name="download_error_io_error">Eroare IO</string> diff --git a/core/src/main/res/values-ru/strings.xml b/core/src/main/res/values-ru/strings.xml index 8e209a5ad..187a92861 100644 --- a/core/src/main/res/values-ru/strings.xml +++ b/core/src/main/res/values-ru/strings.xml @@ -58,20 +58,26 @@ <string name="close_label">Закрыть</string> <string name="retry_label">Повторить</string> <string name="auto_download_label">Добавить в автозагрузки</string> + <string name="parallel_downloads_suffix">\u0020одновременных загрузок</string> <!--'Add Feed' Activity labels--> <string name="feedurl_label">URL канала</string> + <string name="etxtFeedurlHint">www.example.com/feed</string> <string name="txtvfeedurl_label">Добавить подкаст по URL</string> <string name="podcastdirectories_label">Найти подкаст в каталоге</string> + <string name="podcastdirectories_descr">Вы можете искать новые подкасты по имени, категории или популярности в каталоге gpodder.net и в магазине iTunes.</string> <string name="browse_gpoddernet_label">Просмотр gpodder.net</string> <!--Actions on feeds--> <string name="mark_all_read_label">Отметить как прослушанное</string> <string name="mark_all_read_msg">Отметить все выпуски как прослушанные</string> + <string name="mark_all_read_confirmation_msg">Подтвердите, что хотите пометить все эпизоды как прослушанные.</string> + <string name="mark_all_read_feed_confirmation_msg">Подтвердите, что хотите пометить все эпизоды в этом канале как прослушанные.</string> <string name="show_info_label">Показать информацию</string> <string name="remove_feed_label">Удалить подкаст</string> <string name="share_link_label">Поделиться ссылкой на сайт</string> <string name="share_source_label">Ссылка на канал</string> <string name="feed_delete_confirmation_msg">Подтвердите удаление канала и всех выпусков, загруженных с этого канала.</string> <string name="feed_remover_msg">Удаление канала</string> + <string name="load_complete_feed">Обновить весь канал</string> <!--actions on feeditems--> <string name="download_label">Загрузить</string> <string name="play_label">Воспроизвести</string> @@ -82,6 +88,7 @@ <string name="remove_episode_lable">Удалить</string> <string name="mark_read_label">Отметить как прочитанное</string> <string name="mark_unread_label">Отметить как непрочитанное</string> + <string name="marked_as_read_label">Помечено как прослушанное</string> <string name="add_to_queue_label">Добавить в очередь</string> <string name="remove_from_queue_label">Удалить из очереди</string> <string name="visit_website_label">Посетить сайт</string> @@ -105,7 +112,7 @@ <string name="download_error_unknown_host">Неизвестный узел</string> <string name="download_error_unauthorized">Ошибка авторизации</string> <string name="cancel_all_downloads_label">Отменить все загрузки</string> - <string name="download_cancelled_msg">Загрузка отменена</string> + <string name="download_canceled_msg">Загрузка отменена</string> <string name="download_report_title">Загрузки завершены</string> <string name="download_error_malformed_url">Неправильный адрес</string> <string name="download_error_io_error">Ошибка ввода-вывода</string> @@ -146,6 +153,7 @@ <string name="duration">По продолжительности</string> <string name="ascending">По возрастанию</string> <string name="descending">По убыванию</string> + <string name="clear_queue_confirmation_msg">Подтвердите, что хотите очистить очередь от ВСЕХ эпизодов.</string> <!--Flattr--> <string name="flattr_auth_label">Авторизоваться в Flattr</string> <string name="flattr_auth_explanation">Нажмите кнопку, чтобы начать процесс авторизации. Вы будете перенаправлены на сайт Flattr, где нужно будет разрешить AntennaPod использовать ваш аккаунт. После этого вы автоматически будете перенаправлены обратно.</string> @@ -218,6 +226,7 @@ <string name="pref_autodl_wifi_filter_sum">Разрешать автоматическую загрузку только для выбранных сетей Wi-Fi.</string> <string name="pref_automatic_download_on_battery_title">Загружать без зарядки</string> <string name="pref_automatic_download_on_battery_sum">Разрешать автоматическую загрузку когда батарея не заряжается</string> + <string name="pref_parallel_downloads_title">Одновременные загрузки</string> <string name="pref_episode_cache_title">Кэш выпусков</string> <string name="pref_theme_title_light">Светлая</string> <string name="pref_theme_title_dark">Тёмная</string> @@ -243,6 +252,9 @@ <string name="pref_persistNotify_sum">Сохранять уведомление и кнопки воспроизведения на экране блокировки во время паузы.</string> <string name="pref_expand_notify_unsupport_toast">Версии Android ниже 4.1 не поддерживают расширенные уведомления.</string> <!--Auto-Flattr dialog--> + <string name="auto_flattr_after_percent">Поддерживать через Flattr эпизоды, прослушанные на %d процентов</string> + <string name="auto_flattr_ater_beginning">Поддерживать эпизод через Flattr в начале воспроизведения</string> + <string name="auto_flattr_ater_end">Поддерживать эпизод через Flattr в конце воспроизведения</string> <!--Search--> <string name="search_hint">Поиск каналов или выпусков</string> <string name="found_in_shownotes_label">Найдено в описании выпуска</string> @@ -252,6 +264,7 @@ <string name="found_in_title_label">Найдено в заголовке</string> <!--OPML import and export--> <string name="opml_import_txtv_button_lable">OPML файлы позволяют перемещать ваши подкасты из одного менеджера подкастов в другой.</string> + <string name="opml_import_explanation_1">Укажите путь к файлу на устройстве</string> <string name="start_import_label">Начать импорт</string> <string name="opml_import_label">Импорт OPML</string> <string name="opml_directory_error">Ошибка</string> @@ -282,6 +295,7 @@ <string name="gpodnetauth_login_title">Войти</string> <string name="gpodnetauth_login_descr">Добро пожаловать в процесс авторизации на gpodder.net. Сначала введите вашу информацию для авторизации:</string> <string name="gpodnetauth_login_butLabel">Войти</string> + <string name="gpodnetauth_login_register">Если у вас ещё нет аккаунта, вы можете создать его здесь:\nhttps://gpodder.net/register/</string> <string name="username_label">Имя пользователя</string> <string name="password_label">Пароль</string> <string name="gpodnetauth_device_title">Выбор устройства</string> @@ -342,4 +356,5 @@ <string name="authentication_label">Авторизация</string> <!--AntennaPodSP--> <string name="sp_apps_importing_feeds_msg">Импорт подписок из одноцелевых приложений…</string> + <string name="search_itunes_label">Поиск в iTunes</string> </resources> diff --git a/core/src/main/res/values-sv-rSE/strings.xml b/core/src/main/res/values-sv-rSE/strings.xml index 4e468c1e1..e70ca5d3f 100644 --- a/core/src/main/res/values-sv-rSE/strings.xml +++ b/core/src/main/res/values-sv-rSE/strings.xml @@ -112,7 +112,7 @@ <string name="download_error_unknown_host">Okänd värd</string> <string name="download_error_unauthorized">Autentiseringsproblem</string> <string name="cancel_all_downloads_label">Avbryt alla nedladdningar</string> - <string name="download_cancelled_msg">Nedladdning avbruten</string> + <string name="download_canceled_msg">Nedladdning avbruten</string> <string name="download_report_title">Nedladdningar färdiga</string> <string name="download_error_malformed_url">Felaktig webbadress</string> <string name="download_error_io_error">IO fel</string> diff --git a/core/src/main/res/values-tr/strings.xml b/core/src/main/res/values-tr/strings.xml index 52b92157e..e83c9b48e 100644 --- a/core/src/main/res/values-tr/strings.xml +++ b/core/src/main/res/values-tr/strings.xml @@ -58,14 +58,19 @@ <string name="close_label">Kapat</string> <string name="retry_label">Yeniden dene</string> <string name="auto_download_label">Otomatik indirmelere dahil et</string> + <string name="parallel_downloads_suffix">\u0020paralel indirmeler</string> <!--'Add Feed' Activity labels--> <string name="feedurl_label">Besleme Adresi</string> + <string name="etxtFeedurlHint">www.example.com/feed</string> <string name="txtvfeedurl_label">URL ile cep yayını ekle</string> <string name="podcastdirectories_label">Dizinde cep yayını bul</string> + <string name="podcastdirectories_descr">gdpodder.net dizininde yeni cep yayınlarını isme, kategoriye veya popülerliğe göre arayabilirsiniz veya iTunes mağazasında arama yapabilirsiniz.</string> <string name="browse_gpoddernet_label">gpodder.net\'e gözat</string> <!--Actions on feeds--> <string name="mark_all_read_label">Hepsini okundu olarak işaretle</string> <string name="mark_all_read_msg">Tüm bölümler okundu olarak işaretlendi</string> + <string name="mark_all_read_confirmation_msg">Lütfen tüm bölümleri okundu olarak işaretlemek istediğinizi onaylayın.</string> + <string name="mark_all_read_feed_confirmation_msg">Lütfen bu besleme içindeki tüm bölümleri okundu olarak işaretlemek istediğinizi onaylayın.</string> <string name="show_info_label">Bilgiyi göster</string> <string name="remove_feed_label">Cep yayını kaldır</string> <string name="share_link_label">Web sayfası bağlantısı paylaş</string> @@ -83,6 +88,7 @@ <string name="remove_episode_lable">Bölümü kaldır</string> <string name="mark_read_label">Okundu olarak işaretle</string> <string name="mark_unread_label">Okunmadı olarak işaretle</string> + <string name="marked_as_read_label">Okundu olarak işaretlendi</string> <string name="add_to_queue_label">Kuyruğa Ekle</string> <string name="remove_from_queue_label">Kuyruktan Kaldır</string> <string name="visit_website_label">Siteyi Ziyaret Et</string> @@ -106,7 +112,7 @@ <string name="download_error_unknown_host">Bilinmeyen sunucu</string> <string name="download_error_unauthorized">Yetkilendirme hatası</string> <string name="cancel_all_downloads_label">Bütün indirmeleri iptal et</string> - <string name="download_cancelled_msg">İndirme iptal edildi</string> + <string name="download_canceled_msg">İndirme iptal edildi</string> <string name="download_report_title">İndirme tamamlandı</string> <string name="download_error_malformed_url">Bozuk URL</string> <string name="download_error_io_error">G/Ç Hatası</string> @@ -148,6 +154,7 @@ <string name="duration">Süre</string> <string name="ascending">Artan</string> <string name="descending">Azalan</string> + <string name="clear_queue_confirmation_msg">Lütfen içerisindeki BÜTÜN ölümlerle birlikte kuyruğu temizleme isteğinizi onaylayın.</string> <!--Flattr--> <string name="flattr_auth_label">Flattr giriş</string> <string name="flattr_auth_explanation">Yetkilendirme işlemini başlatmak için aşağıdaki butona basın. Tarayıcınızda flattr giriş ekranına yönlendirileceksiniz ve AntennaPod\'un flattr ile etkileşime girebilmesi için izniniz istenecek. İzin verdikten sonra otomatik olarak bu ekrana döneceksiniz.</string> @@ -223,6 +230,7 @@ <string name="pref_autodl_wifi_filter_sum">Seçilen kablosuz ağlar için otomatik indirmeye izin ver.</string> <string name="pref_automatic_download_on_battery_title">Şarj olmuyorken indir</string> <string name="pref_automatic_download_on_battery_sum">Pil şarj olmuyorken otomatik indirmeye izin ver</string> + <string name="pref_parallel_downloads_title">Paralel indirmeler</string> <string name="pref_episode_cache_title">Bölüm ön belleği</string> <string name="pref_theme_title_light">Aydınlık</string> <string name="pref_theme_title_dark">Karanlık</string> @@ -247,6 +255,8 @@ <string name="pref_persistNotify_title">Kalıcı oynatma kontrolleri</string> <string name="pref_persistNotify_sum">Çalma duraklatıldığında bildirim ve ekran kilidi ayarlarını sakla.</string> <string name="pref_expand_notify_unsupport_toast">Android 4.1 öncesi sürümler genişletilmiş bildirimleri desteklememektedir.</string> + <string name="pref_queueAddToFront_sum">Yeni bölümleri kuyruğun önüne ekle.</string> + <string name="pref_queueAddToFront_title">Kuyruğun önüne ekle.</string> <!--Auto-Flattr dialog--> <string name="auto_flattr_enable">Otomatik Flattr\'lamayı etkinleştir</string> <string name="auto_flattr_after_percent">Bölümün yüzde %d kısmı oynatıldığında Flattr\'la</string> @@ -261,6 +271,9 @@ <string name="found_in_title_label">Başlıkta bulundu</string> <!--OPML import and export--> <string name="opml_import_txtv_button_lable">OPML dosyaları cep yayınlarını bir cihazdan diğerine aktarmanıza yarar.</string> + <string name="opml_import_explanation_1">Yerel dosya sisteminden belirli bir yol seçin.</string> + <string name="opml_import_explanation_2">OPML dosyasını açmak için harici uygulamalardan Dropbox, Google Drive veya kendi favori dosya yöneticinizi kullanın.</string> + <string name="opml_import_explanation_3">Google Mail, Dropbox, Google Drive gibi birçok uygulama ve çoğu dosya yöneticisi OPML dosyalarını AntennaPod <i>ile</i> <i>açabilir.</i></string> <string name="start_import_label">İçe aktarmayı başlat</string> <string name="opml_import_label">OPML içe aktar</string> <string name="opml_directory_error">HATA!</string> @@ -269,9 +282,12 @@ <string name="opml_import_error_dir_empty">İça aktarma dizini boş</string> <string name="select_all_label">Hepsini seç</string> <string name="deselect_all_label">Tüm seçimleri geri al</string> + <string name="choose_file_from_filesystem">Yerel dosya sisteminden</string> + <string name="choose_file_from_external_application">Harici uygulama kullan</string> <string name="opml_export_label">OPML dışa aktar</string> <string name="exporting_label">Dışa aktarılıyor...</string> <string name="export_error_label">Dışa aktarma hatası</string> + <string name="opml_export_success_title">Opml dışa aktarma başarılı.</string> <string name="opml_export_success_sum">.opml dosyasy yazıldı: \u0020</string> <!--Sleep timer--> <string name="set_sleeptimer_label">Zamanlayıcıyı ayarla</string> @@ -353,4 +369,5 @@ <string name="authentication_descr">Bu cep yayını ve içerdiği bölümler için kullanıcı adı şifreyi değiştir.</string> <!--AntennaPodSP--> <string name="sp_apps_importing_feeds_msg">Üyelikler tek-amaçlı uygulamalardan içe aktarılıyor...</string> + <string name="search_itunes_label">iTunes\'da Arama</string> </resources> diff --git a/core/src/main/res/values-uk-rUA/strings.xml b/core/src/main/res/values-uk-rUA/strings.xml index 9ceab72f6..20374232a 100644 --- a/core/src/main/res/values-uk-rUA/strings.xml +++ b/core/src/main/res/values-uk-rUA/strings.xml @@ -58,6 +58,7 @@ <string name="close_label">Закрити</string> <string name="retry_label">Повторити знову</string> <string name="auto_download_label">Включити до автозавантаження</string> + <string name="parallel_downloads_suffix">\u0020паралельні завантаження</string> <!--'Add Feed' Activity labels--> <string name="feedurl_label">Посилання на канал</string> <string name="etxtFeedurlHint">URL канала або сайта</string> @@ -68,6 +69,8 @@ <!--Actions on feeds--> <string name="mark_all_read_label">Позначити всі як переглянуті</string> <string name="mark_all_read_msg">Позначено всі епізоди як переглянуті</string> + <string name="mark_all_read_confirmation_msg">Будь ласка, підтвердіть що ви бажаєте позначити всі епізоди як прочитані.</string> + <string name="mark_all_read_feed_confirmation_msg">Будь ласка, підтвердіть що ви бажаєте позначити всі епізоди цього канала як прочитані.</string> <string name="show_info_label">Інформація</string> <string name="remove_feed_label">Видалити подкаст</string> <string name="share_link_label">Поділитися URL сайту</string> @@ -85,6 +88,7 @@ <string name="remove_episode_lable">Видалити епізод</string> <string name="mark_read_label">Позначити як переглянутий</string> <string name="mark_unread_label">Позначити як не переглянутий</string> + <string name="marked_as_read_label">Позначено як прочитане</string> <string name="add_to_queue_label">Додати до черги</string> <string name="remove_from_queue_label">Видалити з черги</string> <string name="visit_website_label">Відкрити сайт</string> @@ -108,7 +112,7 @@ <string name="download_error_unknown_host">Невідомий host</string> <string name="download_error_unauthorized">Помилка автентифікації</string> <string name="cancel_all_downloads_label">Скасувати всі завантаження</string> - <string name="download_cancelled_msg">Відмінено завантаження</string> + <string name="download_canceled_msg">Відмінено завантаження</string> <string name="download_report_title">Завантажили</string> <string name="download_error_malformed_url">Невірний URL</string> <string name="download_error_io_error">Помилка IO</string> @@ -150,6 +154,7 @@ <string name="duration">За тривалістю</string> <string name="ascending">За зростанням</string> <string name="descending">За спаданням</string> + <string name="clear_queue_confirmation_msg">Будь ласка, підтвердіть що ви бажаєте вилучити всі епізоди з черги.</string> <!--Flattr--> <string name="flattr_auth_label">Увійти до Flattr</string> <string name="flattr_auth_explanation">Нажміть цю кнопку для початку авторізації. Буде відкрито flattr в браузері, буде запит на дозвіл доступу Antennapod до flattr. Після надання доступу ви повернетесь до цього екрану автоматично</string> @@ -225,6 +230,7 @@ <string name="pref_autodl_wifi_filter_sum">Дозволити автоматичне завантаження тільки в цих Wi-Fi мережах</string> <string name="pref_automatic_download_on_battery_title">Завантаження без зарядного пристрою</string> <string name="pref_automatic_download_on_battery_sum">Дозволити завантаження коли зарядний пристрій не підключено</string> + <string name="pref_parallel_downloads_title">Паралельні завантаження</string> <string name="pref_episode_cache_title">Кеш епізодів</string> <string name="pref_theme_title_light">Світла</string> <string name="pref_theme_title_dark">Темна</string> @@ -249,6 +255,8 @@ <string name="pref_persistNotify_title">Завжди показувати елементи керування відтворенням</string> <string name="pref_persistNotify_sum">Показувати повідомлення та елементи керування на lockscreen в режимі паузи.</string> <string name="pref_expand_notify_unsupport_toast">Android до версії 4.1 не підтримує розширені повідомлення.</string> + <string name="pref_queueAddToFront_sum">Додавати нові епізоди до початку черги.</string> + <string name="pref_queueAddToFront_title">Додавати в початок черги.</string> <!--Auto-Flattr dialog--> <string name="auto_flattr_enable">Включити автоматичне заохочення авторів через сервіс flattr</string> <string name="auto_flattr_after_percent">Заохотити автора через Flattr щойно %d відсотків епізода було відтворено</string> @@ -263,6 +271,9 @@ <string name="found_in_title_label">Знайдено у назві</string> <!--OPML import and export--> <string name="opml_import_txtv_button_lable">OPML файли дозволяют вам перенести подкасти з однієї программи до іншої</string> + <string name="opml_import_explanation_1">Виберіть локальну папку.</string> + <string name="opml_import_explanation_2">Вибрати OPML файл за допомогою таких додатків як Dropbox, Google Drive або файловий менеджер.</string> + <string name="opml_import_explanation_3">Багато додатків таких як Google Mail, Dropbox, Google Drive та більшість файлових менеджерів здатні <i>відкрити</i> OPML файли <i>для</i> AntennaPod.</string> <string name="start_import_label">Почати імпорт</string> <string name="opml_import_label">OPML імпорт</string> <string name="opml_directory_error">Помилка!</string> @@ -271,6 +282,8 @@ <string name="opml_import_error_dir_empty">Директорія імпорту пуста</string> <string name="select_all_label">Обрати все</string> <string name="deselect_all_label">Убрати виділення</string> + <string name="choose_file_from_filesystem">З локальної файлової системи</string> + <string name="choose_file_from_external_application">За допомогою додатка</string> <string name="opml_export_label">OPML экспорт</string> <string name="exporting_label">Експорт ...</string> <string name="export_error_label">Помилка експорту</string> @@ -356,4 +369,5 @@ <string name="authentication_descr">Змінити ваші логін та пароль для подкаста та епізодів</string> <!--AntennaPodSP--> <string name="sp_apps_importing_feeds_msg">Імпорт подкастів з інших програм...</string> + <string name="search_itunes_label">Пошук в iTunes</string> </resources> diff --git a/core/src/main/res/values-zh-rCN/strings.xml b/core/src/main/res/values-zh-rCN/strings.xml index d857ea194..0a2b9355e 100644 --- a/core/src/main/res/values-zh-rCN/strings.xml +++ b/core/src/main/res/values-zh-rCN/strings.xml @@ -103,7 +103,7 @@ <string name="download_error_unknown_host">未知主机</string> <string name="download_error_unauthorized">认证错误</string> <string name="cancel_all_downloads_label">取消所有下载</string> - <string name="download_cancelled_msg">已取消下载</string> + <string name="download_canceled_msg">已取消下载</string> <string name="download_report_title">下载完成</string> <string name="download_error_malformed_url">畸形 URL</string> <string name="download_error_io_error">IO 错误</string> diff --git a/core/src/main/res/values/arrays.xml b/core/src/main/res/values/arrays.xml index 4bb29ac85..4ecf2cf61 100644 --- a/core/src/main/res/values/arrays.xml +++ b/core/src/main/res/values/arrays.xml @@ -10,7 +10,7 @@ </string-array> - <string-array name="seek_delta_values"> + <integer-array name="seek_delta_values"> <item>5</item> <item>10</item> <item>15</item> @@ -18,7 +18,7 @@ <item>30</item> <item>45</item> <item>60</item> - </string-array> + </integer-array> <string-array name="update_intervall_options"> <item>Manual</item> @@ -118,6 +118,7 @@ <string-array name="autodl_select_networks_default_values"> <item>0</item> </string-array> + <string-array name="theme_options"> <item>@string/pref_theme_title_light</item> <item>@string/pref_theme_title_dark</item> @@ -126,4 +127,34 @@ <item>0</item> <item>1</item> </string-array> + + <string-array name="nav_drawer_titles"> + <item>@string/queue_label</item> + <item>@string/new_episodes_label</item> + <item>@string/all_episodes_label</item> + <item>@string/downloads_label</item> + <item>@string/playback_history_label</item> + <item>@string/add_feed_label</item> + </string-array> + + <string-array name="episode_hide_options"> + <item>@string/hide_unplayed_episodes_label</item> + <item>@string/hide_paused_episodes_label</item> + <item>@string/hide_played_episodes_label</item> + <item>@string/hide_queued_episodes_label</item> + <item>@string/hide_not_queued_episodes_label</item> + <item>@string/hide_downloaded_episodes_label</item> + <item>@string/hide_not_downloaded_episodes_label</item> + </string-array> + + <string-array name="episode_hide_values"> + <item>unplayed</item> + <item>paused</item> + <item>played</item> + <item>queued</item> + <item>not_queued</item> + <item>downloaded</item> + <item>not_downloaded</item> + </string-array> + </resources> diff --git a/core/src/main/res/values/attrs.xml b/core/src/main/res/values/attrs.xml index 368921f76..2bdda2378 100644 --- a/core/src/main/res/values/attrs.xml +++ b/core/src/main/res/values/attrs.xml @@ -37,6 +37,9 @@ <attr name="av_ff_big" format="reference"/> <attr name="av_rew_big" format="reference"/> <attr name="ic_settings" format="reference"/> + <attr name="ic_lock_open" format="reference"/> + <attr name="ic_lock_closed" format="reference"/> + <attr name="ic_filter" format="reference"/> <!-- Used in itemdescription --> <attr name="non_transparent_background" format="reference"/> diff --git a/core/src/main/res/values/dimens.xml b/core/src/main/res/values/dimens.xml index 81a55142a..c46537b3e 100644 --- a/core/src/main/res/values/dimens.xml +++ b/core/src/main/res/values/dimens.xml @@ -16,10 +16,10 @@ <dimen name="thumbnail_length_downloaded_item">64dp</dimen> <dimen name="thumbnail_length_onlinefeedview">100dp</dimen> <dimen name="feeditemlist_header_height">132dp</dimen> - <dimen name="thumbnail_length_navlist">42dp</dimen> + <dimen name="thumbnail_length_navlist">40dp</dimen> <dimen name="listview_secondary_button_width">48dp</dimen> <dimen name="drawer_width">280dp</dimen> - <dimen name="listitem_iconwithtext_height">56dp</dimen> + <dimen name="listitem_iconwithtext_height">48dp</dimen> <dimen name="listitem_iconwithtext_textleftpadding">14dp</dimen> <dimen name="listitem_iconwithtext_textverticalpadding">16dp</dimen> diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index bdb3ad606..c6308fa7b 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -32,6 +32,7 @@ <!-- Main activity --> <string name="drawer_open">Open menu</string> <string name="drawer_close">Close menu</string> + <string name="drawer_preferences">Drawer Preferences</string> <!-- Webview actions --> <string name="open_in_browser_label">Open in browser</string> @@ -48,6 +49,7 @@ <string name="cancel_label">Cancel</string> <string name="author_label">Author</string> <string name="language_label">Language</string> + <string name="url_label">URL</string> <string name="podcast_settings_label">Settings</string> <string name="cover_label">Picture</string> <string name="error_label">Error</string> @@ -78,10 +80,10 @@ <string name="browse_gpoddernet_label">Browse gpodder.net</string> <!-- Actions on feeds --> - <string name="mark_all_read_label">Mark all as read</string> - <string name="mark_all_read_msg">Marked all episodes as read</string> - <string name="mark_all_read_confirmation_msg">Please confirm that you want to mark all episodes as being read.</string> - <string name="mark_all_read_feed_confirmation_msg">Please confirm that you want to mark all episodes in this feed as being read.</string> + <string name="mark_all_read_label">Mark all as played</string> + <string name="mark_all_read_msg">Marked all episodes as played</string> + <string name="mark_all_read_confirmation_msg">Please confirm that you want to mark all episodes as being played.</string> + <string name="mark_all_read_feed_confirmation_msg">Please confirm that you want to mark all episodes in this feed as being played.</string> <string name="show_info_label">Show information</string> <string name="remove_feed_label">Remove podcast</string> <string name="share_link_label">Share website link</string> @@ -89,6 +91,16 @@ <string name="feed_delete_confirmation_msg">Please confirm that you want to delete this feed and ALL episodes of this feed that you have downloaded.</string> <string name="feed_remover_msg">Removing feed</string> <string name="load_complete_feed">Refresh complete feed</string> + <string name="hide_episodes_title">Hide episodes</string> + <string name="hide_unplayed_episodes_label">Unplayed</string> + <string name="hide_paused_episodes_label">Paused</string> + <string name="hide_played_episodes_label">Played</string> + <string name="hide_queued_episodes_label">Queued</string> + <string name="hide_not_queued_episodes_label">Not queued</string> + <string name="hide_downloaded_episodes_label">Downloaded</string> + <string name="hide_not_downloaded_episodes_label">Not downloaded</string> + <string name="filtered_label">Filtered</string> + <string name="refresh_failed_msg">{fa-exclamation-circle} Last refresh failed</string> <!-- actions on feeditems --> <string name="download_label">Download</string> @@ -102,12 +114,16 @@ <string name="mark_unread_label">Mark as unplayed</string> <string name="marked_as_read_label">Marked as played</string> <string name="add_to_queue_label">Add to Queue</string> + <string name="added_to_queue_label">Added to Queue</string> <string name="remove_from_queue_label">Remove from Queue</string> <string name="visit_website_label">Visit Website</string> <string name="support_label">Flattr this</string> <string name="enqueue_all_new">Enqueue all</string> <string name="download_all">Download all</string> <string name="skip_episode_label">Skip episode</string> + <string name="activate_auto_download">Activate auto download</string> + <string name="deactivate_auto_download">Deactivate auto download</string> + <string name="reset_position">Reset playback position</string> <!-- Download messages and labels --> <string name="download_successful">successful</string> @@ -125,8 +141,10 @@ <string name="download_error_unknown_host">Unknown host</string> <string name="download_error_unauthorized">Authentication error</string> <string name="cancel_all_downloads_label">Cancel all downloads</string> - <string name="download_cancelled_msg">Download cancelled</string> - <string name="download_report_title">Downloads completed</string> + <string name="download_canceled_msg">Download canceled</string> + <string name="download_canceled_autodownload_enabled_msg">Download canceled\nDisabled <i>Auto Download</i> for this item</string> + <string name="download_report_title">Downloads completed with error(s)</string> + <string name="download_report_content_title">Download report</string> <string name="download_error_malformed_url">Malformed URL</string> <string name="download_error_io_error">IO Error</string> <string name="download_error_request_error">Request error</string> @@ -142,6 +160,11 @@ <string name="download_request_error_dialog_message_prefix">An error occurred when trying to download the file:\u0020</string> <string name="authentication_notification_title">Authentication required</string> <string name="authentication_notification_msg">The resource you requested requires a username and a password</string> + <string name="confirm_mobile_download_dialog_title">Confirm Mobile Download</string> + <string name="confirm_mobile_download_dialog_message_not_in_queue">Downloading over mobile data connection is disabled in the settings.\n\nEnable temporarily or just add to queue?\n\n<small>Your choice will be remember for 10 minutes.</small></string> + <string name="confirm_mobile_download_dialog_message">Downloading over mobile data connection is disabled in the settings.\n\nEnable temporarily?\n\n<small>Your choice will be remember for 10 minutes.</small></string> + <string name="confirm_mobile_download_dialog_only_add_to_queue">Only add to Queue</string> + <string name="confirm_mobile_download_dialog_enable_temporarily">Enable temporarily</string> <!-- Mediaplayer messages --> <string name="player_error_msg">Error!</string> @@ -158,6 +181,8 @@ <string name="unknown_media_key">AntennaPod - Unknown media key: %1$d</string> <!-- Queue operations --> + <string name="lock_queue">Lock queue</string> + <string name="unlock_queue">Unlock queue</string> <string name="clear_queue_label">Clear queue</string> <string name="undo">Undo</string> <string name="removed_from_queue">Item removed</string> @@ -245,6 +270,8 @@ <string name="pref_auto_flattr_sum">Configure automatic flattring</string> <string name="user_interface_label">User Interface</string> <string name="pref_set_theme_title">Select theme</string> + <string name="pref_nav_drawer_items_title">Change navigation drawer</string> + <string name="pref_nav_drawer_items_sum">Change which items appear in the navigation drawer.</string> <string name="pref_set_theme_sum">Change the appearance of AntennaPod.</string> <string name="pref_automatic_download_title">Automatic download</string> <string name="pref_automatic_download_sum">Configure the automatic download of episodes.</string> @@ -268,8 +295,8 @@ <string name="pref_gpodnet_setlogin_information_sum">Change the login information for your gpodder.net account.</string> <string name="pref_playback_speed_title">Playback Speeds</string> <string name="pref_playback_speed_sum">Customize the speeds available for variable speed audio playback</string> - <string name="pref_seek_delta_title">Seek time</string> - <string name="pref_seek_delta_sum">Seek this many seconds when rewinding or fast-forwarding</string> + <string name="pref_fast_forward">Fast forward time</string> + <string name="pref_rewind">Rewind time</string> <string name="pref_gpodnet_sethostname_title">Set hostname</string> <string name="pref_gpodnet_sethostname_use_default_host">Use default host</string> <string name="pref_expandNotify_title">Expand Notification</string> @@ -372,6 +399,8 @@ <string name="set_to_default_folder">Choose default folder</string> <string name="pref_pausePlaybackForFocusLoss_sum">Pause playback instead of lowering volume when another app wants to play sounds</string> <string name="pref_pausePlaybackForFocusLoss_title">Pause for interruptions</string> + <string name="pref_resumeAfterCall_sum">Resume playback after a phone call completes</string> + <string name="pref_resumeAfterCall_title">Resume after call</string> <!-- Online feed view --> <string name="subscribe_label">Subscribe</string> diff --git a/core/src/main/res/values/styles.xml b/core/src/main/res/values/styles.xml index 4ac4a79fd..8619869c8 100644 --- a/core/src/main/res/values/styles.xml +++ b/core/src/main/res/values/styles.xml @@ -42,6 +42,9 @@ <item name="attr/av_ff_big">@drawable/ic_fast_forward_grey600_36dp</item> <item name="attr/av_rew_big">@drawable/ic_fast_rewind_grey600_36dp</item> <item name="attr/ic_settings">@drawable/ic_settings_grey600_24dp</item> + <item name="attr/ic_lock_open">@drawable/ic_lock_open_grey600_24dp</item> + <item name="attr/ic_lock_closed">@drawable/ic_lock_closed_grey600_24dp</item> + <item name="attr/ic_filter">@drawable/ic_filter_grey600_24dp</item> </style> <style name="Theme.AntennaPod.Dark" parent="@style/Theme.AppCompat"> @@ -84,6 +87,9 @@ <item name="attr/av_ff_big">@drawable/ic_fast_forward_white_36dp</item> <item name="attr/av_rew_big">@drawable/ic_fast_rewind_white_36dp</item> <item name="attr/ic_settings">@drawable/ic_settings_white_24dp</item> + <item name="attr/ic_lock_open">@drawable/ic_lock_open_white_24dp</item> + <item name="attr/ic_lock_closed">@drawable/ic_lock_closed_white_24dp</item> + <item name="attr/ic_filter">@drawable/ic_filter_white_24dp</item> </style> <style name="Theme.AntennaPod.Light.NoTitle" parent="@style/Theme.AppCompat.Light.NoActionBar"> @@ -129,6 +135,9 @@ <item name="attr/av_ff_big">@drawable/ic_fast_forward_grey600_36dp</item> <item name="attr/av_rew_big">@drawable/ic_fast_rewind_grey600_36dp</item> <item name="attr/ic_settings">@drawable/ic_settings_grey600_24dp</item> + <item name="attr/ic_lock_open">@drawable/ic_lock_open_grey600_24dp</item> + <item name="attr/ic_lock_closed">@drawable/ic_lock_closed_grey600_24dp</item> + <item name="attr/ic_filter">@drawable/ic_filter_grey600_24dp</item> </style> <style name="Theme.AntennaPod.Dark.NoTitle" parent="@style/Theme.AppCompat.NoActionBar"> @@ -173,6 +182,9 @@ <item name="attr/av_ff_big">@drawable/ic_fast_forward_white_36dp</item> <item name="attr/av_rew_big">@drawable/ic_fast_rewind_white_36dp</item> <item name="attr/ic_settings">@drawable/ic_settings_white_24dp</item> + <item name="attr/ic_lock_open">@drawable/ic_lock_open_white_24dp</item> + <item name="attr/ic_lock_closed">@drawable/ic_lock_closed_white_24dp</item> + <item name="attr/ic_filter">@drawable/ic_filter_white_24dp</item> </style> <style name="Theme.AntennaPod.VideoPlayer" parent="@style/Theme.AntennaPod.Dark"> diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar Binary files differindex 3d0dee6e8..b5166dad4 100644 --- a/gradle/wrapper/gradle-wrapper.jar +++ b/gradle/wrapper/gradle-wrapper.jar diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 5909f8dfb..fb3375000 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Fri Nov 28 16:00:13 CET 2014 +#Tue May 19 11:59:21 CEST 2015 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.4-all.zip diff --git a/gradlew.bat b/gradlew.bat index 8a0b282aa..aec99730b 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,90 +1,90 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windowz variants - -if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/DragSortController.java b/library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/DragSortController.java index 6acf6b42e..fef853d22 100644 --- a/library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/DragSortController.java +++ b/library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/DragSortController.java @@ -378,6 +378,12 @@ public class DragSortController extends SimpleFloatViewManager implements View.O @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + // it can happen where the motion events are null + if (e1 == null || e2 == null) { + // we can't really do anything + return false; + } + final int x1 = (int) e1.getX(); final int y1 = (int) e1.getY(); final int x2 = (int) e2.getX(); |