diff options
Diffstat (limited to 'core')
114 files changed, 4200 insertions, 1462 deletions
diff --git a/core/build.gradle b/core/build.gradle index 710378a18..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,10 @@ 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/tests/util/DateUtilsTest.java b/core/src/androidTest/java/de/danoeh/antennapod/core/tests/util/DateUtilsTest.java new file mode 100644 index 000000000..2a2d6414a --- /dev/null +++ b/core/src/androidTest/java/de/danoeh/antennapod/core/tests/util/DateUtilsTest.java @@ -0,0 +1,77 @@ +package de.danoeh.antennapod.core.tests.util; + + +import android.test.AndroidTestCase; + +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 { + GregorianCalendar exp = new GregorianCalendar(2015, 2, 28, 13, 31, 4); + Date expected = new Date(exp.getTimeInMillis() + 963); + Date actual = DateUtils.parse("2015-03-28T13:31:04.963870"); + assertEquals(expected, actual); + } + + public void testParseDateWithCentiseconds() throws Exception { + GregorianCalendar exp = new GregorianCalendar(2015, 2, 28, 13, 31, 4); + Date expected = new Date(exp.getTimeInMillis() + 960); + Date actual = DateUtils.parse("2015-03-28T13:31:04.96"); + assertEquals(expected, actual); + } + + public void testParseDateWithDeciseconds() throws Exception { + GregorianCalendar exp = new GregorianCalendar(2015, 2, 28, 13, 31, 4); + Date expected = new Date(exp.getTimeInMillis() + 900); + Date actual = DateUtils.parse("2015-03-28T13:31:04.9"); + assertEquals(expected.getTime()/1000, actual.getTime()/1000); + assertEquals(900, actual.getTime()%1000); + } + + public void testParseDateWithMicrosecondsAndTimezone() throws Exception { + GregorianCalendar exp = new GregorianCalendar(2015, 2, 28, 6, 31, 4); + exp.setTimeZone(TimeZone.getTimeZone("UTC")); + Date expected = new Date(exp.getTimeInMillis() + 963); + Date actual = DateUtils.parse("2015-03-28T13:31:04.963870 +0700"); + assertEquals(expected, actual); + } + + public void testParseDateWithCentisecondsAndTimezone() throws Exception { + GregorianCalendar exp = new GregorianCalendar(2015, 2, 28, 6, 31, 4); + exp.setTimeZone(TimeZone.getTimeZone("UTC")); + Date expected = new Date(exp.getTimeInMillis() + 960); + Date actual = DateUtils.parse("2015-03-28T13:31:04.96 +0700"); + assertEquals(expected, actual); + } + + public void testParseDateWithDecisecondsAndTimezone() throws Exception { + GregorianCalendar exp = new GregorianCalendar(2015, 2, 28, 6, 31, 4); + exp.setTimeZone(TimeZone.getTimeZone("UTC")); + Date expected = new Date(exp.getTimeInMillis() + 900); + Date actual = DateUtils.parse("2015-03-28T13:31:04.9 +0700"); + assertEquals(expected.getTime()/1000, actual.getTime()/1000); + 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/com/aocate/media/AndroidMediaPlayer.java b/core/src/main/java/com/aocate/media/AndroidMediaPlayer.java index 17ee74a13..7c2ea3d61 100644 --- a/core/src/main/java/com/aocate/media/AndroidMediaPlayer.java +++ b/core/src/main/java/com/aocate/media/AndroidMediaPlayer.java @@ -14,13 +14,13 @@ package com.aocate.media; -import java.io.IOException; - import android.content.Context; import android.media.MediaPlayer; import android.net.Uri; import android.util.Log; +import java.io.IOException; + public class AndroidMediaPlayer extends MediaPlayerImpl { private final static String AMP_TAG = "AocateAndroidMediaPlayer"; 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/EventDistributor.java b/core/src/main/java/de/danoeh/antennapod/core/feed/EventDistributor.java index 5a2cfa40e..20a85d43f 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/EventDistributor.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/EventDistributor.java @@ -5,8 +5,6 @@ import android.util.Log; import org.apache.commons.lang3.Validate; -import de.danoeh.antennapod.core.BuildConfig; - import java.util.AbstractQueue; import java.util.Observable; import java.util.Observer; @@ -26,7 +24,6 @@ public class EventDistributor extends Observable { public static final int FEED_LIST_UPDATE = 1; public static final int UNREAD_ITEMS_UPDATE = 2; - public static final int QUEUE_UPDATE = 4; public static final int DOWNLOADLOG_UPDATE = 8; public static final int PLAYBACK_HISTORY_UPDATE = 16; public static final int DOWNLOAD_QUEUED = 32; @@ -71,23 +68,17 @@ public class EventDistributor extends Observable { private void processEventQueue() { Integer result = 0; - if (BuildConfig.DEBUG) - Log.d(TAG, - "Processing event queue. Number of events: " - + events.size()); + Log.d(TAG, "Processing event queue. Number of events: " + events.size()); for (Integer current = events.poll(); current != null; current = events .poll()) { result |= current; } if (result != 0) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Notifying observers. Data: " + result); + Log.d(TAG, "Notifying observers. Data: " + result); setChanged(); notifyObservers(result); } else { - if (BuildConfig.DEBUG) - Log.d(TAG, - "Event queue didn't contain any new events. Observers will not be notified."); + Log.d(TAG, "Event queue didn't contain any new events. Observers will not be notified."); } } @@ -105,10 +96,6 @@ public class EventDistributor extends Observable { addEvent(UNREAD_ITEMS_UPDATE); } - public void sendQueueUpdateBroadcast() { - addEvent(QUEUE_UPDATE); - } - public void sendFeedUpdateBroadcast() { addEvent(FEED_LIST_UPDATE); } 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 8860653a1..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); } /** @@ -516,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 5a4d869e7..1168c60e4 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 @@ -2,6 +2,9 @@ package de.danoeh.antennapod.core.feed; import android.net.Uri; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + import java.util.Date; import java.util.List; import java.util.concurrent.Callable; @@ -60,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(); @@ -71,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; @@ -83,6 +88,7 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr this.image = image; this.read = read; this.itemIdentifier = itemIdentifier; + this.autoDownload = autoDownload; } /** @@ -233,7 +239,7 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr } public boolean isRead() { - return read || isInProgress(); + return read; } public void setRead(boolean read) { @@ -312,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 { @@ -324,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() { @@ -336,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() { @@ -384,4 +390,24 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr public boolean hasChapters() { 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() && + 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 69e96c503..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; @@ -12,6 +13,7 @@ import java.util.concurrent.Callable; import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.preferences.PlaybackPreferences; +import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.util.ChapterUtils; @@ -33,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; @@ -49,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; @@ -60,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) { @@ -146,6 +144,11 @@ public class FeedMedia extends FeedFile implements Playable { } + public boolean hasAlmostEnded() { + int smartMarkAsPlayedSecs = UserPreferences.getSmartMarkAsPlayedSecs(); + return this.position >= this.duration - smartMarkAsPlayedSecs * 1000; + } + @Override public int getTypeAsInt() { return FEEDFILETYPE_FEEDMEDIA; @@ -221,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); @@ -409,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 new file mode 100644 index 000000000..c8497f509 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/QueueEvent.java @@ -0,0 +1,51 @@ +package de.danoeh.antennapod.core.feed; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.util.List; + +public class QueueEvent { + + public enum Action { + ADDED, ADDED_ITEMS, REMOVED, CLEARED, DELETED_MEDIA, SORTED, MOVED + } + + public final Action action; + public final FeedItem item; + public final int position; + public final List<FeedItem> items; + + public QueueEvent(Action action) { + this(action, null, null, -1); + } + + public QueueEvent(Action action, FeedItem item) { + this(action, item, null, -1); + } + + public QueueEvent(Action action, FeedItem item, int position) { + this(action, item, null, position); + } + + public QueueEvent(Action action, List<FeedItem> items) { + this(action, null, items, -1); + } + + private QueueEvent(Action action, FeedItem item, List<FeedItem> items, int position) { + this.action = action; + this.item = item; + this.items = items; + this.position = position; + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) + .append("action", action) + .append("item", item) + .append("items", items) + .append("position", position) + .toString(); + } +} 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 4d88449d6..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 @@ -45,6 +45,9 @@ import javax.security.auth.x500.X500Principal; import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.gpoddernet.model.GpodnetDevice; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeActionGetResponse; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeActionPostResponse; import de.danoeh.antennapod.core.gpoddernet.model.GpodnetPodcast; import de.danoeh.antennapod.core.gpoddernet.model.GpodnetSubscriptionChange; import de.danoeh.antennapod.core.gpoddernet.model.GpodnetTag; @@ -462,6 +465,85 @@ public class GpodnetService { } /** + * Updates the episode actions + * <p/> + * This method requires authentication. + * + * @param episodeActions Collection of episode actions. + * @return a GpodnetUploadChangesResponse. See {@link de.danoeh.antennapod.core.gpoddernet.model.GpodnetUploadChangesResponse} + * for details. + * @throws java.lang.IllegalArgumentException if username, deviceId, added or removed is null. + * @throws de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException if added or removed contain duplicates or if there + * is an authentication error. + */ + public GpodnetEpisodeActionPostResponse uploadEpisodeActions(Collection<GpodnetEpisodeAction> episodeActions) + throws GpodnetServiceException { + + Validate.notNull(episodeActions); + + String username = GpodnetPreferences.getUsername(); + + try { + URL url = new URI(BASE_SCHEME, BASE_HOST, String.format( + "/api/2/episodes/%s.json", username), null).toURL(); + + final JSONArray list = new JSONArray(); + for(GpodnetEpisodeAction episodeAction : episodeActions) { + JSONObject obj = episodeAction.writeToJSONObject(); + if(obj != null) { + list.put(obj); + } + } + + RequestBody body = RequestBody.create(JSON, list.toString()); + Request.Builder request = new Request.Builder().post(body).url(url); + + final String response = executeRequest(request); + return GpodnetEpisodeActionPostResponse.fromJSONObject(response); + } catch (JSONException | MalformedURLException | URISyntaxException e) { + e.printStackTrace(); + throw new GpodnetServiceException(e); + } + + } + + /** + * Returns all subscription changes of a specific device. + * <p/> + * This method requires authentication. + * + * @param timestamp A timestamp that can be used to receive all changes since a + * specific point in time. + * @throws IllegalArgumentException If username or deviceId is null. + * @throws GpodnetServiceAuthenticationException If there is an authentication error. + */ + public GpodnetEpisodeActionGetResponse getEpisodeChanges(long timestamp) throws GpodnetServiceException { + + String username = GpodnetPreferences.getUsername(); + + String params = String.format("since=%d", timestamp); + String path = String.format("/api/2/episodes/%s.json", + username); + try { + URL url = new URI(BASE_SCHEME, null, BASE_HOST, -1, path, params, + null).toURL(); + Request.Builder request = new Request.Builder().url(url); + + String response = executeRequest(request); + JSONObject json = new JSONObject(response); + return readEpisodeActionsFromJSONObject(json); + } catch (URISyntaxException e) { + e.printStackTrace(); + throw new IllegalStateException(e); + } catch (JSONException | MalformedURLException e) { + e.printStackTrace(); + throw new GpodnetServiceException(e); + } + + } + + + /** * Logs in a specific user. This method must be called if any of the methods * that require authentication is used. * @@ -569,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 { @@ -698,4 +785,24 @@ public class GpodnetService { long timestamp = object.getLong("timestamp"); return new GpodnetSubscriptionChange(added, removed, timestamp); } + + private GpodnetEpisodeActionGetResponse readEpisodeActionsFromJSONObject( + JSONObject object) throws JSONException { + Validate.notNull(object); + + List<GpodnetEpisodeAction> episodeActions = new ArrayList<GpodnetEpisodeAction>(); + + long timestamp = object.getLong("timestamp"); + JSONArray jsonActions = object.getJSONArray("actions"); + for(int i=0; i < jsonActions.length(); i++) { + JSONObject jsonAction = jsonActions.getJSONObject(i); + GpodnetEpisodeAction episodeAction = GpodnetEpisodeAction.readFromJSONObject(jsonAction); + if(episodeAction != null) { + episodeActions.add(episodeAction); + } + } + return new GpodnetEpisodeActionGetResponse(episodeActions, timestamp); + } + + } 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 new file mode 100644 index 000000000..bd6210d13 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetEpisodeAction.java @@ -0,0 +1,315 @@ +package de.danoeh.antennapod.core.gpoddernet.model; + + +import android.util.Log; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import org.json.JSONException; +import org.json.JSONObject; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.preferences.GpodnetPreferences; +import de.danoeh.antennapod.core.util.DateUtils; + +public class GpodnetEpisodeAction { + + private static final String TAG = "GpodnetEpisodeAction"; + + public enum Action { + NEW, DOWNLOAD, PLAY, DELETE + } + + private final String podcast; + private final String episode; + private final String deviceId; + private final Action action; + private final Date timestamp; + private final int started; + private final int position; + private final int total; + + private GpodnetEpisodeAction(Builder builder) { + this.podcast = builder.podcast; + this.episode = builder.episode; + this.action = builder.action; + this.deviceId = builder.deviceId; + this.timestamp = builder.timestamp; + this.started = builder.started; + this.position = builder.position; + this.total = builder.total; + } + + /** + * Creates an episode action object from a String representation. The representation includes + * all mandatory and optional attributes + * + * @param s String representation (output from {@link #writeToString()}) + * @return episode action object, or null if s is invalid + */ + public static GpodnetEpisodeAction readFromString(String s) { + String[] fields = s.split("\t"); + if(fields.length != 8) { + return null; + } + String podcast = fields[0]; + String episode = fields[1]; + String deviceId = fields[2]; + try { + Action action = Action.valueOf(fields[3]); + GpodnetEpisodeAction result = new Builder(podcast, episode, action) + .deviceId(deviceId) + .timestamp(new Date(Long.valueOf(fields[4]))) + .started(Integer.valueOf(fields[5])) + .position(Integer.valueOf(fields[6])) + .total(Integer.valueOf(fields[7])) + .build(); + return result; + } catch(IllegalArgumentException e) { + Log.e(TAG, "readFromString(" + s + "): " + e.getMessage()); + return null; + } + } + + /** + * Create an episode action object from JSON representation. Mandatory fields are "podcast", + * "episode" and "action". + * + * @param object JSON representation + * @return episode action object, or null if mandatory values are missing + */ + public static GpodnetEpisodeAction readFromJSONObject(JSONObject object) { + String podcast = object.optString("podcast", null); + String episode = object.optString("episode", null); + String actionString = object.optString("action", null); + if(StringUtils.isEmpty(podcast) || StringUtils.isEmpty(episode) || StringUtils.isEmpty(actionString)) { + return null; + } + GpodnetEpisodeAction.Action action = GpodnetEpisodeAction.Action.valueOf(actionString.toUpperCase()); + String deviceId = object.optString("device", ""); + GpodnetEpisodeAction.Builder builder = new GpodnetEpisodeAction.Builder(podcast, episode, action) + .deviceId(deviceId); + String utcTimestamp = object.optString("timestamp", null); + if(StringUtils.isNotEmpty(utcTimestamp)) { + builder.timestamp(DateUtils.parse(utcTimestamp)); + } + if(action == GpodnetEpisodeAction.Action.PLAY) { + int started = object.optInt("started", -1); + int position = object.optInt("position", -1); + int total = object.optInt("total", -1); + if(started >= 0 && position > 0 && total > 0) { + builder + .started(started) + .position(position) + .total(total); + } + } + return builder.build(); + } + + public String getPodcast() { + return this.podcast; + } + + public String getEpisode() { + return this.episode; + } + + public String getDeviceId() { + return this.deviceId; + } + + public Action getAction() { + return this.action; + } + + public String getActionString() { + return this.action.name().toLowerCase(); + } + + public Date getTimestamp() { + return this.timestamp; + } + + /** + * Returns the position (in seconds) at which the client started playback + * + * @return start position (in seconds) + */ + public int getStarted() { + return this.started; + } + + /** + * Returns the position (in seconds) at which the client stopped playback + * + * @return stop position (in seconds) + */ + public int getPosition() { + return this.position; + } + + /** + * Returns the total length of the file in seconds. + * + * @return total length in seconds + */ + public int getTotal() { + return this.total; + } + + @Override + public boolean equals(Object o) { + if(o == null) return false; + if(this == o) return true; + if(this.getClass() != o.getClass()) return false; + GpodnetEpisodeAction that = (GpodnetEpisodeAction)o; + return new EqualsBuilder() + .append(this.podcast, that.podcast) + .append(this.episode, that.episode) + .append(this.deviceId, that.deviceId) + .append(this.action, that.action) + .append(this.timestamp, that.timestamp) + .append(this.started, that.started) + .append(this.position, that.position) + .append(this.total, that.total) + .isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder() + .append(this.podcast) + .append(this.episode) + .append(this.deviceId) + .append(this.action) + .append(this.timestamp) + .append(this.started) + .append(this.position) + .append(this.total) + .toHashCode(); + } + + public String writeToString() { + StringBuilder result = new StringBuilder(); + result.append(this.podcast).append("\t"); + result.append(this.episode).append("\t"); + result.append(this.deviceId).append("\t"); + result.append(this.action).append("\t"); + result.append(this.timestamp.getTime()).append("\t"); + result.append(String.valueOf(this.started)).append("\t"); + result.append(String.valueOf(this.position)).append("\t"); + result.append(String.valueOf(this.total)); + return result.toString(); + } + + /** + * Returns a JSON object representation of this object + * + * @return JSON object representation, or null if the object is invalid + */ + public JSONObject writeToJSONObject() { + JSONObject obj = new JSONObject(); + try { + obj.putOpt("podcast", this.podcast); + obj.putOpt("episode", this.episode); + obj.put("device", this.deviceId); + obj.put("action", this.getActionString()); + SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US); + formatter.setTimeZone(TimeZone.getTimeZone("UTC")); + obj.put("timestamp",formatter.format(this.timestamp)); + if (this.getAction() == Action.PLAY) { + obj.put("started", this.started); + obj.put("position", this.position); + obj.put("total", this.total); + } + } catch(JSONException e) { + Log.e(TAG, "writeToJSONObject(): " + e.getMessage()); + return null; + } + return obj; + } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); + } + + public static class Builder { + + // mandatory + private final String podcast; + private final String episode; + private final Action action; + + // optional + private String deviceId = ""; + private Date timestamp; + private int started = -1; + private int position = -1; + private int total = -1; + + public Builder(FeedItem item, Action action) { + this(item.getFeed().getDownload_url(), item.getMedia().getDownload_url(), action); + } + + public Builder(String podcast, String episode, Action action) { + this.podcast = podcast; + this.episode = episode; + this.action = action; + } + + public Builder deviceId(String deviceId) { + this.deviceId = deviceId; + return this; + } + + public Builder currentDeviceId() { + return deviceId(GpodnetPreferences.getDeviceID()); + } + + public Builder timestamp(Date timestamp) { + this.timestamp = timestamp; + return this; + } + + public Builder currentTimestamp() { + return timestamp(new Date()); + } + + public Builder started(int seconds) { + if(action == Action.PLAY) { + this.started = seconds; + } + return this; + } + + public Builder position(int seconds) { + if(action == Action.PLAY) { + this.position = seconds; + } + return this; + } + + public Builder total(int seconds) { + if(action == Action.PLAY) { + this.total = seconds; + } + return this; + } + + public GpodnetEpisodeAction build() { + return new GpodnetEpisodeAction(this); + } + + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetEpisodeActionGetResponse.java b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetEpisodeActionGetResponse.java new file mode 100644 index 000000000..50420f0a3 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetEpisodeActionGetResponse.java @@ -0,0 +1,34 @@ +package de.danoeh.antennapod.core.gpoddernet.model; + + +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.util.List; + +public class GpodnetEpisodeActionGetResponse { + + private final List<GpodnetEpisodeAction> episodeActions; + private final long timestamp; + + public GpodnetEpisodeActionGetResponse(List<GpodnetEpisodeAction> episodeActions, long timestamp) { + Validate.notNull(episodeActions); + this.episodeActions = episodeActions; + this.timestamp = timestamp; + } + + public List<GpodnetEpisodeAction> getEpisodeActions() { + return this.episodeActions; + } + + public long getTimestamp() { + return this.timestamp; + } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetEpisodeActionPostResponse.java b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetEpisodeActionPostResponse.java new file mode 100644 index 000000000..e06a88d5c --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetEpisodeActionPostResponse.java @@ -0,0 +1,53 @@ +package de.danoeh.antennapod.core.gpoddernet.model; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.HashMap; +import java.util.Map; + +public class GpodnetEpisodeActionPostResponse { + + /** + * timestamp/ID that can be used for requesting changes since this upload. + */ + public final long timestamp; + + /** + * URLs that should be updated. The key of the map is the original URL, the value of the map + * is the sanitized URL. + */ + public final Map<String, String> updatedUrls; + + public GpodnetEpisodeActionPostResponse(long timestamp, Map<String, String> updatedUrls) { + this.timestamp = timestamp; + this.updatedUrls = updatedUrls; + } + + /** + * Creates a new GpodnetUploadChangesResponse-object from a JSON object that was + * returned by an uploadChanges call. + * + * @throws org.json.JSONException If the method could not parse the JSONObject. + */ + public static GpodnetEpisodeActionPostResponse fromJSONObject(String objectString) throws JSONException { + final JSONObject object = new JSONObject(objectString); + final long timestamp = object.getLong("timestamp"); + Map<String, String> updatedUrls = new HashMap<String, String>(); + JSONArray urls = object.getJSONArray("update_urls"); + for (int i = 0; i < urls.length(); i++) { + JSONArray urlPair = urls.getJSONArray(i); + updatedUrls.put(urlPair.getString(0), urlPair.getString(1)); + } + return new GpodnetEpisodeActionPostResponse(timestamp, updatedUrls); + } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); + } +} + 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 af04df017..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 @@ -4,14 +4,20 @@ import android.content.Context; import android.content.SharedPreferences; import android.util.Log; +import org.apache.commons.lang3.StringUtils; + +import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Set; import java.util.concurrent.locks.ReentrantLock; import de.danoeh.antennapod.core.BuildConfig; import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.gpoddernet.GpodnetService; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction; import de.danoeh.antennapod.core.service.GpodnetSyncService; /** @@ -28,9 +34,11 @@ public class GpodnetPreferences { public static final String PREF_GPODNET_HOSTNAME = "prefGpodnetHostname"; - public static final String PREF_LAST_SYNC_TIMESTAMP = "de.danoeh.antennapod.preferences.gpoddernet.last_sync_timestamp"; + public static final String PREF_LAST_SUBSCRIPTION_SYNC_TIMESTAMP = "de.danoeh.antennapod.preferences.gpoddernet.last_sync_timestamp"; + public static final String PREF_LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP = "de.danoeh.antennapod.preferences.gpoddernet.last_episode_actions_sync_timestamp"; public static final String PREF_SYNC_ADDED = "de.danoeh.antennapod.preferences.gpoddernet.sync_added"; public static final String PREF_SYNC_REMOVED = "de.danoeh.antennapod.preferences.gpoddernet.sync_removed"; + public static final String PREF_SYNC_EPISODE_ACTIONS = "de.danoeh.antennapod.preferences.gpoddernet.sync_queued_episode_actions"; private static String username; private static String password; @@ -41,10 +49,14 @@ public class GpodnetPreferences { private static Set<String> addedFeeds; private static Set<String> removedFeeds; + private static List<GpodnetEpisodeAction> queuedEpisodeActions; + /** * Last value returned by getSubscriptionChanges call. Will be used for all subsequent calls of getSubscriptionChanges. */ - private static long lastSyncTimestamp; + private static long lastSubscriptionSyncTimestamp; + + private static long lastEpisodeActionsSyncTimeStamp; private static boolean preferencesLoaded = false; @@ -58,9 +70,11 @@ public class GpodnetPreferences { username = prefs.getString(PREF_GPODNET_USERNAME, null); password = prefs.getString(PREF_GPODNET_PASSWORD, null); deviceID = prefs.getString(PREF_GPODNET_DEVICEID, null); - lastSyncTimestamp = prefs.getLong(PREF_LAST_SYNC_TIMESTAMP, 0); + lastSubscriptionSyncTimestamp = prefs.getLong(PREF_LAST_SUBSCRIPTION_SYNC_TIMESTAMP, 0); + lastEpisodeActionsSyncTimeStamp = prefs.getLong(PREF_LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP, 0); addedFeeds = readListFromString(prefs.getString(PREF_SYNC_ADDED, "")); removedFeeds = readListFromString(prefs.getString(PREF_SYNC_REMOVED, "")); + queuedEpisodeActions = readEpisodeActionsFromString(prefs.getString(PREF_SYNC_EPISODE_ACTIONS, "")); hostname = checkGpodnetHostname(prefs.getString(PREF_GPODNET_HOSTNAME, GpodnetService.DEFAULT_BASE_HOST)); preferencesLoaded = true; @@ -115,14 +129,24 @@ public class GpodnetPreferences { writePreference(PREF_GPODNET_DEVICEID, deviceID); } - public static long getLastSyncTimestamp() { + public static long getLastSubscriptionSyncTimestamp() { + ensurePreferencesLoaded(); + return lastSubscriptionSyncTimestamp; + } + + public static void setLastSubscriptionSyncTimestamp(long timestamp) { + GpodnetPreferences.lastSubscriptionSyncTimestamp = timestamp; + writePreference(PREF_LAST_SUBSCRIPTION_SYNC_TIMESTAMP, timestamp); + } + + public static long getLastEpisodeActionsSyncTimestamp() { ensurePreferencesLoaded(); - return lastSyncTimestamp; + return lastEpisodeActionsSyncTimeStamp; } - public static void setLastSyncTimestamp(long lastSyncTimestamp) { - GpodnetPreferences.lastSyncTimestamp = lastSyncTimestamp; - writePreference(PREF_LAST_SYNC_TIMESTAMP, lastSyncTimestamp); + public static void setLastEpisodeActionsSyncTimestamp(long timestamp) { + GpodnetPreferences.lastEpisodeActionsSyncTimeStamp = timestamp; + writePreference(PREF_LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP, timestamp); } public static String getHostname() { @@ -149,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) { @@ -162,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() { @@ -195,7 +219,24 @@ public class GpodnetPreferences { ensurePreferencesLoaded(); removedFeeds.removeAll(removed); writePreference(PREF_SYNC_REMOVED, removedFeeds); + } + + public static synchronized void enqueueEpisodeAction(GpodnetEpisodeAction action) { + ensurePreferencesLoaded(); + queuedEpisodeActions.add(action); + writePreference(PREF_SYNC_EPISODE_ACTIONS, writeEpisodeActionsToString(queuedEpisodeActions)); + GpodnetSyncService.sendSyncActionsIntent(ClientConfig.applicationCallbacks.getApplicationInstance()); + } + public static List<GpodnetEpisodeAction> getQueuedEpisodeActions() { + ensurePreferencesLoaded(); + return Collections.unmodifiableList(queuedEpisodeActions); + } + + public static synchronized void removeQueuedEpisodeActions(Collection<GpodnetEpisodeAction> queued) { + ensurePreferencesLoaded(); + queuedEpisodeActions.removeAll(queued); + writePreference(PREF_SYNC_EPISODE_ACTIONS, writeEpisodeActionsToString(queuedEpisodeActions)); } /** @@ -215,7 +256,9 @@ public class GpodnetPreferences { writePreference(PREF_SYNC_ADDED, addedFeeds); removedFeeds.clear(); writePreference(PREF_SYNC_REMOVED, removedFeeds); - setLastSyncTimestamp(0); + queuedEpisodeActions.clear(); + writePreference(PREF_SYNC_EPISODE_ACTIONS, writeEpisodeActionsToString(queuedEpisodeActions)); + setLastSubscriptionSyncTimestamp(0); } private static Set<String> readListFromString(String s) { @@ -235,6 +278,29 @@ public class GpodnetPreferences { return result.toString().trim(); } + private static List<GpodnetEpisodeAction> readEpisodeActionsFromString(String s) { + String[] lines = s.split("\n"); + List<GpodnetEpisodeAction> result = new ArrayList<GpodnetEpisodeAction>(lines.length); + for(String line : lines) { + if(StringUtils.isNotBlank(line)) { + GpodnetEpisodeAction action = GpodnetEpisodeAction.readFromString(line); + if(action != null) { + result.add(GpodnetEpisodeAction.readFromString(line)); + } + } + } + return result; + } + + private static String writeEpisodeActionsToString(Collection<GpodnetEpisodeAction> c) { + StringBuilder result = new StringBuilder(); + for(GpodnetEpisodeAction item : c) { + result.append(item.writeToString()); + result.append("\n"); + } + return result.toString(); + } + private static String checkGpodnetHostname(String value) { int startIndex = 0; if (value.startsWith("http://")) { 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 6cb2faba5..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,34 +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_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_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_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; @@ -70,31 +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 boolean autoDelete; + private int smartMarkAsPlayedSecs; + private String[] playbackSpeedArray; + private boolean pauseForFocusLoss; + private boolean resumeAfterCall; + + // Network private long updateInterval; private boolean allowMobileUpdate; - private boolean displayOnlyEpisodes; - private boolean autoDelete; - private boolean autoFlattr; - private float autoFlattrPlayedDurationThreshold; - private int theme; + 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; @@ -107,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); @@ -121,47 +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); - 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, ""), ','); + smartMarkAsPlayedSecs = Integer.valueOf(sp.getString(PREF_SMART_MARK_AS_PLAYED_SECS, "30")); + 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) { @@ -211,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(); } } @@ -221,99 +259,118 @@ 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() { - instanceAvailable(); - return instance.unpauseOnHeadsetReconnect; + 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 boolean isFollowQueue() { + public static List<String> getHiddenDrawerItems() { instanceAvailable(); - return instance.followQueue; + return new ArrayList<String>(instance.hiddenDrawerItems); } - public static boolean isDownloadMediaOnWifiOnly() { + /** + * Returns notification priority. + * + * @return NotificationCompat.PRIORITY_MAX or NotificationCompat.PRIORITY_DEFAULT + */ + public static int getNotifyPriority() { instanceAvailable(); - return instance.downloadMediaOnWifiOnly; + return instance.notifyPriority; } - public static long getUpdateInterval() { + /** + * Returns true if notifications are persistent + * + * @return {@code true} if notifications are persistent, {@code false} otherwise + */ + public static boolean isPersistNotify() { instanceAvailable(); - return instance.updateInterval; + return instance.persistNotify; } - public static boolean isAllowMobileUpdate() { + /** + * 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.allowMobileUpdate; + return instance.enqueueAtFront; } - public static boolean isDisplayOnlyEpisodes() { + public static boolean isPauseOnHeadsetDisconnect() { instanceAvailable(); - //return instance.displayOnlyEpisodes; - return false; + return instance.pauseOnHeadsetDisconnect; } - public static boolean isAutoDelete() { + public static boolean isUnpauseOnHeadsetReconnect() { instanceAvailable(); - return instance.autoDelete; + return instance.unpauseOnHeadsetReconnect; } - public static boolean isAutoFlattr() { + + public static boolean isFollowQueue() { instanceAvailable(); - return instance.autoFlattr; + return instance.followQueue; } - public static int getNotifyPriority() { + public static boolean isAutoDelete() { instanceAvailable(); - return instance.notifyPriority; + return instance.autoDelete; } - public static boolean isPersistNotify() { + public static int getSmartMarkAsPlayedSecs() { instanceAvailable(); - return instance.persistNotify; + return instance.smartMarkAsPlayedSecs; } + public static boolean isAutoFlattr() { + instanceAvailable(); + return instance.autoFlattr; + } - /** - * 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 getPlaybackSpeed() { instanceAvailable(); - return instance.autoFlattrPlayedDurationThreshold; + return instance.playbackSpeed; } - public static int getTheme() { + public static String[] getPlaybackSpeedArray() { instanceAvailable(); - return instance.theme; + return instance.playbackSpeedArray; } - 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 boolean shouldPauseForFocusLoss() { + instanceAvailable(); + 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() { @@ -325,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 @@ -360,89 +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 boolean isFreshInstall() { + public static int getFastFowardSecs() { instanceAvailable(); - return instance.isFreshInstall; + 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 shouldResumeAfterCall() { + instanceAvailable(); + return instance.resumeAfterCall; + } + + public static boolean isQueueLocked() { + instanceAvailable(); + return instance.queueLocked; } @Override public void onSharedPreferenceChanged(SharedPreferences sp, String key) { - if (BuildConfig.DEBUG) - 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_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); + Log.d(TAG, "Registered change of user preferences. Key: " + key); + 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(); @@ -502,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. @@ -517,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); @@ -549,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); @@ -586,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"); } } @@ -600,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."); } } @@ -618,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( @@ -628,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 fed0d3bc8..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 @@ -9,24 +9,31 @@ import android.content.Intent; import android.os.IBinder; import android.support.v4.app.NotificationCompat; import android.util.Log; +import android.util.Pair; +import java.util.Collection; import java.util.Date; -import java.util.LinkedList; +import java.util.HashMap; import java.util.List; -import java.util.Set; +import java.util.Map; -import de.danoeh.antennapod.core.BuildConfig; 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; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeActionGetResponse; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeActionPostResponse; import de.danoeh.antennapod.core.gpoddernet.model.GpodnetSubscriptionChange; import de.danoeh.antennapod.core.gpoddernet.model.GpodnetUploadChangesResponse; 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.NetworkUtils; @@ -43,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; } @@ -61,9 +89,8 @@ public class GpodnetSyncService extends Service { @Override public void onDestroy() { super.onDestroy(); - if (BuildConfig.DEBUG) Log.d(TAG, "onDestroy"); + Log.d(TAG, "onDestroy"); syncWaiterThread.interrupt(); - } @Override @@ -79,73 +106,180 @@ public class GpodnetSyncService extends Service { return service; } - private synchronized void syncChanges() { - if (GpodnetPreferences.loggedIn() && NetworkUtils.networkAvailable(this)) { - final long timestamp = GpodnetPreferences.getLastSyncTimestamp(); - try { - final List<String> localSubscriptions = DBReader.getFeedListDownloadUrls(this); - GpodnetService service = tryLogin(); - - if (timestamp == 0) { - // first sync: download all subscriptions... - GpodnetSubscriptionChange changes = - service.getSubscriptionChanges(GpodnetPreferences.getUsername(), GpodnetPreferences.getDeviceID(), 0); - if (BuildConfig.DEBUG) - Log.d(TAG, "Downloaded subscription changes: " + changes); - processSubscriptionChanges(localSubscriptions, changes); - - // ... then upload all local subscriptions - if (BuildConfig.DEBUG) - Log.d(TAG, "Uploading subscription list: " + localSubscriptions); - GpodnetUploadChangesResponse uploadChangesResponse = - service.uploadChanges(GpodnetPreferences.getUsername(), GpodnetPreferences.getDeviceID(), localSubscriptions, new LinkedList<String>()); - if (BuildConfig.DEBUG) - Log.d(TAG, "Uploading changes response: " + uploadChangesResponse); - GpodnetPreferences.removeAddedFeeds(localSubscriptions); - GpodnetPreferences.removeRemovedFeeds(GpodnetPreferences.getRemovedFeedsCopy()); - GpodnetPreferences.setLastSyncTimestamp(uploadChangesResponse.timestamp); - } else { - Set<String> added = GpodnetPreferences.getAddedFeedsCopy(); - Set<String> removed = GpodnetPreferences.getRemovedFeedsCopy(); - - // download remote changes first... - GpodnetSubscriptionChange subscriptionChanges = service.getSubscriptionChanges(GpodnetPreferences.getUsername(), GpodnetPreferences.getDeviceID(), timestamp); - if (BuildConfig.DEBUG) - Log.d(TAG, "Downloaded subscription changes: " + subscriptionChanges); - processSubscriptionChanges(localSubscriptions, subscriptionChanges); - - // ... then upload changes local changes - if (BuildConfig.DEBUG) - Log.d(TAG, String.format("Uploading subscriptions, Added: %s\nRemoved: %s", - added.toString(), removed)); - GpodnetUploadChangesResponse uploadChangesResponse = service.uploadChanges(GpodnetPreferences.getUsername(), GpodnetPreferences.getDeviceID(), added, removed); - if (BuildConfig.DEBUG) - Log.d(TAG, "Upload subscriptions response: " + uploadChangesResponse); - - GpodnetPreferences.removeAddedFeeds(added); - GpodnetPreferences.removeRemovedFeeds(removed); - GpodnetPreferences.setLastSyncTimestamp(uploadChangesResponse.timestamp); - } - clearErrorNotifications(); - } catch (GpodnetServiceException e) { - e.printStackTrace(); - updateErrorNotification(e); - } catch (DownloadRequestException e) { - e.printStackTrace(); - } + + private synchronized void sync() { + if (GpodnetPreferences.loggedIn() == false || NetworkUtils.networkAvailable(this) == false) { + stopSelf(); + return; + } + if(syncSubscriptions) { + syncSubscriptionChanges(); + syncSubscriptions = false; + } + if(syncActions) { + syncEpisodeActions(); + syncActions = false; } stopSelf(); } - private synchronized void processSubscriptionChanges(List<String> localSubscriptions, GpodnetSubscriptionChange changes) throws DownloadRequestException { + private synchronized void syncSubscriptionChanges() { + 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 newTimeStamp = subscriptionChanges.getTimestamp(); + + Log.d(TAG, "Downloaded subscription changes: " + subscriptionChanges); + 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(localAdded.size() > 0 || localRemoved.size() > 0) { + Log.d(TAG, String.format("Uploading subscriptions, Added: %s\nRemoved: %s", + localAdded, localRemoved)); + GpodnetUploadChangesResponse uploadResponse = service.uploadChanges(GpodnetPreferences.getUsername(), + GpodnetPreferences.getDeviceID(), localAdded, localRemoved); + newTimeStamp = uploadResponse.timestamp; + Log.d(TAG, "Upload changes response: " + uploadResponse); + GpodnetPreferences.removeAddedFeeds(localAdded); + GpodnetPreferences.removeRemovedFeeds(localRemoved); + } + GpodnetPreferences.setLastSubscriptionSyncTimestamp(newTimeStamp); + clearErrorNotifications(); + } catch (GpodnetServiceException e) { + e.printStackTrace(); + updateErrorNotification(e); + } catch (DownloadRequestException e) { + e.printStackTrace(); + } + } + + 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 (!localSubscriptions.contains(downloadUrl)) { + 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()) { - DBTasks.removeFeedWithDownloadUrl(GpodnetSyncService.this, downloadUrl); + 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); + try { + GpodnetService service = tryLogin(); + + // download episode actions + GpodnetEpisodeActionGetResponse getResponse = service.getEpisodeChanges(timestamp); + long lastUpdate = getResponse.getTimestamp(); + Log.d(TAG, "Downloaded episode actions: " + getResponse); + List<GpodnetEpisodeAction> remoteActions = getResponse.getEpisodeActions(); + + List<GpodnetEpisodeAction> localActions = GpodnetPreferences.getQueuedEpisodeActions(); + processEpisodeActions(localActions, remoteActions); + + // upload local actions + if(localActions.size() > 0) { + Log.d(TAG, "Uploading episode actions: " + localActions); + GpodnetEpisodeActionPostResponse postResponse = service.uploadEpisodeActions(localActions); + lastUpdate = postResponse.timestamp; + Log.d(TAG, "Upload episode response: " + postResponse); + GpodnetPreferences.removeQueuedEpisodeActions(localActions); + } + GpodnetPreferences.setLastEpisodeActionsSyncTimestamp(lastUpdate); + clearErrorNotifications(); + } catch (GpodnetServiceException e) { + e.printStackTrace(); + updateErrorNotification(e); + } catch (DownloadRequestException e) { + e.printStackTrace(); + } + } + + + 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>(); + for(GpodnetEpisodeAction action : localActions) { + Pair key = new Pair(action.getPodcast(), action.getEpisode()); + GpodnetEpisodeAction mostRecent = localMostRecentPlayAction.get(key); + if (mostRecent == null) { + localMostRecentPlayAction.put(key, action); + } else if (mostRecent.getTimestamp().before(action.getTimestamp())) { + 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: + FeedItem newItem = DBReader.getFeedItem(this, action.getPodcast(), action.getEpisode()); + if(newItem != null) { + DBWriter.markItemRead(this, newItem, false, true); + } else { + Log.i(TAG, "Unknown feed item: " + action); + } + break; + case DOWNLOAD: + break; + case PLAY: + Pair key = new Pair(action.getPodcast(), action.getEpisode()); + GpodnetEpisodeAction localMostRecent = localMostRecentPlayAction.get(key); + if(localMostRecent == null || + localMostRecent.getTimestamp().before(action.getTimestamp())) { + GpodnetEpisodeAction mostRecent = mostRecentPlayAction.get(key); + if (mostRecent == null) { + mostRecentPlayAction.put(key, action); + } else if (mostRecent.getTimestamp().before(action.getTimestamp())) { + mostRecentPlayAction.put(key, action); + } + } + break; + case DELETE: + // NEVER EVER call DBWriter.deleteFeedMediaOfItem() here, leads to an infinite loop + break; + } + } + for (GpodnetEpisodeAction action : mostRecentPlayAction.values()) { + FeedItem playItem = DBReader.getFeedItem(this, action.getPodcast(), action.getEpisode()); + if (playItem != null) { + 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()); + } + } } } @@ -156,7 +290,7 @@ public class GpodnetSyncService extends Service { } private void updateErrorNotification(GpodnetServiceException exception) { - if (BuildConfig.DEBUG) Log.d(TAG, "Posting error notification"); + Log.d(TAG, "Posting error notification"); NotificationCompat.Builder builder = new NotificationCompat.Builder(this); final String title; @@ -186,7 +320,7 @@ public class GpodnetSyncService extends Service { private WaiterThread syncWaiterThread = new WaiterThread(WAIT_INTERVAL) { @Override public void onWaitCompleted() { - syncChanges(); + sync(); } }; @@ -209,7 +343,7 @@ public class GpodnetSyncService extends Service { private void reinit() { if (thread != null && thread.isAlive()) { - Log.d(TAG, "Interrupting waiter thread"); + Log.d(TAG, "Interrupting waiter thread"); thread.interrupt(); } thread = new Thread() { @@ -248,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 60d463178..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 @@ -60,6 +60,9 @@ import de.danoeh.antennapod.core.feed.FeedImage; 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.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.storage.DBReader; import de.danoeh.antennapod.core.storage.DBTasks; @@ -517,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 @@ -547,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) @@ -800,6 +801,18 @@ public class DownloadService extends Service { // queue new media files for automatic download for (FeedItem item : savedFeed.getItems()) { + if(item.getPubDate() == null) { + Log.d(TAG, item.toString()); + } + if(item.getImage() != null && item.getImage().isDownloaded() == false) { + item.getImage().setOwner(item); + try { + requester.downloadImage(DownloadService.this, + item.getImage()); + } catch (DownloadRequestException e) { + e.printStackTrace(); + } + } if (!item.isRead() && item.hasMedia() && !item.getMedia().isDownloaded()) { newMediaFiles.add(item.getMedia().getId()); } @@ -924,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(); @@ -1040,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"); @@ -1166,6 +1188,15 @@ public class DownloadService extends Service { saveDownloadStatus(status); sendDownloadHandledIntent(); + if(GpodnetPreferences.loggedIn()) { + FeedItem item = media.getItem(); + GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, Action.DOWNLOAD) + .currentDeviceId() + .currentTimestamp() + .build(); + GpodnetPreferences.enqueueEpisodeAction(action); + } + numberOfDownloads.decrementAndGet(); queryDownloadsAsync(); } 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..40b7de170 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,15 +187,18 @@ public class HttpDownloader extends Downloader { return; } - if (BuildConfig.DEBUG) - Log.d(TAG, "Starting download"); - while (!cancelled - && (count = connection.read(buffer)) != -1) { - out.write(buffer, 0, count); - request.setSoFar(request.getSoFar() + count); - request.setProgressPercent((int) (((double) request - .getSoFar() / (double) request - .getSize()) * 100)); + Log.d(TAG, "Starting download"); + try { + while (!cancelled + && (count = connection.read(buffer)) != -1) { + out.write(buffer, 0, count); + request.setSoFar(request.getSoFar() + count); + request.setProgressPercent((int) (((double) request + .getSoFar() / (double) request + .getSize()) * 100)); + } + } catch(IOException e) { + Log.e(TAG, Log.getStackTraceString(e)); } if (cancelled) { onCancelled(); @@ -198,6 +214,9 @@ public class HttpDownloader extends Downloader { request.getSize() ); return; + } else if(request.getSize() > 0 && request.getSoFar() == 0){ + onFail(DownloadError.ERROR_IO_ERROR, "Download completed, but nothing was read"); + return; } onSuccess(); } @@ -226,15 +245,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 +258,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 +271,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 6f3eedcb2..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 @@ -36,13 +36,15 @@ import org.apache.commons.lang3.StringUtils; import java.io.IOException; import java.util.List; -import de.danoeh.antennapod.core.BuildConfig; import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.R; import de.danoeh.antennapod.core.feed.Chapter; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.MediaType; +import de.danoeh.antennapod.core.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.PlaybackPreferences; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.receiver.MediaButtonReceiver; @@ -167,6 +169,8 @@ public class PlaybackService extends Service { private PlaybackServiceMediaPlayer mediaPlayer; private PlaybackServiceTaskManager taskManager; + private int startPosition; + private static volatile MediaType currentMediaType = MediaType.UNKNOWN; private final IBinder mBinder = new LocalBinder(); @@ -179,8 +183,7 @@ public class PlaybackService extends Service { @Override public boolean onUnbind(Intent intent) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Received onUnbind event"); + Log.d(TAG, "Received onUnbind event"); return super.onUnbind(intent); } @@ -214,8 +217,7 @@ public class PlaybackService extends Service { @Override public void onCreate() { super.onCreate(); - if (BuildConfig.DEBUG) - Log.d(TAG, "Service created."); + Log.d(TAG, "Service created."); isRunning = true; registerReceiver(headsetDisconnected, new IntentFilter( @@ -242,8 +244,7 @@ public class PlaybackService extends Service { @Override public void onDestroy() { super.onDestroy(); - if (BuildConfig.DEBUG) - Log.d(TAG, "Service is about to be destroyed"); + Log.d(TAG, "Service is about to be destroyed"); isRunning = false; started = false; currentMediaType = MediaType.UNKNOWN; @@ -253,14 +254,15 @@ public class PlaybackService extends Service { unregisterReceiver(bluetoothStateUpdated); unregisterReceiver(audioBecomingNoisy); unregisterReceiver(skipCurrentEpisodeReceiver); + unregisterReceiver(pausePlayCurrentEpisodeReceiver); + unregisterReceiver(pauseResumeCurrentEpisodeReceiver); mediaPlayer.shutdown(); taskManager.shutdown(); } @Override public IBinder onBind(Intent intent) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Received onBind event"); + Log.d(TAG, "Received onBind event"); return mBinder; } @@ -268,8 +270,7 @@ public class PlaybackService extends Service { public int onStartCommand(Intent intent, int flags, int startId) { super.onStartCommand(intent, flags, startId); - if (BuildConfig.DEBUG) - Log.d(TAG, "OnStartCommand called"); + Log.d(TAG, "OnStartCommand called"); final int keycode = intent.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1); final Playable playable = intent.getParcelableExtra(EXTRA_PLAYABLE); if (keycode == -1 && playable == null) { @@ -278,14 +279,12 @@ public class PlaybackService extends Service { } if ((flags & Service.START_FLAG_REDELIVERY) != 0) { - if (BuildConfig.DEBUG) - Log.d(TAG, "onStartCommand is a redelivered intent, calling stopForeground now."); + Log.d(TAG, "onStartCommand is a redelivered intent, calling stopForeground now."); stopForeground(true); } else { if (keycode != -1) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Received media button event"); + Log.d(TAG, "Received media button event"); handleKeycode(keycode); } else { started = true; @@ -305,8 +304,7 @@ public class PlaybackService extends Service { * Handles media button events */ private void handleKeycode(int keycode) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Handling keycode: " + keycode); + Log.d(TAG, "Handling keycode: " + keycode); final PlaybackServiceMediaPlayer.PSMPInfo info = mediaPlayer.getPSMPInfo(); final PlayerStatus status = info.playerStatus; switch (keycode) { @@ -348,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) { @@ -376,8 +374,7 @@ public class PlaybackService extends Service { * mediaplayer. */ public void setVideoSurface(SurfaceHolder sh) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Setting display"); + Log.d(TAG, "Setting display"); mediaPlayer.setVideoSurface(sh); } @@ -445,6 +442,21 @@ public class PlaybackService extends Service { } writePlayerStatusPlaybackPreferences(); + final Playable playable = mediaPlayer.getPSMPInfo().playable; + + // Gpodder: send play action + if(GpodnetPreferences.loggedIn() && playable instanceof FeedMedia) { + FeedMedia media = (FeedMedia) playable; + FeedItem item = media.getItem(); + GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, Action.PLAY) + .currentDeviceId() + .currentTimestamp() + .started(startPosition / 1000) + .position(getCurrentPosition() / 1000) + .total(getDuration() / 1000) + .build(); + GpodnetPreferences.enqueueEpisodeAction(action); + } break; case STOPPED: @@ -453,16 +465,15 @@ public class PlaybackService extends Service { break; case PLAYING: - if (BuildConfig.DEBUG) - Log.d(TAG, "Audiofocus successfully requested"); - if (BuildConfig.DEBUG) - Log.d(TAG, "Resuming/Starting playback"); + Log.d(TAG, "Audiofocus successfully requested"); + Log.d(TAG, "Resuming/Starting playback"); taskManager.startPositionSaver(); taskManager.startWidgetUpdater(); writePlayerStatusPlaybackPreferences(); setupNotification(newInfo); started = true; + startPosition = mediaPlayer.getPosition(); break; case ERROR: @@ -472,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); @@ -537,11 +547,10 @@ public class PlaybackService extends Service { }; private void endPlayback(boolean playNextEpisode) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Playback ended"); + Log.d(TAG, "Playback ended"); - final Playable media = mediaPlayer.getPSMPInfo().playable; - if (media == null) { + final Playable playable = mediaPlayer.getPSMPInfo().playable; + if (playable == null) { Log.e(TAG, "Cannot end playback: media was null"); return; } @@ -551,36 +560,46 @@ public class PlaybackService extends Service { boolean isInQueue = false; FeedItem nextItem = null; - if (media instanceof FeedMedia) { - FeedItem item = ((FeedMedia) media).getItem(); + if (playable instanceof FeedMedia) { + FeedMedia media = (FeedMedia) playable; + FeedItem item = media.getItem(); DBWriter.markItemRead(PlaybackService.this, item, true, true); try { final List<FeedItem> queue = taskManager.getQueue(); - isInQueue = QueueAccess.ItemListAccess(queue).contains(((FeedMedia) media).getItem().getId()); + isInQueue = QueueAccess.ItemListAccess(queue).contains(item.getId()); nextItem = DBTasks.getQueueSuccessorOfItem(this, item.getId(), queue); } catch (InterruptedException e) { e.printStackTrace(); // isInQueue remains false } if (isInQueue) { - DBWriter.removeQueueItem(PlaybackService.this, item.getId(), true); + DBWriter.removeQueueItem(PlaybackService.this, item, true); } - DBWriter.addItemToPlaybackHistory(PlaybackService.this, (FeedMedia) media); + DBWriter.addItemToPlaybackHistory(PlaybackService.this, media); // auto-flattr if enabled if (isAutoFlattrable(media) && UserPreferences.getAutoFlattrPlayedDurationThreshold() == 1.0f) { DBTasks.flattrItemIfLoggedIn(PlaybackService.this, item); } - //Delete episode if enabled + // Delete episode if enabled if(UserPreferences.isAutoDelete()) { - DBWriter.deleteFeedMediaOfItem(PlaybackService.this, item.getMedia().getId()); - - if(BuildConfig.DEBUG) - Log.d(TAG, "Episode Deleted"); + DBWriter.deleteFeedMediaOfItem(PlaybackService.this, media.getId()); + Log.d(TAG, "Episode Deleted"); } + // gpodder play action + if(GpodnetPreferences.loggedIn()) { + GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, Action.PLAY) + .currentDeviceId() + .currentTimestamp() + .started(startPosition / 1000) + .position(getDuration() / 1000) + .total(getDuration() / 1000) + .build(); + GpodnetPreferences.enqueueEpisodeAction(action); + } } // Load next episode if previous episode was in the queue and if there @@ -596,8 +615,7 @@ public class PlaybackService extends Service { UserPreferences.isFollowQueue(); if (loadNextItem) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Loading next item in queue"); + Log.d(TAG, "Loading next item in queue"); nextMedia = nextItem.getMedia(); } final boolean prepareImmediately; @@ -605,13 +623,10 @@ public class PlaybackService extends Service { final boolean stream; if (playNextEpisode) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Playback of next episode will start immediately."); + Log.d(TAG, "Playback of next episode will start immediately."); prepareImmediately = startWhenPrepared = true; } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "No more episodes available to play"); - + Log.d(TAG, "No more episodes available to play"); prepareImmediately = startWhenPrepared = false; stopForeground(true); stopWidgetUpdater(); @@ -619,7 +634,7 @@ public class PlaybackService extends Service { writePlaybackPreferencesNoMediaPlaying(); if (nextMedia != null) { - stream = !media.localFileAvailable(); + stream = !playable.localFileAvailable(); mediaPlayer.playMediaObject(nextMedia, stream, startWhenPrepared, prepareImmediately); sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, (nextMedia.getMediaType() == MediaType.VIDEO) ? EXTRA_CODE_VIDEO : EXTRA_CODE_AUDIO); @@ -631,8 +646,7 @@ public class PlaybackService extends Service { } public void setSleepTimer(long waitingTime) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Setting sleep timer to " + Long.toString(waitingTime) + Log.d(TAG, "Setting sleep timer to " + Long.toString(waitingTime) + " milliseconds"); taskManager.setSleepTimer(waitingTime); sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0); @@ -675,8 +689,7 @@ public class PlaybackService extends Service { } private void writePlaybackPreferences() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Writing playback preferences"); + Log.d(TAG, "Writing playback preferences"); SharedPreferences.Editor editor = PreferenceManager .getDefaultSharedPreferences(getApplicationContext()).edit(); @@ -727,8 +740,7 @@ public class PlaybackService extends Service { } private void writePlayerStatusPlaybackPreferences() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Writing player status playback preferences"); + Log.d(TAG, "Writing player status playback preferences"); SharedPreferences.Editor editor = PreferenceManager .getDefaultSharedPreferences(getApplicationContext()).edit(); @@ -777,8 +789,7 @@ public class PlaybackService extends Service { @Override protected Void doInBackground(Void... params) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Starting background work"); + Log.d(TAG, "Starting background work"); if (android.os.Build.VERSION.SDK_INT >= 11) { if (info.playable != null) { try { @@ -888,8 +899,7 @@ public class PlaybackService extends Service { notification = notificationBuilder.build(); } startForeground(NOTIFICATION_ID, notification); - if (BuildConfig.DEBUG) - Log.d(TAG, "Notification set up"); + Log.d(TAG, "Notification set up"); } } @@ -915,18 +925,15 @@ public class PlaybackService extends Service { float playbackSpeed = getCurrentPlaybackSpeed(); final Playable playable = mediaPlayer.getPSMPInfo().playable; if (position != INVALID_TIME && duration != INVALID_TIME && playable != null) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Saving current position to " + position); + Log.d(TAG, "Saving current position to " + position); if (updatePlayedDuration && playable instanceof FeedMedia) { - FeedMedia m = (FeedMedia) playable; - FeedItem item = m.getItem(); - m.setPlayedDuration(m.getPlayedDuration() + ((int) (deltaPlayedDuration * playbackSpeed))); + FeedMedia media = (FeedMedia) playable; + FeedItem item = media.getItem(); + media.setPlayedDuration(media.getPlayedDuration() + ((int) (deltaPlayedDuration * playbackSpeed))); // Auto flattr - if (isAutoFlattrable(m) && - (m.getPlayedDuration() > UserPreferences.getAutoFlattrPlayedDurationThreshold() * duration)) { - - if (BuildConfig.DEBUG) - Log.d(TAG, "saveCurrentPosition: performing auto flattr since played duration " + Integer.toString(m.getPlayedDuration()) + 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); } @@ -1019,8 +1026,7 @@ public class PlaybackService extends Service { editor.apply(); } - if (BuildConfig.DEBUG) - Log.d(TAG, "RemoteControlClient state was refreshed"); + Log.d(TAG, "RemoteControlClient state was refreshed"); } } } @@ -1063,15 +1069,12 @@ public class PlaybackService extends Service { if (StringUtils.equals(intent.getAction(), Intent.ACTION_HEADSET_PLUG)) { int state = intent.getIntExtra("state", -1); if (state != -1) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Headset plug event. State is " + state); + Log.d(TAG, "Headset plug event. State is " + state); if (state == UNPLUGGED) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Headset was unplugged during playback."); + Log.d(TAG, "Headset was unplugged during playback."); pauseIfPauseOnDisconnect(); } else if (state == PLUGGED) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Headset was plugged in during playback."); + Log.d(TAG, "Headset was plugged in during playback."); unpauseIfPauseOnDisconnect(); } } else { @@ -1088,8 +1091,7 @@ public class PlaybackService extends Service { int state = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, -1); int prevState = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_PREVIOUS_STATE, -1); if (state == AudioManager.SCO_AUDIO_STATE_CONNECTED) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Received bluetooth connection intent"); + Log.d(TAG, "Received bluetooth connection intent"); unpauseIfPauseOnDisconnect(); } } @@ -1101,8 +1103,7 @@ public class PlaybackService extends Service { @Override public void onReceive(Context context, Intent intent) { // sound is about to change, eg. bluetooth -> speaker - if (BuildConfig.DEBUG) - Log.d(TAG, "Pausing playback because audio is becoming noisy"); + Log.d(TAG, "Pausing playback because audio is becoming noisy"); pauseIfPauseOnDisconnect(); } // android.media.AUDIO_BECOMING_NOISY @@ -1148,8 +1149,7 @@ public class PlaybackService extends Service { @Override public void onReceive(Context context, Intent intent) { if (StringUtils.equals(intent.getAction(), ACTION_SKIP_CURRENT_EPISODE)) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Received SKIP_CURRENT_EPISODE intent"); + Log.d(TAG, "Received SKIP_CURRENT_EPISODE intent"); mediaPlayer.endPlayback(); } } @@ -1159,8 +1159,7 @@ public class PlaybackService extends Service { @Override public void onReceive(Context context, Intent intent) { if (StringUtils.equals(intent.getAction(), ACTION_RESUME_PLAY_CURRENT_EPISODE)) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Received RESUME_PLAY_CURRENT_EPISODE intent"); + Log.d(TAG, "Received RESUME_PLAY_CURRENT_EPISODE intent"); mediaPlayer.resume(); } } @@ -1170,8 +1169,7 @@ public class PlaybackService extends Service { @Override public void onReceive(Context context, Intent intent) { if (StringUtils.equals(intent.getAction(), ACTION_PAUSE_PLAY_CURRENT_EPISODE)) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Received PAUSE_PLAY_CURRENT_EPISODE intent"); + Log.d(TAG, "Received PAUSE_PLAY_CURRENT_EPISODE intent"); mediaPlayer.pause(false, false); } } @@ -1231,7 +1229,26 @@ public class PlaybackService extends Service { public void seekTo(final int t) { + if(mediaPlayer.getPlayerStatus() == PlayerStatus.PLAYING + && GpodnetPreferences.loggedIn()) { + final Playable playable = mediaPlayer.getPSMPInfo().playable; + if (playable instanceof FeedMedia) { + FeedMedia media = (FeedMedia) playable; + FeedItem item = media.getItem(); + GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, Action.PLAY) + .currentDeviceId() + .currentTimestamp() + .started(startPosition / 1000) + .position(getCurrentPosition() / 1000) + .total(getDuration() / 1000) + .build(); + GpodnetPreferences.enqueueEpisodeAction(action); + } + } mediaPlayer.seekTo(t); + if(mediaPlayer.getPlayerStatus() == PlayerStatus.PLAYING ) { + startPosition = t; + } } @@ -1270,10 +1287,9 @@ public class PlaybackService extends Service { return mediaPlayer.getVideoSize(); } - private boolean isAutoFlattrable(Playable p) { - if (p != null && p instanceof FeedMedia) { - FeedMedia media = (FeedMedia) p; - FeedItem item = ((FeedMedia) p).getItem(); + private boolean isAutoFlattrable(FeedMedia media) { + if (media != null) { + FeedItem item = media.getItem(); return item != null && FlattrUtils.hasToken() && UserPreferences.isAutoFlattr() && item.getPaymentLink() != null && item.getFlattrStatus().getUnflattred(); } else { return false; 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 b7c02011d..7a8e38c59 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 @@ -24,11 +24,13 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.ReentrantLock; -import de.danoeh.antennapod.core.BuildConfig; import de.danoeh.antennapod.core.feed.Chapter; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.MediaType; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.receiver.MediaButtonReceiver; +import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.util.playback.AudioPlayer; import de.danoeh.antennapod.core.util.playback.IPlayer; import de.danoeh.antennapod.core.util.playback.Playable; @@ -38,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. @@ -91,7 +93,7 @@ public class PlaybackServiceMediaPlayer { new RejectedExecutionHandler() { @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { - if (BuildConfig.DEBUG) Log.d(TAG, "Rejected execution of runnable"); + Log.d(TAG, "Rejected execution of runnable"); } } ); @@ -137,7 +139,7 @@ public class PlaybackServiceMediaPlayer { public void playMediaObject(final Playable playable, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) { Validate.notNull(playable); - if (BuildConfig.DEBUG) Log.d(TAG, "Play media object."); + Log.d(TAG, "playMediaObject(...)"); executor.submit(new Runnable() { @Override public void run() { @@ -164,16 +166,16 @@ public class PlaybackServiceMediaPlayer { */ private void playMediaObject(final Playable playable, final boolean forceReset, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) { Validate.notNull(playable); - if (!playerLock.isHeldByCurrentThread()) + if (!playerLock.isHeldByCurrentThread()) { throw new IllegalStateException("method requires playerLock"); + } if (media != null) { if (!forceReset && media.getIdentifier().equals(playable.getIdentifier()) && playerStatus == PlayerStatus.PLAYING) { // episode is already playing -> ignore method call - if (BuildConfig.DEBUG) - Log.d(TAG, "Method call to playMediaObject was ignored: media file already playing."); + Log.d(TAG, "Method call to playMediaObject was ignored: media file already playing."); return; } else { // stop playback of this episode @@ -184,6 +186,23 @@ public class PlaybackServiceMediaPlayer { if (playerStatus == PlayerStatus.PLAYING) { setPlayerStatus(PlayerStatus.PAUSED, media); } + + // smart mark as played + if(media != null && media instanceof FeedMedia) { + FeedMedia oldMedia = (FeedMedia) media; + if(oldMedia.hasAlmostEnded()) { + Log.d(TAG, "smart mark as read"); + FeedItem item = oldMedia.getItem(); + DBWriter.markItemRead(context, item, true, false); + DBWriter.removeQueueItem(context, item, false); + DBWriter.addItemToPlaybackHistory(context, oldMedia); + if (UserPreferences.isAutoDelete()) { + Log.d(TAG, "Delete " + oldMedia.toString()); + DBWriter.deleteFeedMediaOfItem(context, oldMedia.getId()); + } + } + } + setPlayerStatus(PlayerStatus.INDETERMINATE, null); } } @@ -281,11 +300,10 @@ public class PlaybackServiceMediaPlayer { media.onPlaybackStart(); } else { - if (BuildConfig.DEBUG) Log.e(TAG, "Failed to request audio focus"); + Log.e(TAG, "Failed to request audio focus"); } } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "Call to resume() was ignored because current state of PSMP object is " + playerStatus); + Log.d(TAG, "Call to resume() was ignored because current state of PSMP object is " + playerStatus); } } @@ -307,8 +325,7 @@ public class PlaybackServiceMediaPlayer { playerLock.lock(); releaseWifiLockIfNecessary(); if (playerStatus == PlayerStatus.PLAYING) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Pausing playback."); + Log.d(TAG, "Pausing playback."); mediaPlayer.pause(); setPlayerStatus(PlayerStatus.PAUSED, media); @@ -320,8 +337,7 @@ public class PlaybackServiceMediaPlayer { reinit(); } } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "Ignoring call to pause: Player is in " + playerStatus + " state"); + Log.d(TAG, "Ignoring call to pause: Player is in " + playerStatus + " state"); } playerLock.unlock(); @@ -342,8 +358,7 @@ public class PlaybackServiceMediaPlayer { playerLock.lock(); if (playerStatus == PlayerStatus.INITIALIZED) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Preparing media player"); + Log.d(TAG, "Preparing media player"); setPlayerStatus(PlayerStatus.PREPARING, media); try { mediaPlayer.prepare(); @@ -370,8 +385,7 @@ public class PlaybackServiceMediaPlayer { throw new IllegalStateException("Player is not in PREPARING state"); } - if (BuildConfig.DEBUG) - Log.d(TAG, "Resource prepared"); + Log.d(TAG, "Resource prepared"); if (mediaType == MediaType.VIDEO) { VideoPlayer vp = (VideoPlayer) mediaPlayer; @@ -383,8 +397,7 @@ public class PlaybackServiceMediaPlayer { } if (media.getDuration() == 0) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Setting duration of media"); + Log.d(TAG, "Setting duration of media"); media.setDuration(mediaPlayer.getDuration()); } setPlayerStatus(PlayerStatus.PREPARED, media); @@ -412,8 +425,7 @@ public class PlaybackServiceMediaPlayer { } else if (mediaPlayer != null) { mediaPlayer.reset(); } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "Call to reinit was ignored: media and mediaPlayer were null"); + Log.d(TAG, "Call to reinit was ignored: media and mediaPlayer were null"); } playerLock.unlock(); } @@ -437,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(); @@ -529,13 +541,15 @@ public class PlaybackServiceMediaPlayer { 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; } @@ -567,8 +581,7 @@ public class PlaybackServiceMediaPlayer { if (media != null && media.getMediaType() == MediaType.AUDIO) { if (mediaPlayer.canSetSpeed()) { mediaPlayer.setPlaybackSpeed((float) speed); - if (BuildConfig.DEBUG) - Log.d(TAG, "Playback speed was set to " + speed); + Log.d(TAG, "Playback speed was set to " + speed); callback.playbackSpeedChanged(speed); } } @@ -651,8 +664,7 @@ public class PlaybackServiceMediaPlayer { @Override public void run() { playerLock.lock(); - if (BuildConfig.DEBUG) - Log.d(TAG, "Resetting video surface"); + Log.d(TAG, "Resetting video surface"); mediaPlayer.setDisplay(null); reinit(); playerLock.unlock(); @@ -716,7 +728,7 @@ public class PlaybackServiceMediaPlayer { private synchronized void setPlayerStatus(PlayerStatus newStatus, Playable newMedia) { Validate.notNull(newStatus); - if (BuildConfig.DEBUG) Log.d(TAG, "Setting player status to " + newStatus); + Log.d(TAG, "Setting player status to " + newStatus); this.playerStatus = newStatus; this.media = newMedia; @@ -725,6 +737,7 @@ public class PlaybackServiceMediaPlayer { int state; if (playerStatus != null) { + Log.d(TAG, "playerStatus: " + playerStatus.toString()); switch (playerStatus) { case PLAYING: state = PlaybackStateCompat.STATE_PLAYING; @@ -788,17 +801,15 @@ 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; - if (BuildConfig.DEBUG) Log.d(TAG, "Call state: " + callState); Log.i(TAG, "Call state:" + callState); - if (focusChange == AudioManager.AUDIOFOCUS_LOSS || callState != TelephonyManager.CALL_STATE_IDLE) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Lost audio focus"); + if (focusChange == AudioManager.AUDIOFOCUS_LOSS || + (!UserPreferences.shouldResumeAfterCall() && callState != TelephonyManager.CALL_STATE_IDLE)) { + Log.d(TAG, "Lost audio focus"); pause(true, false); callback.shouldStop(); } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Gained audio focus"); + Log.d(TAG, "Gained audio focus"); if (pausedBecauseOfTransientAudiofocusLoss) { // we paused => play now resume(); } else { // we ducked => raise audio level back @@ -808,22 +819,19 @@ public class PlaybackServiceMediaPlayer { } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) { if (playerStatus == PlayerStatus.PLAYING) { if (!UserPreferences.shouldPauseForFocusLoss()) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Lost audio focus temporarily. Ducking..."); + Log.d(TAG, "Lost audio focus temporarily. Ducking..."); audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, AudioManager.ADJUST_LOWER, 0); pausedBecauseOfTransientAudiofocusLoss = false; } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "Lost audio focus temporarily. Could duck, but won't, pausing..."); + Log.d(TAG, "Lost audio focus temporarily. Could duck, but won't, pausing..."); pause(false, false); pausedBecauseOfTransientAudiofocusLoss = true; } } } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) { if (playerStatus == PlayerStatus.PLAYING) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Lost audio focus temporarily. Pausing..."); + Log.d(TAG, "Lost audio focus temporarily. Pausing..."); pause(false, false); pausedBecauseOfTransientAudiofocusLoss = true; } @@ -873,8 +881,7 @@ public class PlaybackServiceMediaPlayer { if (playerStatus == PlayerStatus.INDETERMINATE) { setPlayerStatus(PlayerStatus.STOPPED, null); } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "Ignored call to stop: Current player state is: " + playerStatus); + Log.d(TAG, "Ignored call to stop: Current player state is: " + playerStatus); } playerLock.unlock(); @@ -1091,13 +1098,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 1865afa6f..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 @@ -5,14 +5,23 @@ import android.util.Log; import org.apache.commons.lang3.Validate; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; + import de.danoeh.antennapod.core.BuildConfig; -import de.danoeh.antennapod.core.feed.EventDistributor; 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 java.util.List; -import java.util.concurrent.*; +import de.greenrobot.event.EventBus; + /** * Manages the background tasks of PlaybackSerivce, i.e. @@ -69,18 +78,13 @@ public class PlaybackServiceTaskManager { } }); loadQueue(); - EventDistributor.getInstance().register(eventDistributorListener); + EventBus.getDefault().register(this); } - private final EventDistributor.EventListener eventDistributorListener = new EventDistributor.EventListener() { - @Override - public void update(EventDistributor eventDistributor, Integer arg) { - if ((EventDistributor.QUEUE_UPDATE & arg) != 0) { - cancelQueueLoader(); - loadQueue(); - } - } - }; + public void onEvent(QueueEvent event) { + cancelQueueLoader(); + loadQueue(); + } private synchronized boolean isQueueLoaderActive() { return queueFuture != null && !queueFuture.isDone(); @@ -145,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."); } } @@ -312,7 +316,7 @@ public class PlaybackServiceTaskManager { * execution of this method. */ public synchronized void shutdown() { - EventDistributor.getInstance().unregister(eventDistributorListener); + EventBus.getDefault().unregister(this); cancelAllTasks(); schedExecutor.shutdown(); } 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 0164e914b..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 @@ -12,7 +12,7 @@ import java.util.concurrent.ExecutionException; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.core.util.QueueAccess; +import de.danoeh.antennapod.core.util.LongList; /** * Implementation of the EpisodeCleanupAlgorithm interface used by AntennaPod. @@ -24,7 +24,7 @@ public class APCleanupAlgorithm implements EpisodeCleanupAlgorithm<Integer> { public int performCleanup(Context context, Integer episodeNumber) { List<FeedItem> candidates = new ArrayList<FeedItem>(); List<FeedItem> downloadedItems = DBReader.getDownloadedItems(context); - QueueAccess queue = QueueAccess.IDListAccess(DBReader.getQueueIDList(context)); + LongList queue = DBReader.getQueueIDList(context); List<FeedItem> delete; for (FeedItem item : downloadedItems) { if (item.hasMedia() && item.getMedia().isDownloaded() @@ -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 217e6fba5..dc24c5784 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 @@ -2,11 +2,13 @@ package de.danoeh.antennapod.core.storage; import android.content.Context; import android.database.Cursor; -import android.database.SQLException; 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; @@ -22,6 +24,8 @@ 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; import de.danoeh.antennapod.core.util.comparator.PlaybackCompletionDateComparator; @@ -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())); @@ -256,8 +261,8 @@ public final class DBReader { item.getMedia().setItem(item); } } while (cursor.moveToNext()); - cursor.close(); } + cursor.close(); } private static FeedMedia extractFeedMediaFromCursorRow(final Cursor cursor) { @@ -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) { @@ -339,8 +362,7 @@ public final class DBReader { } static List<FeedItem> getQueue(Context context, PodDBAdapter adapter) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Extracting queue"); + Log.d(TAG, "getQueue()"); Cursor itemlistCursor = adapter.getQueueCursor(); List<FeedItem> items = extractItemlistFromCursor(adapter, @@ -359,31 +381,48 @@ public final class DBReader { * @return A list of IDs sorted by the same order as the queue. The caller can wrap the returned * list in a {@link de.danoeh.antennapod.core.util.QueueAccess} object for easier access to the queue's properties. */ - public static List<Long> getQueueIDList(Context context) { + public static LongList getQueueIDList(Context context) { PodDBAdapter adapter = new PodDBAdapter(context); adapter.open(); - List<Long> result = getQueueIDList(adapter); + LongList result = getQueueIDList(adapter); adapter.close(); return result; } - static List<Long> getQueueIDList(PodDBAdapter adapter) { + static LongList getQueueIDList(PodDBAdapter adapter) { adapter.open(); Cursor queueCursor = adapter.getQueueIDCursor(); - List<Long> queueIds = new ArrayList<Long>(queueCursor.getCount()); + LongList queueIds = new LongList(queueCursor.getCount()); if (queueCursor.moveToFirst()) { do { queueIds.add(queueCursor.getLong(0)); } while (queueCursor.moveToNext()); } + queueCursor.close(); return queueIds; } /** + * Return the size of the queue. + * + * @param context A context that is used for opening a database connection. + * @return Size of the queue. + */ + public static int getQueueSize(Context context) { + Log.d(TAG, "getQueueSize()"); + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + int size = adapter.getQueueSize(); + adapter.close(); + return size; + } + + /** * Loads a list of the FeedItems in the queue. If the FeedItems of the queue are not used directly, consider using * {@link #getQueueIDList(android.content.Context)} instead. * @@ -392,8 +431,7 @@ public final class DBReader { * list in a {@link de.danoeh.antennapod.core.util.QueueAccess} object for easier access to the queue's properties. */ public static List<FeedItem> getQueue(Context context) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Extracting queue"); + Log.d(TAG, "getQueue()"); PodDBAdapter adapter = new PodDBAdapter(context); adapter.open(); @@ -454,24 +492,49 @@ 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()); } + cursor.close(); return itemIds; } @@ -551,27 +614,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(); @@ -580,6 +623,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. @@ -655,7 +752,34 @@ public final class DBReader { } } } + itemCursor.close(); 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(); + } + itemCursor.close(); + return result; } @@ -677,7 +801,96 @@ 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) { + Log.d(TAG, "Loading feeditem with podcast url " + podcastUrl + " and episode url " + episodeUrl); + FeedItem item = null; + Cursor itemCursor = adapter.getFeedItemCursor(podcastUrl, episodeUrl); + if (itemCursor.moveToFirst()) { + List<FeedItem> list = extractItemlistFromCursor(adapter, itemCursor); + if (list.size() > 0) { + item = list.get(0); + loadFeedDataOfFeedItemlist(context, list); + if (item.hasChapters()) { + loadChaptersOfFeedItem(adapter, item); + } + } + } + itemCursor.close(); + return item; + } + + /** + * 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); + try { + if (cursor.moveToFirst()) { + String username = cursor.getString(0); + String password = cursor.getString(1); + return username + ":" + password; + } + return ""; + } finally { + cursor.close(); + } + } + + /** + * Loads a specific FeedItem from the database. + * + * @param context A context that is used for opening a database connection. + * @param podcastUrl the corresponding feed's url + * @param episodeUrl the feed item's url + * @return The FeedItem or null if the FeedItem could not be found. All FeedComponent-attributes + * as well as chapter marks of the FeedItem will also be loaded from the database. + */ + public static FeedItem getFeedItem(final Context context, final String podcastUrl, final String episodeUrl) { + Log.d(TAG, "Loading feeditem with podcast url " + podcastUrl + " and episode url " + episodeUrl); + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + FeedItem item = getFeedItem(context, podcastUrl, episodeUrl, adapter); + adapter.close(); + return item; } /** @@ -698,6 +911,7 @@ public final class DBReader { item.setDescription(description); item.setContentEncoded(contentEncoded); } + extraCursor.close(); adapter.close(); } @@ -778,10 +992,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.getNumberOfUnreadItems(); + 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 LongIntMap result = adapter.getNumberOfUnreadFeedItems(feedIds); adapter.close(); return result; } @@ -910,9 +1138,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; } @@ -920,12 +1170,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 8dd6ddea7..defce5930 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; @@ -34,7 +33,7 @@ import de.danoeh.antennapod.core.service.GpodnetSyncService; import de.danoeh.antennapod.core.service.download.DownloadStatus; import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.core.util.DownloadError; -import de.danoeh.antennapod.core.util.QueueAccess; +import de.danoeh.antennapod.core.util.LongList; import de.danoeh.antennapod.core.util.comparator.FeedItemPubdateComparator; import de.danoeh.antennapod.core.util.exception.MediaFileNotFoundException; import de.danoeh.antennapod.core.util.flattr.FlattrUtils; @@ -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,15 +302,17 @@ public final class DBTasks { */ public static void refreshFeed(Context context, Feed feed) throws DownloadRequestException { + Log.d(TAG, "id " + feed.getId()); refreshFeed(context, feed, false); } private static void refreshFeed(Context context, Feed feed, boolean loadAllPages) throws DownloadRequestException { Feed f; + Date lastUpdate = feed.hasLastUpdateFailed() ? new Date(0) : feed.getLastUpdate(); if (feed.getPreferences() == null) { - f = new Feed(feed.getDownload_url(), feed.getLastUpdate(), feed.getTitle()); + f = new Feed(feed.getDownload_url(), lastUpdate, feed.getTitle()); } else { - f = new Feed(feed.getDownload_url(), feed.getLastUpdate(), feed.getTitle(), + f = new Feed(feed.getDownload_url(), lastUpdate, feed.getTitle(), feed.getPreferences().getUsername(), feed.getPreferences().getPassword()); } f.setId(feed.getId()); @@ -428,25 +426,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 +458,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. @@ -524,8 +495,8 @@ public final class DBTasks { * @param feedItemId ID of the FeedItem */ public static boolean isInQueue(Context context, final long feedItemId) { - List<Long> queue = DBReader.getQueueIDList(context); - return QueueAccess.IDListAccess(queue).contains(feedItemId); + LongList queue = DBReader.getQueueIDList(context); + return queue.contains(feedItemId); } private static Feed searchFeedByIdentifyingValueOrID(Context context, PodDBAdapter adapter, @@ -599,8 +570,7 @@ public final class DBTasks { 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()); @@ -608,21 +578,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 @@ -634,6 +600,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); @@ -645,6 +612,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 c5bf89533..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 @@ -7,6 +7,7 @@ import android.content.SharedPreferences; import android.database.Cursor; import android.preference.PreferenceManager; import android.util.Log; + import org.shredzone.flattr4j.model.Flattr; import java.io.File; @@ -28,19 +29,23 @@ 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; import de.danoeh.antennapod.core.feed.FeedPreferences; +import de.danoeh.antennapod.core.feed.QueueEvent; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction; import de.danoeh.antennapod.core.preferences.GpodnetPreferences; import de.danoeh.antennapod.core.preferences.PlaybackPreferences; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.download.DownloadStatus; import de.danoeh.antennapod.core.service.playback.PlaybackService; -import de.danoeh.antennapod.core.util.QueueAccess; +import de.danoeh.antennapod.core.util.LongList; import de.danoeh.antennapod.core.util.flattr.FlattrStatus; import de.danoeh.antennapod.core.util.flattr.FlattrThing; import de.danoeh.antennapod.core.util.flattr.SimpleFlattrThing; +import de.greenrobot.event.EventBus; /** * Provides methods for writing data to AntennaPod's database. @@ -120,10 +125,18 @@ public class DBWriter { PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE)); } } + // Gpodder: queue delete action for synchronization + if(GpodnetPreferences.loggedIn()) { + FeedItem item = media.getItem(); + GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, GpodnetEpisodeAction.Action.DELETE) + .currentDeviceId() + .currentTimestamp() + .build(); + GpodnetPreferences.enqueueEpisodeAction(action); + } } - if (BuildConfig.DEBUG) - Log.d(TAG, "Deleting File. Result: " + result); - EventDistributor.getInstance().sendQueueUpdateBroadcast(); + Log.d(TAG, "Deleting File. Result: " + result); + EventBus.getDefault().post(new QueueEvent(QueueEvent.Action.DELETED_MEDIA, media.getItem())); EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast(); } } @@ -337,36 +350,20 @@ public class DBWriter { public void run() { final PodDBAdapter adapter = new PodDBAdapter(context); adapter.open(); - final List<FeedItem> queue = DBReader - .getQueue(context, adapter); + final List<FeedItem> queue = DBReader.getQueue(context, adapter); 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); - EventDistributor.getInstance() - .sendQueueUpdateBroadcast(); - } - if (unreadItemsModified && item != null) { - adapter.setSingleFeedItem(item); - EventDistributor.getInstance() - .sendUnreadItemsUpdateBroadcast(); - } } + adapter.close(); if (performAutoDownload) { DBTasks.autodownloadUndownloadedItems(context); @@ -407,33 +404,21 @@ 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; - } } } } if (queueModified) { adapter.setQueue(queue); - EventDistributor.getInstance() - .sendQueueUpdateBroadcast(); - } - if (unreadItemsModified) { - adapter.setFeedItemlist(itemsToSave); - EventDistributor.getInstance() - .sendUnreadItemsUpdateBroadcast(); + EventBus.getDefault().post(new QueueEvent(QueueEvent.Action.ADDED_ITEMS, queue)); } } adapter.close(); @@ -459,7 +444,7 @@ public class DBWriter { adapter.clearQueue(); adapter.close(); - EventDistributor.getInstance().sendQueueUpdateBroadcast(); + EventBus.getDefault().post(new QueueEvent(QueueEvent.Action.CLEARED)); } }); } @@ -468,34 +453,25 @@ public class DBWriter { * Removes a FeedItem object from the queue. * * @param context A context that is used for opening a database connection. - * @param itemId ID of the FeedItem that should be removed. + * @param item FeedItem that should be removed. * @param performAutoDownload true if an auto-download process should be started after the operation. */ public static Future<?> removeQueueItem(final Context context, - final long itemId, final boolean performAutoDownload) { + final FeedItem item, final boolean performAutoDownload) { return dbExec.submit(new Runnable() { @Override public void run() { final PodDBAdapter adapter = new PodDBAdapter(context); adapter.open(); - final List<FeedItem> queue = DBReader - .getQueue(context, adapter); - FeedItem item = null; + final List<FeedItem> queue = DBReader.getQueue(context, adapter); if (queue != null) { - boolean queueModified = false; - QueueAccess queueAccess = QueueAccess.ItemListAccess(queue); - if (queueAccess.contains(itemId)) { - item = DBReader.getFeedItem(context, itemId); - if (item != null) { - queueModified = queueAccess.remove(itemId); - } - } - if (queueModified) { + int position = queue.indexOf(item); + if(position >= 0) { + queue.remove(position); adapter.setQueue(queue); - EventDistributor.getInstance() - .sendQueueUpdateBroadcast(); + EventBus.getDefault().post(new QueueEvent(QueueEvent.Action.REMOVED, item, position)); } else { Log.w(TAG, "Queue was not modified by call to removeQueueItem"); } @@ -523,16 +499,13 @@ public class DBWriter { return dbExec.submit(new Runnable() { @Override public void run() { - List<Long> queueIdList = DBReader.getQueueIDList(context); - int currentLocation = 0; - for (long id : queueIdList) { - if (id == itemId) { - moveQueueItemHelper(context, currentLocation, 0, broadcastUpdate); - return; - } - currentLocation++; + LongList queueIdList = DBReader.getQueueIDList(context); + int index = queueIdList.indexOf(itemId); + if (index >=0) { + moveQueueItemHelper(context, index, 0, broadcastUpdate); + } else { + Log.e(TAG, "moveQueueItemToTop: item not found"); } - Log.e(TAG, "moveQueueItemToTop: item not found"); } }); } @@ -550,17 +523,14 @@ public class DBWriter { return dbExec.submit(new Runnable() { @Override public void run() { - List<Long> queueIdList = DBReader.getQueueIDList(context); - int currentLocation = 0; - for (long id : queueIdList) { - if (id == itemId) { - moveQueueItemHelper(context, currentLocation, queueIdList.size() - 1, - broadcastUpdate); - return; - } - currentLocation++; + LongList queueIdList = DBReader.getQueueIDList(context); + int index = queueIdList.indexOf(itemId); + if(index >= 0) { + moveQueueItemHelper(context, index, queueIdList.size() - 1, + broadcastUpdate); + } else { + Log.e(TAG, "moveQueueItemToBottom: item not found"); } - Log.e(TAG, "moveQueueItemToBottom: item not found"); } }); } @@ -614,8 +584,7 @@ public class DBWriter { adapter.setQueue(queue); if (broadcastUpdate) { - EventDistributor.getInstance() - .sendQueueUpdateBroadcast(); + EventBus.getDefault().post(new QueueEvent(QueueEvent.Action.MOVED, item, to)); } } @@ -628,6 +597,19 @@ public class DBWriter { /** * Sets the 'read'-attribute of a FeedItem to the specified value. * + * @param context A context that is used for opening a database connection. + * @param itemId ID of the FeedItem + * @param read New value of the 'read'-attribute + */ + public static Future<?> markItemRead(final Context context, final long itemId, + final boolean read) { + return markItemRead(context, itemId, read, 0, false); + } + + + /** + * Sets the 'read'-attribute of a FeedItem to the specified value. + * * @param context A context that is used for opening a database connection. * @param item The FeedItem object * @param read New value of the 'read'-attribute @@ -639,18 +621,6 @@ public class DBWriter { return markItemRead(context, item.getId(), read, mediaId, resetMediaPosition); } - /** - * Sets the 'read'-attribute of a FeedItem to the specified value. - * - * @param context A context that is used for opening a database connection. - * @param itemId ID of the FeedItem - * @param read New value of the 'read'-attribute - */ - public static Future<?> markItemRead(final Context context, final long itemId, - final boolean read) { - return markItemRead(context, itemId, read, 0, false); - } - private static Future<?> markItemRead(final Context context, final long itemId, final boolean read, final long mediaId, final boolean resetMediaPosition) { @@ -942,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) */ @@ -1036,8 +1026,7 @@ public class DBWriter { Collections.sort(queue, comparator); adapter.setQueue(queue); if (broadcastUpdate) { - EventDistributor.getInstance() - .sendQueueUpdateBroadcast(); + EventBus.getDefault().post(new QueueEvent(QueueEvent.Action.SORTED)); } } else { Log.e(TAG, "sortQueue: Could not load queue"); @@ -1046,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 f72858adc..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) { @@ -1120,7 +1219,11 @@ public class PodDBAdapter { return c; } - public final Cursor getFeedItemCursor(final String... ids) { + public final Cursor getFeedItemCursor(final String id) { + return getFeedItemCursor(new String[]{id}); + } + + public final Cursor getFeedItemCursor(final String[] ids) { if (ids.length > IN_OPERATOR_MAXIMUM) { throw new IllegalArgumentException( "number of IDs must not be larger than " @@ -1133,6 +1236,29 @@ public class PodDBAdapter { } + public final Cursor getFeedItemCursor(final String podcastUrl, final String episodeUrl) { + final String query = "SELECT " + SEL_FI_SMALL_STR + " FROM " + TABLE_NAME_FEED_ITEMS + + " INNER JOIN " + + TABLE_NAME_FEEDS + " ON " + TABLE_NAME_FEED_ITEMS + "." + KEY_FEED + "=" + + TABLE_NAME_FEEDS + "." + KEY_ID + " WHERE " + TABLE_NAME_FEED_ITEMS + "." + KEY_ITEM_IDENTIFIER + "='" + + episodeUrl + "' AND " + TABLE_NAME_FEEDS + "." + KEY_DOWNLOAD_URL + "='" + podcastUrl + "'"; + 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); @@ -1144,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()) { @@ -1156,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"; @@ -1317,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/syndication/namespace/NSDublinCore.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSDublinCore.java index 099593eed..23f76186b 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSDublinCore.java +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSDublinCore.java @@ -3,7 +3,7 @@ package de.danoeh.antennapod.core.syndication.namespace; import org.xml.sax.Attributes; import de.danoeh.antennapod.core.syndication.handler.HandlerState; -import de.danoeh.antennapod.core.syndication.util.SyndDateUtils; +import de.danoeh.antennapod.core.util.DateUtils; public class NSDublinCore extends Namespace { private static final String TAG = "NSDublinCore"; @@ -30,7 +30,7 @@ public class NSDublinCore extends Namespace { String second = secondElement.getName(); if (top.equals(DATE) && second.equals(ITEM)) { state.getCurrentItem().setPubDate( - SyndDateUtils.parseISO8601Date(content)); + DateUtils.parse(content)); } } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSRSS20.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSRSS20.java index 0ca261a0e..6455332be 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSRSS20.java +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSRSS20.java @@ -1,14 +1,16 @@ package de.danoeh.antennapod.core.syndication.namespace; import android.util.Log; + +import org.xml.sax.Attributes; + import de.danoeh.antennapod.core.BuildConfig; import de.danoeh.antennapod.core.feed.FeedImage; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.syndication.handler.HandlerState; -import de.danoeh.antennapod.core.syndication.util.SyndDateUtils; import de.danoeh.antennapod.core.syndication.util.SyndTypeUtils; -import org.xml.sax.Attributes; +import de.danoeh.antennapod.core.util.DateUtils; /** * SAX-Parser for reading RSS-Feeds @@ -129,7 +131,7 @@ public class NSRSS20 extends Namespace { } } else if (top.equals(PUBDATE) && second.equals(ITEM)) { state.getCurrentItem().setPubDate( - SyndDateUtils.parseRFC822Date(content)); + DateUtils.parse(content)); } else if (top.equals(URL) && second.equals(IMAGE) && third != null && third.equals(CHANNEL)) { state.getFeed().getImage().setDownload_url(content); diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSSimpleChapters.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSSimpleChapters.java index a2e5d0187..64b82100e 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSSimpleChapters.java +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSSimpleChapters.java @@ -10,7 +10,7 @@ import de.danoeh.antennapod.core.BuildConfig; import de.danoeh.antennapod.core.feed.Chapter; import de.danoeh.antennapod.core.feed.SimpleChapter; import de.danoeh.antennapod.core.syndication.handler.HandlerState; -import de.danoeh.antennapod.core.syndication.util.SyndDateUtils; +import de.danoeh.antennapod.core.util.DateUtils; public class NSSimpleChapters extends Namespace { private static final String TAG = "NSSimpleChapters"; @@ -33,7 +33,7 @@ public class NSSimpleChapters extends Namespace { try { state.getCurrentItem() .getChapters() - .add(new SimpleChapter(SyndDateUtils + .add(new SimpleChapter(DateUtils .parseTimeString(attributes.getValue(START)), attributes.getValue(TITLE), state.getCurrentItem(), attributes.getValue(HREF))); diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/NSAtom.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/NSAtom.java index 3928c65b3..abff5b2db 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/NSAtom.java +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/NSAtom.java @@ -13,8 +13,8 @@ import de.danoeh.antennapod.core.syndication.namespace.NSITunes; import de.danoeh.antennapod.core.syndication.namespace.NSRSS20; import de.danoeh.antennapod.core.syndication.namespace.Namespace; import de.danoeh.antennapod.core.syndication.namespace.SyndElement; -import de.danoeh.antennapod.core.syndication.util.SyndDateUtils; import de.danoeh.antennapod.core.syndication.util.SyndTypeUtils; +import de.danoeh.antennapod.core.util.DateUtils; public class NSAtom extends Namespace { private static final String TAG = "NSAtom"; @@ -191,12 +191,12 @@ public class NSAtom extends Namespace { if (second.equals(ENTRY) && state.getCurrentItem().getPubDate() == null) { state.getCurrentItem().setPubDate( - SyndDateUtils.parseRFC3339Date(content)); + DateUtils.parse(content)); } } else if (top.equals(PUBLISHED)) { if (second.equals(ENTRY)) { state.getCurrentItem().setPubDate( - SyndDateUtils.parseRFC3339Date(content)); + DateUtils.parse(content)); } } else if (top.equals(IMAGE)) { state.getFeed().setImage(new FeedImage(state.getFeed(), content, null)); diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndDateUtils.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndDateUtils.java deleted file mode 100644 index a9929d7b1..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndDateUtils.java +++ /dev/null @@ -1,194 +0,0 @@ -package de.danoeh.antennapod.core.syndication.util; - -import android.util.Log; - -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; - -import de.danoeh.antennapod.core.BuildConfig; - -/** - * Parses several date formats. - */ -public class SyndDateUtils { - 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"; - - public static final String ISO8601_SHORT = "yyyy-MM-dd"; - - private static ThreadLocal<SimpleDateFormat> RFC822Formatter = new ThreadLocal<SimpleDateFormat>() { - @Override - protected SimpleDateFormat initialValue() { - return new SimpleDateFormat(RFC822DATES[0], Locale.US); - } - - }; - - private static ThreadLocal<SimpleDateFormat> RFC3339Formatter = new ThreadLocal<SimpleDateFormat>() { - @Override - protected SimpleDateFormat initialValue() { - return new SimpleDateFormat(RFC3339UTC, Locale.US); - } - - }; - - private static ThreadLocal<SimpleDateFormat> ISO8601ShortFormatter = new ThreadLocal<SimpleDateFormat>() { - @Override - protected SimpleDateFormat initialValue() { - return new SimpleDateFormat(ISO8601_SHORT, Locale.US); - } - - }; - - public static Date parseRFC822Date(String date) { - Date result = null; - if (date.contains("PDT")) { - date = date.replace("PDT", "PST8PDT"); - } - if (date.contains(",")) { - // Remove day of the week - date = date.substring(date.indexOf(",") + 1).trim(); - } - SimpleDateFormat format = RFC822Formatter.get(); - - for (String RFC822DATE : RFC822DATES) { - try { - format.applyPattern(RFC822DATE); - result = format.parse(date); - break; - } catch (ParseException e) { - if (BuildConfig.DEBUG) Log.d(TAG, "ParserException", e); - } - } - if (result == null) { - Log.e(TAG, "Unable to parse feed date correctly:" + date); - } - - return result; - } - - public static Date parseRFC3339Date(String date) { - Date result = null; - SimpleDateFormat format = RFC3339Formatter.get(); - boolean isLocal = date.endsWith("Z"); - if (date.contains(".")) { - // remove secfrac - int fracIndex = date.indexOf("."); - String first = date.substring(0, fracIndex); - String second = null; - if (isLocal) { - second = date.substring(date.length() - 1); - } else { - if (date.contains("+")) { - second = date.substring(date.indexOf("+")); - } else { - second = date.substring(date.indexOf("-")); - } - } - - date = first + second; - } - if (isLocal) { - try { - result = format.parse(date); - } catch (ParseException e) { - e.printStackTrace(); - } - } else { - format.applyPattern(RFC3339LOCAL); - // remove last colon - StringBuffer buf = new StringBuffer(date.length() - 1); - int colonIdx = date.lastIndexOf(':'); - for (int x = 0; x < date.length(); x++) { - if (x != colonIdx) - buf.append(date.charAt(x)); - } - String bufStr = buf.toString(); - try { - result = format.parse(bufStr); - } catch (ParseException e) { - e.printStackTrace(); - Log.e(TAG, "Unable to parse date"); - } finally { - format.applyPattern(RFC3339UTC); - } - - } - - return result; - - } - - public static Date parseISO8601Date(String date) { - if(date.length() > ISO8601_SHORT.length()) { - return parseRFC3339Date(date); - } - Date result = null; - if(date.length() == "YYYYMMDD".length()) { - date = date.substring(0, 4) + "-" + date.substring(4, 6) + "-" + date.substring(6,8); - } - SimpleDateFormat format = ISO8601ShortFormatter.get(); - try { - result = format.parse(date); - } catch (ParseException e) { - e.printStackTrace(); - } - return result; - } - - /** - * Takes a string of the form [HH:]MM:SS[.mmm] and converts it to - * milliseconds. - * - * @throws java.lang.NumberFormatException if the number segments contain invalid numbers. - */ - public static long parseTimeString(final String time) { - String[] parts = time.split(":"); - long result = 0; - int idx = 0; - if (parts.length == 3) { - // string has hours - result += Integer.valueOf(parts[idx]) * 3600000L; - idx++; - } - if (parts.length >= 2) { - result += Integer.valueOf(parts[idx]) * 60000L; - idx++; - result += (Float.valueOf(parts[idx])) * 1000L; - } - return result; - } - - public static String formatRFC822Date(Date date) { - SimpleDateFormat format = RFC822Formatter.get(); - return format.format(date); - } - - public static String formatRFC3339Local(Date date) { - SimpleDateFormat format = RFC3339Formatter.get(); - format.applyPattern(RFC3339LOCAL); - String result = format.format(date); - format.applyPattern(RFC3339UTC); - return result; - } - - public static String formatRFC3339UTC(Date date) { - SimpleDateFormat format = RFC3339Formatter.get(); - format.applyPattern(RFC3339UTC); - return format.format(date); - } -} 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 new file mode 100644 index 000000000..b6df2dc85 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/DateUtils.java @@ -0,0 +1,114 @@ +package de.danoeh.antennapod.core.util; + +import android.util.Log; + +import org.apache.commons.lang3.StringUtils; + +import java.text.ParsePosition; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +/** + * Parses several date formats. + */ +public class DateUtils { + + private static final String TAG = "DateUtils"; + + public static Date parse(final String input) { + if(input == null) { + throw new IllegalArgumentException("Date most not be null"); + } + String date = input.replace('/', '-'); + if(date.contains(".")) { + int start = date.indexOf('.'); + int current = start+1; + while(current < date.length() && Character.isDigit(date.charAt(current))) { + current++; + } + if(current - start > 4) { + if(current < date.length()-1) { + date = date.substring(0, start + 4) + date.substring(current); + } else { + date = date.substring(0, start + 4); + } + } else if(current - start < 4) { + if(current < date.length()-1) { + date = date.substring(0, current) + StringUtils.repeat("0", 4-(current-start)) + date.substring(current); + } else { + date = date.substring(0, current) + StringUtils.repeat("0", 4-(current-start)); + } + + } + } + String[] patterns = { + "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" + }; + SimpleDateFormat parser = new SimpleDateFormat("", Locale.US); + parser.setLenient(false); + ParsePosition pos = new ParsePosition(0); + for(String pattern : patterns) { + parser.applyPattern(pattern); + pos.setIndex(0); + Date result = parser.parse(date, pos); + if(result != null && pos.getIndex() == date.length()) { + return result; + } + } + + Log.d(TAG, "Could not parse date string \"" + input + "\""); + return null; + } + + + /** + * Takes a string of the form [HH:]MM:SS[.mmm] and converts it to + * milliseconds. + * + * @throws java.lang.NumberFormatException if the number segments contain invalid numbers. + */ + public static long parseTimeString(final String time) { + String[] parts = time.split(":"); + long result = 0; + int idx = 0; + if (parts.length == 3) { + // string has hours + result += Integer.valueOf(parts[idx]) * 3600000L; + idx++; + } + if (parts.length >= 2) { + result += Integer.valueOf(parts[idx]) * 60000L; + idx++; + result += (Float.valueOf(parts[idx])) * 1000L; + } + return result; + } + + public static String formatRFC822Date(Date date) { + SimpleDateFormat format = new SimpleDateFormat("dd MMM yy HH:mm:ss Z", Locale.US); + return format.format(date); + } + + public static String formatRFC3339Local(Date date) { + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US); + return format.format(date); + } + + public static String formatRFC3339UTC(Date date) { + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US); + return format.format(date); + } +} 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 new file mode 100644 index 000000000..8934f3272 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/LongList.java @@ -0,0 +1,240 @@ +package de.danoeh.antennapod.core.util; + +import java.util.Arrays; + +/** + * Fast and memory efficient long list + */ +public final class LongList { + + private long[] values; + private int size; + + /** + * Constructs an empty instance with a default initial capacity. + */ + public LongList() { + this(4); + } + + /** + * Constructs an empty instance. + * + * @param initialCapacity {@code >= 0;} initial capacity of the list + */ + public LongList(int initialCapacity) { + if(initialCapacity < 0) { + throw new IllegalArgumentException("initial capacity must be 0 or higher"); + } + values = new long[initialCapacity]; + size = 0; + } + + @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 boolean equals(Object other) { + if (other == this) { + return true; + } + if (! (other instanceof LongList)) { + return false; + } + LongList otherList = (LongList) 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("LongList{"); + 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 long 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 long set(int index, long value) { + if (index >= size) { + throw new IndexOutOfBoundsException("n >= size()"); + } else if(index < 0) { + throw new IndexOutOfBoundsException("n < 0"); + } + long 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(long 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(long 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. + long[] newArray = new long[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(long 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 long[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(long 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 long[] toArray() { + return Arrays.copyOf(values, size); + + } +} 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/QueueAccess.java b/core/src/main/java/de/danoeh/antennapod/core/util/QueueAccess.java index 8e40ae184..7377b202d 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/QueueAccess.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/QueueAccess.java @@ -1,10 +1,10 @@ package de.danoeh.antennapod.core.util; -import de.danoeh.antennapod.core.feed.FeedItem; - import java.util.Iterator; import java.util.List; +import de.danoeh.antennapod.core.feed.FeedItem; + /** * Provides methods for accessing the queue. It is possible to load only a part of the information about the queue that * is stored in the database (e.g. sometimes the user just has to test if a specific item is contained in the List. @@ -25,23 +25,6 @@ public abstract class QueueAccess { public abstract boolean remove(long id); private QueueAccess() { - - } - - public static QueueAccess IDListAccess(final List<Long> ids) { - return new QueueAccess() { - @Override - public boolean contains(long id) { - return (ids != null) && ids.contains(id); - } - - @Override - public boolean remove(long id) { - return ids.remove(id); - } - - - }; } public static QueueAccess ItemListAccess(final List<FeedItem> items) { 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 0e03bc8b4..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 @@ -1,9 +1,6 @@ package de.danoeh.antennapod.core.util.gui; -import android.os.Bundle; import android.os.Handler; -import android.os.Parcelable; -import android.text.TextUtils; import android.view.View; import android.widget.TextView; @@ -16,23 +13,36 @@ import de.danoeh.antennapod.core.R; import static com.nineoldandroids.view.ViewPropertyAnimator.animate; -public class UndoBarController { +public class UndoBarController<T> { private View mBarView; private TextView mMessageView; private ViewPropertyAnimator mBarAnimator; private Handler mHideHandler = new Handler(); - private UndoListener mUndoListener; + private UndoListener<T> mUndoListener; // State objects - private Parcelable mUndoToken; + private T mUndoToken; private CharSequence mUndoMessage; - public interface UndoListener { - void onUndo(Parcelable token); + public interface UndoListener<T> { + /** + * This callback function is called when the undo button is pressed + * + * @param token + */ + void onUndo(T token); + + /** + * + * This callback function is called when the bar fades out without button press + * + * @param token + */ + void onHide(T token); } - public UndoBarController(View undoBarView, UndoListener undoListener) { + public UndoBarController(View undoBarView, UndoListener<T> undoListener) { mBarView = undoBarView; mBarAnimator = animate(mBarView); mUndoListener = undoListener; @@ -50,7 +60,7 @@ public class UndoBarController { hideUndoBar(true); } - public void showUndoBar(boolean immediate, CharSequence message, Parcelable undoToken) { + public void showUndoBar(boolean immediate, CharSequence message, T undoToken) { mUndoToken = undoToken; mUndoMessage = message; mMessageView.setText(mUndoMessage); @@ -73,6 +83,15 @@ public class UndoBarController { } } + public boolean isShowing() { + return mBarView.getVisibility() == View.VISIBLE; + } + + public void close() { + hideUndoBar(true); + mUndoListener.onHide(mUndoToken); + } + public void hideUndoBar(boolean immediate) { mHideHandler.removeCallbacks(mHideRunnable); if (immediate) { @@ -96,26 +115,11 @@ public class UndoBarController { } } - public void onSaveInstanceState(Bundle outState) { - outState.putCharSequence("undo_message", mUndoMessage); - outState.putParcelable("undo_token", mUndoToken); - } - - public void onRestoreInstanceState(Bundle savedInstanceState) { - if (savedInstanceState != null) { - mUndoMessage = savedInstanceState.getCharSequence("undo_message"); - mUndoToken = savedInstanceState.getParcelable("undo_token"); - - if (mUndoToken != null || !TextUtils.isEmpty(mUndoMessage)) { - showUndoBar(true, mUndoMessage, mUndoToken); - } - } - } - private Runnable mHideRunnable = new Runnable() { @Override public void run() { hideUndoBar(false); + mUndoListener.onHide(mUndoToken); } }; } 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 26dd2ec4c..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 @@ -1,7 +1,13 @@ package de.danoeh.antennapod.core.util.playback; import android.app.Activity; -import android.content.*; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.ServiceConnection; +import android.content.SharedPreferences; import android.content.res.TypedArray; import android.media.MediaPlayer; import android.os.AsyncTask; @@ -19,13 +25,18 @@ import android.widget.TextView; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; -import de.danoeh.antennapod.core.BuildConfig; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + 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; @@ -33,8 +44,6 @@ import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.util.Converter; import de.danoeh.antennapod.core.util.playback.Playable.PlayableUtils; -import java.util.concurrent.*; - /** * Communicates with the playback service. GUI classes should use this class to * control playback instead of communicating with the PlaybackService directly. @@ -118,8 +127,7 @@ public abstract class PlaybackController { * example in the activity's onStop() method. */ public void release() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Releasing PlaybackController"); + Log.d(TAG, "Releasing PlaybackController"); try { activity.unregisterReceiver(statusUpdate); @@ -164,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) { @@ -177,7 +184,7 @@ public abstract class PlaybackController { boolean bound = false; if (!PlaybackService.started) { if (serviceIntent != null) { - if (BuildConfig.DEBUG) Log.d(TAG, "Calling start service"); + Log.d(TAG, "Calling start service"); activity.startService(serviceIntent); bound = activity.bindService(serviceIntent, mConnection, 0); } else { @@ -186,14 +193,11 @@ public abstract class PlaybackController { handleStatus(); } } else { - if (BuildConfig.DEBUG) - Log.d(TAG, - "PlaybackService is running, trying to connect without start command."); + Log.d(TAG, "PlaybackService is running, trying to connect without start command."); bound = activity.bindService(new Intent(activity, PlaybackService.class), mConnection, 0); } - if (BuildConfig.DEBUG) - Log.d(TAG, "Result for service binding: " + bound); + Log.d(TAG, "Result for service binding: " + bound); } }; intentLoader.execute(); @@ -204,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 @@ -233,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; } @@ -246,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, @@ -259,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); } } @@ -272,8 +272,7 @@ public abstract class PlaybackController { .getService(); if (!released) { queryService(); - if (BuildConfig.DEBUG) - Log.d(TAG, "Connection to Service established"); + Log.d(TAG, "Connection to Service established"); } else { Log.i(TAG, "Connection to playback service has been established, but controller has already been released"); } @@ -282,17 +281,14 @@ public abstract class PlaybackController { @Override public void onServiceDisconnected(ComponentName name) { playbackService = null; - if (BuildConfig.DEBUG) - Log.d(TAG, "Disconnected from Service"); - + Log.d(TAG, "Disconnected from Service"); } }; 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; @@ -349,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(); @@ -421,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: @@ -466,6 +462,7 @@ public abstract class PlaybackController { updatePlayButtonAppearance(playResource, playText); break; case SEEKING: + onPositionObserverUpdate(); postStatusMsg(R.string.player_seeking_msg); break; case INITIALIZED: @@ -501,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(); @@ -610,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..90304a404 100644 --- a/core/src/main/res/values-cs-rCZ/strings.xml +++ b/core/src/main/res/values-cs-rCZ/strings.xml @@ -26,6 +26,7 @@ <!--Main activity--> <string name="drawer_open">Otevřít menu</string> <string name="drawer_close">Zavřít menu</string> + <string name="drawer_preferences">Nastavení panelu</string> <!--Webview actions--> <string name="open_in_browser_label">Otevřít v prohlížeči</string> <string name="copy_url_label">Kopírovat URL</string> @@ -39,6 +40,7 @@ <string name="cancel_label">Zrušit</string> <string name="author_label">Autor</string> <string name="language_label">Jazyk</string> + <string name="url_label">URL</string> <string name="podcast_settings_label">Nastavení</string> <string name="cover_label">Obrázek</string> <string name="error_label">Chyba</string> @@ -58,6 +60,7 @@ <string name="close_label">Zavřít</string> <string name="retry_label">Zkusit znovu</string> <string name="auto_download_label">Zahrnout do automaticky stahovaných</string> + <string name="parallel_downloads_suffix">\u0020paralelních stahování</string> <!--'Add Feed' Activity labels--> <string name="feedurl_label">URL kanálu</string> <string name="etxtFeedurlHint">URL nebo webová stránka kanálu</string> @@ -66,8 +69,10 @@ <string name="podcastdirectories_descr">Můžete vyhledávat nové podcasty podle jména, kategorie nebo popularity v seznamu gpodder.net.</string> <string name="browse_gpoddernet_label">Prohledávat gpodder.net</string> <!--Actions on feeds--> - <string name="mark_all_read_label">Označit vše jako přečtené</string> - <string name="mark_all_read_msg">Označit všechny epizody jako přečtené</string> + <string name="mark_all_read_label">Označit vše jako poslechnuté</string> + <string name="mark_all_read_msg">Všechny epizody označeny jako poslechnuté</string> + <string name="mark_all_read_confirmation_msg">Prosím potvrďte, že chcete označit všechny vybrané epizody jako poslechnuté.</string> + <string name="mark_all_read_feed_confirmation_msg">Prosím potvrďte, že chcete označit všechny epizody z tohoto zdroje jako poslechnuté.</string> <string name="show_info_label">Informace o zdroji</string> <string name="remove_feed_label">Odstranit podcast</string> <string name="share_link_label">Sdílet odkaz</string> @@ -75,6 +80,16 @@ <string name="feed_delete_confirmation_msg">Prosím potvrďte, že chcete smazat tento kanál včetně všech stažených epizod.</string> <string name="feed_remover_msg">Odstranit kanál</string> <string name="load_complete_feed">Obnovit kompletní kanál</string> + <string name="hide_episodes_title">Skrýt epizody</string> + <string name="hide_unplayed_episodes_label">Neposlechnuté</string> + <string name="hide_paused_episodes_label">Pozastavené</string> + <string name="hide_played_episodes_label">Poslechnuté</string> + <string name="hide_queued_episodes_label">Ve frontě</string> + <string name="hide_not_queued_episodes_label">Mimo frontu</string> + <string name="hide_downloaded_episodes_label">Stažené</string> + <string name="hide_not_downloaded_episodes_label">Nestažené</string> + <string name="filtered_label">Filtrované</string> + <string name="refresh_failed_msg">{fa-exclamation-circle} Poslední aktualizace selhala</string> <!--actions on feeditems--> <string name="download_label">Stáhnout</string> <string name="play_label">Přehrát</string> @@ -83,15 +98,20 @@ <string name="stream_label">Vysílat</string> <string name="remove_label">Odstranit</string> <string name="remove_episode_lable">Odstranit epizodu</string> - <string name="mark_read_label">Označit jako přečtené</string> - <string name="mark_unread_label">Označit jako nepřečtené</string> + <string name="mark_read_label">Označit jako poslechnuté</string> + <string name="mark_unread_label">Označit jako neposlechnuté</string> + <string name="marked_as_read_label">Označeno jako poslechnuté</string> <string name="add_to_queue_label">Přidat do fronty</string> + <string name="added_to_queue_label">Přidáno do fronty</string> <string name="remove_from_queue_label">Odebrat z fronty</string> <string name="visit_website_label">Navštívit stránku</string> <string name="support_label">Flattrovat</string> <string name="enqueue_all_new">Vše do fronty</string> <string name="download_all">Stáhnout vše</string> <string name="skip_episode_label">Přeskočit epizodu</string> + <string name="activate_auto_download">Aktivovat automatické stahování</string> + <string name="deactivate_auto_download">Deaktivovat automatické stahování</string> + <string name="reset_position">Vymazat pozici přehrávání</string> <!--Download messages and labels--> <string name="download_successful">úspěšné</string> <string name="download_failed">selhalo</string> @@ -108,8 +128,10 @@ <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_report_title">Všechna stahování dokončena</string> + <string name="download_canceled_msg">Stahování zrušeno</string> + <string name="download_canceled_autodownload_enabled_msg">Stahování zrušeno\nVypnuto <i>automatické stahování</i> této položky</string> + <string name="download_report_title">Stahování dokončeno s chybou</string> + <string name="download_report_content_title">Report stahování</string> <string name="download_error_malformed_url">Chybné URL</string> <string name="download_error_io_error">IO chyba</string> <string name="download_error_request_error">Chyba požadavku</string> @@ -125,6 +147,11 @@ <string name="download_request_error_dialog_message_prefix">Nastala chyba při pokusu o stažení souboru:\u0020</string> <string name="authentication_notification_title">Vyžadováno ověření</string> <string name="authentication_notification_msg">Zdroj který jste vybrali vyžaduje zadání uživatelského jména a hesla</string> + <string name="confirm_mobile_download_dialog_title">Potvrdit mobilní stahování</string> + <string name="confirm_mobile_download_dialog_message_not_in_queue">Stahování dat přes mobilní připojení je v nastavení vypnuto.\n\nDočasně povolit nebo pouze přidat do fronty?\n\n<small>Tato volba bude platná po dalších 10 minut.</small></string> + <string name="confirm_mobile_download_dialog_message">Stahování dat přes mobilní připojení je v nastavení vypnuto.\n\nDočasně povolit?\n\n<small>Tato volba bude platná po dalších 10 minut.</small></string> + <string name="confirm_mobile_download_dialog_only_add_to_queue">Pouze přidat do fronty</string> + <string name="confirm_mobile_download_dialog_enable_temporarily">Dočasně povolit</string> <!--Mediaplayer messages--> <string name="player_error_msg">Chyba!</string> <string name="player_stopped_msg">Žádné probíhající přehrávání</string> @@ -139,6 +166,8 @@ <string name="playbackservice_notification_title">Přehrávaný podcast</string> <string name="unknown_media_key">AntennaPod - Neznámý klíč médií: %1$d</string> <!--Queue operations--> + <string name="lock_queue">Zamknout frontu</string> + <string name="unlock_queue">Odemknout frontu</string> <string name="clear_queue_label">Vyprázdnit frontu</string> <string name="undo">Zpět</string> <string name="removed_from_queue">Položka odebrána</string> @@ -150,6 +179,7 @@ <string name="duration">Délka</string> <string name="ascending">Vzestupně</string> <string name="descending">Sestupně</string> + <string name="clear_queue_confirmation_msg">Prosím potvrďte, že chcete vyčistit tuto frontu a VŠECHNY v ní obsažené epizody</string> <!--Flattr--> <string name="flattr_auth_label">Flattr přihlášení</string> <string name="flattr_auth_explanation">Stiskněte následující tlačítko pro spuštění autentizačního procesu. Budete přesměrováni na přihlašovací obrazovku flattru a vyzváni k potvrzení udělení práv pro použití flattru aplikací AntennaPod. Po udělení práv se automaticky vrátíte na tuto obrazovku.</string> @@ -165,10 +195,17 @@ <string name="access_revoked_title">Přístup ukončen</string> <string name="access_revoked_info">Úspěšně revokován přístup AntennPodu k vašemu účtu. Pro dokončení tohoto procesu je ještě zapotřebí na stránkách flattru odebrat z vašeho účtu AntennaPod ze seznamu povolených aplikací.</string> <!--Flattr--> + <string name="flattr_click_success">Flattrován jeden příspěvek!</string> + <string name="flattr_click_success_count">Flattrováno %d příspěvků!</string> + <string name="flattr_click_success_queue">Flattrován: %s.</string> + <string name="flattr_click_failure_count">Selhalo flattrování %d příspěvků!</string> + <string name="flattr_click_failure">Neflattrováno: %s.</string> + <string name="flattr_click_enqueued">Příspěvek bude flattrován později</string> <string name="flattring_thing">Flattruji %s</string> <string name="flattring_label">AntennaPod flattruje</string> <string name="flattrd_label">AntennaPod flattroval</string> <string name="flattrd_failed_label">AntennaPod flattr selhal</string> + <string name="flattr_retrieving_status">Získávání flattrovaných příspěvků</string> <!--Variable Speed--> <string name="download_plugin_label">Stáhnout plugin</string> <string name="no_playback_plugin_title">Plugin není nainstalován</string> @@ -188,6 +225,8 @@ <string name="pref_followQueue_sum">Po přehrání položky z fronty přejít automaticky na další</string> <string name="pref_auto_delete_sum">Smazat díl po jeho přehrání</string> <string name="pref_auto_delete_title">Automatické mazání</string> + <string name="pref_smart_mark_as_played_sum">Označit epizody jako poslechnuté i pokud ještě zbývá určitý počet sekund přehrávání do jejich konce</string> + <string name="pref_smart_mark_as_played_title">Chytré označování jako poslechnuté</string> <string name="playback_pref">Přehrávání</string> <string name="network_pref">Síť</string> <string name="pref_autoUpdateIntervall_title">Interval aktualizace zdrojů</string> @@ -211,6 +250,8 @@ <string name="pref_auto_flattr_sum">Nastavit automatické flattrování</string> <string name="user_interface_label">Uživatelské rozhraní</string> <string name="pref_set_theme_title">Vybrat motiv</string> + <string name="pref_nav_drawer_items_title">Změnit navigační panel</string> + <string name="pref_nav_drawer_items_sum">Upravit zobrazení položek v navigačním panelu.</string> <string name="pref_set_theme_sum">Změnit vzhled AntennaPod.</string> <string name="pref_automatic_download_title">Automatické stahování</string> <string name="pref_automatic_download_sum">Nastavení automatického stahování epizod.</string> @@ -218,6 +259,7 @@ <string name="pref_autodl_wifi_filter_sum">Povolit automatické stahování pouze pomocí vybraných Wi-Fi sítí.</string> <string name="pref_automatic_download_on_battery_title">Stahovat, pokud neprobíhá nabíjení</string> <string name="pref_automatic_download_on_battery_sum">Povolit automatické stahování i pokud není baterie nabíjena</string> + <string name="pref_parallel_downloads_title">Paralelní stahování</string> <string name="pref_episode_cache_title">Historie epizod</string> <string name="pref_theme_title_light">Světlý</string> <string name="pref_theme_title_dark">Tmavý</string> @@ -233,8 +275,8 @@ <string name="pref_gpodnet_setlogin_information_sum">Změní přihlašovací údaje k vašemu gpodder.net účtu.</string> <string name="pref_playback_speed_title">Rychlosti přehrávání</string> <string name="pref_playback_speed_sum">Přizpůsobení rychlosti je dostupné pro přehrávání zvuku různými rychlostmi</string> - <string name="pref_seek_delta_title">Čas poskočení</string> - <string name="pref_seek_delta_sum">Poskočí o vybraný počet sekund při posunu zpět nebo vpřed</string> + <string name="pref_fast_forward">Čas rychlého posunu</string> + <string name="pref_rewind">Čas přetočení</string> <string name="pref_gpodnet_sethostname_title">Nastavit hostname</string> <string name="pref_gpodnet_sethostname_use_default_host">Použít přednastaveného hosta</string> <string name="pref_expandNotify_title">Rozšířené upozornění</string> @@ -242,6 +284,9 @@ <string name="pref_persistNotify_title">Pevné ovládání přehrávání</string> <string name="pref_persistNotify_sum">Zachovat upozornění a ovládání na obrazovce uzamčení i při pozastaveném přehrávání.</string> <string name="pref_expand_notify_unsupport_toast">Verze Androidu nižší než 4.1 nepodporují rozšířená upozornění.</string> + <string name="pref_queueAddToFront_sum">Přidávat nové epizody na začátek fronty.</string> + <string name="pref_queueAddToFront_title">Přidat na začátek.</string> + <string name="pref_smart_mark_as_played_disabled">Vypnuto</string> <!--Auto-Flattr dialog--> <string name="auto_flattr_enable">Povolit automatické flattrování</string> <string name="auto_flattr_after_percent">Flattrovat díl jakmile bude odehráno %d procent</string> @@ -256,6 +301,9 @@ <string name="found_in_title_label">Nalezeno v názvu</string> <!--OPML import and export--> <string name="opml_import_txtv_button_lable">OPML soubory umožňují přenést vaše podcasty z jednoho přehrávače na jiný.</string> + <string name="opml_import_explanation_1">Vybrat umístění v místním souborovém systému.</string> + <string name="opml_import_explanation_2">Použít externí aplikaci jako třeba Dropbox, Google Drive nebo oblíbeného správce souborů k otevření OPML souboru.</string> + <string name="opml_import_explanation_3">Mnoho aplikací jako třeba Google Mail, Dropbox, Google Drive a většina správců souborů umí <i>otevřít</i> OPML soubory <i>v aplikaci</i> AntennaPod.</string> <string name="start_import_label">Importovat</string> <string name="opml_import_label">OPML import</string> <string name="opml_directory_error">CHYBA!</string> @@ -264,6 +312,8 @@ <string name="opml_import_error_dir_empty">Adresář importu je prázdný.</string> <string name="select_all_label">Označit vše</string> <string name="deselect_all_label">Zrušit výběr</string> + <string name="choose_file_from_filesystem">Z místního souborového systému</string> + <string name="choose_file_from_external_application">Použít externí aplikaci</string> <string name="opml_export_label">OPML export</string> <string name="exporting_label">Exportuji...</string> <string name="export_error_label">Chyba exportu</string> @@ -283,7 +333,7 @@ <string name="gpodnet_taglist_header">KATEGORIE</string> <string name="gpodnet_toplist_header">TOP PODCASTY</string> <string name="gpodnet_suggestions_header">DOPORUČENÉ</string> - <string name="gpodnet_search_hint">Vyhledat na gpodder.net</string> + <string name="gpodnet_search_hint">Prohledat gpodder.net</string> <string name="gpodnetauth_login_title">Přihlásit</string> <string name="gpodnetauth_login_descr">Vítejte do průvodce přihlášením ke gpodder.net účtu. Zadejte vaše přihlašovací údaje:</string> <string name="gpodnetauth_login_butLabel">Přihlásit</string> @@ -321,6 +371,8 @@ <string name="set_to_default_folder">Vybrat hlavní adresář</string> <string name="pref_pausePlaybackForFocusLoss_sum">Místo snížení hlasitosti pozastavit přehrávání v případě, že jiná aplikace přehrává zvuk.</string> <string name="pref_pausePlaybackForFocusLoss_title">Automatické pozastavení přehrávání</string> + <string name="pref_resumeAfterCall_sum">Pokračovat v přehrávání po ukončení telefonního hovoru</string> + <string name="pref_resumeAfterCall_title">Pokračovat po telefonátu</string> <!--Online feed view--> <string name="subscribe_label">Odebírat</string> <string name="subscribed_label">Odebíráno</string> @@ -349,4 +401,5 @@ <string name="authentication_descr">Změnit uživatelské jméno a heslo pro tento podcast a jeho epizody.</string> <!--AntennaPodSP--> <string name="sp_apps_importing_feeds_msg">Importuji odběry z jednoúčelových aplikací...</string> + <string name="search_itunes_label">Prohledat iTunes</string> </resources> 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..93e52acb6 100644 --- a/core/src/main/res/values-de/strings.xml +++ b/core/src/main/res/values-de/strings.xml @@ -26,6 +26,7 @@ <!--Main activity--> <string name="drawer_open">Menü öffnen</string> <string name="drawer_close">Menü schließen</string> + <string name="drawer_preferences">Seitenleisten-Einstellungen</string> <!--Webview actions--> <string name="open_in_browser_label">Im Browser öffnen</string> <string name="copy_url_label">URL kopieren</string> @@ -39,6 +40,7 @@ <string name="cancel_label">Abbrechen</string> <string name="author_label">Autor</string> <string name="language_label">Sprache</string> + <string name="url_label">URL</string> <string name="podcast_settings_label">Einstellungen</string> <string name="cover_label">Bild</string> <string name="error_label">Fehler</string> @@ -64,7 +66,7 @@ <string name="etxtFeedurlHint">URL des Feeds oder der Webseite</string> <string name="txtvfeedurl_label">Podcast per URL hinzufügen</string> <string name="podcastdirectories_label">Podcast in Verzeichnis finden</string> - <string name="podcastdirectories_descr">Bei gpodder.net kannst du neue Podcasts nach Name, Kategorie oder Popularität suchen.</string> + <string name="podcastdirectories_descr">Bei gpodder.net kannst du neue Podcasts nach Name, Kategorie oder Popularität suchen. Oder suche bei iTunes.</string> <string name="browse_gpoddernet_label">gpodder.net durchsuchen</string> <!--Actions on feeds--> <string name="mark_all_read_label">Alle als gespielt markieren</string> @@ -78,6 +80,16 @@ <string name="feed_delete_confirmation_msg">Bitte bestätige, dass du diesen Feed und ALLE heruntergeladenen Episoden dieses Feeds entfernen möchtest.</string> <string name="feed_remover_msg">Entferne Feed</string> <string name="load_complete_feed">Kompletten Feed aktualisieren</string> + <string name="hide_episodes_title">Episoden verbergen</string> + <string name="hide_unplayed_episodes_label">Ungespielt</string> + <string name="hide_paused_episodes_label">Pausiert</string> + <string name="hide_played_episodes_label">Gespielt</string> + <string name="hide_queued_episodes_label">In Abspielliste</string> + <string name="hide_not_queued_episodes_label">Nicht in Abspielliste</string> + <string name="hide_downloaded_episodes_label">Heruntergeladen</string> + <string name="hide_not_downloaded_episodes_label">Nicht heruntergeladen</string> + <string name="filtered_label">Gefiltert</string> + <string name="refresh_failed_msg">{fa-exclamation-circle} Aktualisierung fehlgeschlagen</string> <!--actions on feeditems--> <string name="download_label">Herunterladen</string> <string name="play_label">Abspielen</string> @@ -90,12 +102,16 @@ <string name="mark_unread_label">Als ungespielt markieren</string> <string name="marked_as_read_label">Als gespielt markiert</string> <string name="add_to_queue_label">Zur Abspielliste hinzufügen</string> + <string name="added_to_queue_label">Zur Abspielliste hinzugefügt</string> <string name="remove_from_queue_label">Aus der Abspielliste entfernen</string> <string name="visit_website_label">Webseite besuchen</string> <string name="support_label">Flattrn</string> <string name="enqueue_all_new">Alle zur Abspielliste hinzufügen</string> <string name="download_all">Alle herunterladen</string> <string name="skip_episode_label">Episode überspringen</string> + <string name="activate_auto_download">Automatischen Download aktivieren</string> + <string name="deactivate_auto_download">Automatischen Download deaktivieren</string> + <string name="reset_position">Wiedergabe-Position zurücksetzen</string> <!--Download messages and labels--> <string name="download_successful">erfolgreich</string> <string name="download_failed">fehlgeschlagen</string> @@ -112,8 +128,10 @@ <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_report_title">Download abgeschlossen</string> + <string name="download_canceled_msg">Download abgebrochen</string> + <string name="download_canceled_autodownload_enabled_msg">Download abgebrochen\n<i>Automatischen Download</i> für diese Episode deaktiviert</string> + <string name="download_report_title">Downloads endeten mit Fehler(n)</string> + <string name="download_report_content_title">Download-Bericht</string> <string name="download_error_malformed_url">Fehler in URL</string> <string name="download_error_io_error">E/A Error</string> <string name="download_error_request_error">Anfragefehler</string> @@ -129,6 +147,11 @@ <string name="download_request_error_dialog_message_prefix">Beim Herunterladen der Datei ist ein Fehler aufgetreten:\u0020</string> <string name="authentication_notification_title">Authentifizierung erforderlich</string> <string name="authentication_notification_msg">Die angeforderte Quelle erfordert einen Benutzernamen und ein Passwort</string> + <string name="confirm_mobile_download_dialog_title">Mobilen Download bestätigen</string> + <string name="confirm_mobile_download_dialog_message_not_in_queue">Das Herunterladen über die mobile Datenverbindung ist in den Einstellungen deaktiviert.\n\nVorübergehend erlauben oder nur zur Abspielliste hinzufügen?\n\n<small>Deine Entscheidung wird für 10 Minuten gespeichert.</small></string> + <string name="confirm_mobile_download_dialog_message">Das Herunterladen über die mobile Datenverbindung ist in den Einstellungen deaktiviert.\n\nVorübergehend erlauben?\n\n<small>Deine Entscheidung wird für 10 Minuten gespeichert.</small></string> + <string name="confirm_mobile_download_dialog_only_add_to_queue">Zur Abspielliste hinzufügen</string> + <string name="confirm_mobile_download_dialog_enable_temporarily">Vorübergehend erlauben</string> <!--Mediaplayer messages--> <string name="player_error_msg">Fehler!</string> <string name="player_stopped_msg">Keine Medienwiedergabe</string> @@ -143,6 +166,8 @@ <string name="playbackservice_notification_title">Spiele Podcast ab</string> <string name="unknown_media_key">AntennaPod - Unbekannte Medientaste: %1$d</string> <!--Queue operations--> + <string name="lock_queue">Abspielliste sperren</string> + <string name="unlock_queue">Abspielliste entsperren</string> <string name="clear_queue_label">Abspielliste leeren</string> <string name="undo">Rückgängig</string> <string name="removed_from_queue">Element entfernt</string> @@ -200,6 +225,8 @@ <string name="pref_followQueue_sum">Springe zur nächsten Episode, wenn die vorherige Episode endet</string> <string name="pref_auto_delete_sum">Episode löschen, wenn die Wiedergabe endet</string> <string name="pref_auto_delete_title">Automatisches Löschen</string> + <string name="pref_smart_mark_as_played_sum">Episoden werden als gespielt markiert, wenn weniger als eine bestimmte Anzahl Sekunden Restspielzeit übrig sind</string> + <string name="pref_smart_mark_as_played_title">Schlaues als gespielt markieren</string> <string name="playback_pref">Wiedergabe</string> <string name="network_pref">Netzwerk</string> <string name="pref_autoUpdateIntervall_title">Aktualisierungsintervall</string> @@ -223,6 +250,8 @@ <string name="pref_auto_flattr_sum">Automatisches Flattrn konfigurieren</string> <string name="user_interface_label">Benutzeroberfläche</string> <string name="pref_set_theme_title">Theme auswählen</string> + <string name="pref_nav_drawer_items_title">Seitenleiste ändern</string> + <string name="pref_nav_drawer_items_sum">Ändere, welche Listen in der Seitenleiste erscheinen</string> <string name="pref_set_theme_sum">Ändere das Aussehen von AntennaPod.</string> <string name="pref_automatic_download_title">Automatisches Herunterladen</string> <string name="pref_automatic_download_sum">Konfiguriere das automatische Herunterladen von Episoden.</string> @@ -246,8 +275,8 @@ <string name="pref_gpodnet_setlogin_information_sum">Ändere die Anmeldeinformationen deines gpodder.net Profils</string> <string name="pref_playback_speed_title">Wiedergabegeschwindigkeiten</string> <string name="pref_playback_speed_sum">Lege die verfügbaren Werte für die Veränderung der Wiedergabeschwindigkeit fest</string> - <string name="pref_seek_delta_title">Spul-Zeit</string> - <string name="pref_seek_delta_sum">Spule so viele Sekunden vor oder zurück</string> + <string name="pref_fast_forward">Vorspulzeit</string> + <string name="pref_rewind">Rückspulzeit</string> <string name="pref_gpodnet_sethostname_title">Hostname ändern</string> <string name="pref_gpodnet_sethostname_use_default_host">Standard-Host verwenden</string> <string name="pref_expandNotify_title">Benachrichtigung erweitern</string> @@ -257,6 +286,7 @@ <string name="pref_expand_notify_unsupport_toast">Android-Versionen vor 4.1 unterstützen keine erweiterten Benachrichtigungen.</string> <string name="pref_queueAddToFront_sum">Fügen Sie neue Folgen auf den Anfang der Warteschlange.</string> <string name="pref_queueAddToFront_title">Vorne in Abspielliste einreihen</string> + <string name="pref_smart_mark_as_played_disabled">Deaktiviert</string> <!--Auto-Flattr dialog--> <string name="auto_flattr_enable">Automatisches Flattrn aktivieren</string> <string name="auto_flattr_after_percent">Flattr eine Episode, sobald %d Prozent gespielt worden sind</string> @@ -341,6 +371,8 @@ <string name="set_to_default_folder">Standardordner auswählen</string> <string name="pref_pausePlaybackForFocusLoss_sum">Pausiere die Wiedergabe statt die Lautstärke zu reduzieren, wenn eine andere Anwendung Töne abspielt</string> <string name="pref_pausePlaybackForFocusLoss_title">Bei Unterbrechungen pausieren</string> + <string name="pref_resumeAfterCall_sum">Wiedergabe fortsetzen, wenn Anruf beendet ist</string> + <string name="pref_resumeAfterCall_title">Nach Anruf fortsetzen</string> <!--Online feed view--> <string name="subscribe_label">Abonnieren</string> <string name="subscribed_label">Abonniert</string> @@ -369,5 +401,5 @@ <string name="authentication_descr">Ändere den Benutzernamen und das Passwort für diesen Podcast und dessen Episoden.</string> <!--AntennaPodSP--> <string name="sp_apps_importing_feeds_msg">Importiere Abonnements aus Single-Purpose Apps</string> - <string name="search_itunes_label">iTunes suchen</string> + <string name="search_itunes_label">iTunes durchsuchen</string> </resources> 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 2dbe4e8d4..d5255a589 100644 --- a/core/src/main/res/values-es/strings.xml +++ b/core/src/main/res/values-es/strings.xml @@ -26,6 +26,7 @@ <!--Main activity--> <string name="drawer_open">Abrir menú</string> <string name="drawer_close">Cerrar menú</string> + <string name="drawer_preferences">Preferencias del cajón</string> <!--Webview actions--> <string name="open_in_browser_label">Abrir en el navegador</string> <string name="copy_url_label">Copiar URL</string> @@ -39,6 +40,7 @@ <string name="cancel_label">Cancelar</string> <string name="author_label">Autor</string> <string name="language_label">Idioma</string> + <string name="url_label">URL</string> <string name="podcast_settings_label">Ajustes</string> <string name="cover_label">Imagen</string> <string name="error_label">Error</string> @@ -67,10 +69,10 @@ <string name="podcastdirectories_descr">Es posible buscar podcasts nuevos por nombre, categoría o popularidad en el directorio de gpodder.net.</string> <string name="browse_gpoddernet_label">Explorar gpodder.net</string> <!--Actions on feeds--> - <string name="mark_all_read_label">Marcar todo como leído</string> - <string name="mark_all_read_msg">Se marcaron todos los episodios como leídos</string> - <string name="mark_all_read_confirmation_msg">Por favor, confirme que desea marcar todos los episodios como leídos.</string> - <string name="mark_all_read_feed_confirmation_msg">Por favor, confirme que desea marcar todos los episodios de este feed como leídos.</string> + <string name="mark_all_read_label">Marcar todos como escuchado</string> + <string name="mark_all_read_msg">Se marcaron todos los episodios como escuchados</string> + <string name="mark_all_read_confirmation_msg">Por favor, confirme que desea marcar todos los episodios como escuchados.</string> + <string name="mark_all_read_feed_confirmation_msg">Por favor, confirme que desea marcar todos los episodios de este feed como escuchados.</string> <string name="show_info_label">Información del programa</string> <string name="remove_feed_label">Eliminar podcast</string> <string name="share_link_label">Compartir el enlace de la web</string> @@ -78,6 +80,16 @@ <string name="feed_delete_confirmation_msg">Confirme que quiere eliminar este canal y TODOS los episodios descargados del mismo.</string> <string name="feed_remover_msg">Quitando el canal</string> <string name="load_complete_feed">Actualizar el canal completo</string> + <string name="hide_episodes_title">Ocultar episodios</string> + <string name="hide_unplayed_episodes_label">No escuchados</string> + <string name="hide_paused_episodes_label">Pausados</string> + <string name="hide_played_episodes_label">Escuchados</string> + <string name="hide_queued_episodes_label">En cola</string> + <string name="hide_not_queued_episodes_label">No en cola</string> + <string name="hide_downloaded_episodes_label">Descargados</string> + <string name="hide_not_downloaded_episodes_label">No descargados</string> + <string name="filtered_label">Filtrados</string> + <string name="refresh_failed_msg">{fa-exclamation-circle} Error en última actualización</string> <!--actions on feeditems--> <string name="download_label">Descargar</string> <string name="play_label">Reproducir</string> @@ -86,16 +98,20 @@ <string name="stream_label">Transmitir</string> <string name="remove_label">Quitar</string> <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="mark_read_label">Marcar como escuchado</string> + <string name="mark_unread_label">Marcar como no escuchado</string> + <string name="marked_as_read_label">Marcado como escuchado</string> <string name="add_to_queue_label">Añadir a la cola</string> + <string name="added_to_queue_label">Añadido 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> <string name="support_label">Añadir a Flattr</string> <string name="enqueue_all_new">Ponerlos todos en cola</string> <string name="download_all">Descargarlos todos</string> <string name="skip_episode_label">Omitir episodio</string> + <string name="activate_auto_download">Activar descarga automática</string> + <string name="deactivate_auto_download">Desactivar descarga automática</string> + <string name="reset_position">Resetear posición de reproducción</string> <!--Download messages and labels--> <string name="download_successful">exitoso</string> <string name="download_failed">fallido</string> @@ -112,8 +128,10 @@ <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_report_title">Descargas completadas</string> + <string name="download_canceled_msg">Descarga cancelada</string> + <string name="download_canceled_autodownload_enabled_msg">Cancelada descarga\nDeshabilitada <i>Descarga Automática</i> para este ítem</string> + <string name="download_report_title">Descargas completadas con error(es)</string> + <string name="download_report_content_title">Informe de descarga</string> <string name="download_error_malformed_url">URL con formato incorrecto</string> <string name="download_error_io_error">Error de E/S</string> <string name="download_error_request_error">Error de solicitud</string> @@ -129,6 +147,11 @@ <string name="download_request_error_dialog_message_prefix">Ha ocurrido un error al intentar descargar el archivo:\u0020</string> <string name="authentication_notification_title">Se necesita autenticación</string> <string name="authentication_notification_msg">Para acceder al recurso solicitado debe proporcionar un usuario y contraseña</string> + <string name="confirm_mobile_download_dialog_title">Confirmar descarga por red móvil</string> + <string name="confirm_mobile_download_dialog_message_not_in_queue">Las descargas sobre la red móvil están deshabilitadas en ajustes.\n\nHabilitar temporalmente o sólo agregar a la cola?\n\n<small>Esta elección será recordada por 10 minutos.</small></string> + <string name="confirm_mobile_download_dialog_message">Las descargas sobre la red móvil están deshabilitadas en ajustes.\n\nHabilitar temporalmente?\n\n<small>Esta elección será recordada por 10 minutos.</small></string> + <string name="confirm_mobile_download_dialog_only_add_to_queue">Sólo agregar a la cola</string> + <string name="confirm_mobile_download_dialog_enable_temporarily">Habilitar temporalmente</string> <!--Mediaplayer messages--> <string name="player_error_msg">Error</string> <string name="player_stopped_msg">No hay medios en reproducción</string> @@ -143,6 +166,8 @@ <string name="playbackservice_notification_title">Reproduciendo el podcast</string> <string name="unknown_media_key">AntennaPod - Tecla multimedia desconocida: %1$d</string> <!--Queue operations--> + <string name="lock_queue">Bloquear cola</string> + <string name="unlock_queue">Desbloquear cola</string> <string name="clear_queue_label">Vaciar la cola</string> <string name="undo">Deshacer</string> <string name="removed_from_queue">Artículo eliminado</string> @@ -200,6 +225,8 @@ <string name="pref_followQueue_sum">Saltar al siguiente elemento de la cola al acabar la reproducción</string> <string name="pref_auto_delete_sum">Borrar episodio cuando finalice la reproducción</string> <string name="pref_auto_delete_title">Eliminar automáticamente</string> + <string name="pref_smart_mark_as_played_sum">Marcar episodios como escuchados incluso si todavía quedan unos segundos por escuchar</string> + <string name="pref_smart_mark_as_played_title">Marcar como escuchado inteligente</string> <string name="playback_pref">Reproducción</string> <string name="network_pref">Red</string> <string name="pref_autoUpdateIntervall_title">Intervalo de actualización</string> @@ -223,6 +250,8 @@ <string name="pref_auto_flattr_sum">Configurar flattr automático</string> <string name="user_interface_label">Interfaz de usuario</string> <string name="pref_set_theme_title">Elegir un tema</string> + <string name="pref_nav_drawer_items_title">Cambiar el cajón de navegación</string> + <string name="pref_nav_drawer_items_sum">Cambiar los ítems que aparecen en el cajón de navegación</string> <string name="pref_set_theme_sum">Cambiar la apariencia de AntennaPod.</string> <string name="pref_automatic_download_title">Descarga automática</string> <string name="pref_automatic_download_sum">Configurar la descarga automática de episodios.</string> @@ -246,8 +275,8 @@ <string name="pref_gpodnet_setlogin_information_sum">Modificar datos de inicio de sesión en gpodder.net.</string> <string name="pref_playback_speed_title">Velocidades de reproducción</string> <string name="pref_playback_speed_sum">Personalice las velocidades disponibles para la reproducción de audio a velocidad variable</string> - <string name="pref_seek_delta_title">Intervalo de búsqueda</string> - <string name="pref_seek_delta_sum">Avanzar o retroceder esta cantidad de segundos</string> + <string name="pref_fast_forward">Intervalo de avance</string> + <string name="pref_rewind">Intervalo de retroceso</string> <string name="pref_gpodnet_sethostname_title">Definir nombre de equipo</string> <string name="pref_gpodnet_sethostname_use_default_host">Usar nombre de equipo por defecto</string> <string name="pref_expandNotify_title">Expandir Notificación</string> @@ -257,6 +286,7 @@ <string name="pref_expand_notify_unsupport_toast">Las versiones de Android anteriores a la 4.1 no soportan notificaciones expandidas</string> <string name="pref_queueAddToFront_sum">Agregar nuevos episodios al principio de la cola.</string> <string name="pref_queueAddToFront_title">Poner al principio de la cola.</string> + <string name="pref_smart_mark_as_played_disabled">Deshabilitado</string> <!--Auto-Flattr dialog--> <string name="auto_flattr_enable">Habilitar Flattr automático</string> <string name="auto_flattr_after_percent">Hacer Flattr del episodio en cuanto se haya reproducido el %d por ciento</string> @@ -341,6 +371,8 @@ <string name="set_to_default_folder">Elegir carpeta predeterminada</string> <string name="pref_pausePlaybackForFocusLoss_sum">Pausar la reproducción en lugar de bajar el volumen cuando otra aplicación reproduzca sonidos</string> <string name="pref_pausePlaybackForFocusLoss_title">Pausar durante las interrupciones</string> + <string name="pref_resumeAfterCall_sum">Reanudar reproducción tras una llamada</string> + <string name="pref_resumeAfterCall_title">Reanudar tras una llamada</string> <!--Online feed view--> <string name="subscribe_label">Suscribirse</string> <string name="subscribed_label">Suscrito</string> diff --git a/core/src/main/res/values-fr/strings.xml b/core/src/main/res/values-fr/strings.xml index 617df3f8f..2f59e139d 100644 --- a/core/src/main/res/values-fr/strings.xml +++ b/core/src/main/res/values-fr/strings.xml @@ -64,7 +64,7 @@ <string name="etxtFeedurlHint">URL ou flux ou site web</string> <string name="txtvfeedurl_label">Ajouter un podcast par son URL</string> <string name="podcastdirectories_label">Trouver le podcast dans la bibliothèque</string> - <string name="podcastdirectories_descr">Vous pouvez chercher de nouveaux podcasts en filtrant par nom, catégorie ou popularité dans la bibliothèque gpodder.net</string> + <string name="podcastdirectories_descr">Vous pouvez chercher de nouveaux podcasts en filtrant par nom, catégorie ou popularité dans la bibliothèque gpodder.net, ou sur l\'iTunes Store.</string> <string name="browse_gpoddernet_label">Chercher sur gpodder.net</string> <!--Actions on feeds--> <string name="mark_all_read_label">Tous marquer comme lus</string> @@ -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> @@ -146,7 +146,7 @@ <string name="clear_queue_label">Effacer la liste</string> <string name="undo">Annuler</string> <string name="removed_from_queue">Élément retiré</string> - <string name="move_to_top_label">Déplacer vers le haut de haut de la liste</string> + <string name="move_to_top_label">Déplacer vers le haut de la liste</string> <string name="move_to_bottom_label">Déplacer vers le bas de la liste</string> <string name="sort">Trier</string> <string name="alpha">Ordre alphabétique</string> @@ -246,8 +246,6 @@ <string name="pref_gpodnet_setlogin_information_sum">Modifier les information de connexion pour votre compte gpodder.net</string> <string name="pref_playback_speed_title">Vitesses de lecture</string> <string name="pref_playback_speed_sum">Modifier la liste des vitesses disponibles pour la lecture audio</string> - <string name="pref_seek_delta_title">Chercher un moment spécifique</string> - <string name="pref_seek_delta_sum">Bouger d\'autant de secondes en rembobinant ou en faisant une avance rapide </string> <string name="pref_gpodnet_sethostname_title">Choisir un nom de domaine</string> <string name="pref_gpodnet_sethostname_use_default_host">Utiliser le nom de domaine par défaut</string> <string name="pref_expandNotify_title">Etendre la notification</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 69bab7326..b81e3f2ce 100644 --- a/core/src/main/res/values-it-rIT/strings.xml +++ b/core/src/main/res/values-it-rIT/strings.xml @@ -39,6 +39,7 @@ <string name="cancel_label">Annulla</string> <string name="author_label">Autore</string> <string name="language_label">Lingua</string> + <string name="url_label">URL</string> <string name="podcast_settings_label">Impostazioni</string> <string name="cover_label">Immagine</string> <string name="error_label">Errore</string> @@ -58,6 +59,7 @@ <string name="close_label">Chiudi</string> <string name="retry_label">Riprova</string> <string name="auto_download_label">Includi nei download automatici</string> + <string name="parallel_downloads_suffix">\u0020download paralleli</string> <!--'Add Feed' Activity labels--> <string name="feedurl_label">URL del feed</string> <string name="etxtFeedurlHint">www.example.com/feed</string> @@ -66,8 +68,8 @@ <string name="podcastdirectories_descr">Puoi cercare dei nuovi podcast in base al nome, alla categoria o alla popolarità nella directory di gpodder.net.</string> <string name="browse_gpoddernet_label">Esplora gpodder.net</string> <!--Actions on feeds--> - <string name="mark_all_read_label">Segna tutti come letti</string> - <string name="mark_all_read_msg">Segnati tutti gli episodi come letti</string> + <string name="mark_all_read_label">Segna tutti come riprodotti</string> + <string name="mark_all_read_msg">Segnati tutti gli episodi come riprodotti</string> <string name="show_info_label">Informazioni</string> <string name="remove_feed_label">Rimuovi un podcast</string> <string name="share_link_label">Condividi il link al sito</string> @@ -75,6 +77,16 @@ <string name="feed_delete_confirmation_msg">Per favore conferma la cancellazione di questo feed e di TUTTI gli episodi collegati che sono stati precedentemente scaricati.</string> <string name="feed_remover_msg">Rimozione feed</string> <string name="load_complete_feed">Ricarica il feed completo</string> + <string name="hide_episodes_title">Nascondi gli episodi</string> + <string name="hide_unplayed_episodes_label">Non riprodotti</string> + <string name="hide_paused_episodes_label">In pausa</string> + <string name="hide_played_episodes_label">Riprodotti</string> + <string name="hide_queued_episodes_label">In coda</string> + <string name="hide_not_queued_episodes_label">Non in coda</string> + <string name="hide_downloaded_episodes_label">Scaricati</string> + <string name="hide_not_downloaded_episodes_label">Non scaricati</string> + <string name="filtered_label">Filtrati</string> + <string name="refresh_failed_msg">{fa-exclamation-circle} Ultimo aggiornamento fallito</string> <!--actions on feeditems--> <string name="download_label">Download</string> <string name="play_label">Riproduci</string> @@ -83,16 +95,20 @@ <string name="stream_label">Stream</string> <string name="remove_label">Rimuovi</string> <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="mark_read_label">Segna come riprodotto</string> + <string name="mark_unread_label">Segna come non riprodotto</string> + <string name="marked_as_read_label">Segnato come riprodotto</string> <string name="add_to_queue_label">Aggiungi alla coda</string> + <string name="added_to_queue_label">Aggiunto alla coda</string> <string name="remove_from_queue_label">Rimuovi dalla coda</string> <string name="visit_website_label">Visita il sito</string> <string name="support_label">Carica questo su Flattr</string> <string name="enqueue_all_new">Accoda tutti</string> <string name="download_all">Scarica tutti</string> <string name="skip_episode_label">Salta l\'episodio</string> + <string name="activate_auto_download">Attiva il download automatico</string> + <string name="deactivate_auto_download">Disattiva il download automatico</string> + <string name="reset_position">Azzera la posizione della riproduzione</string> <!--Download messages and labels--> <string name="download_successful">successo</string> <string name="download_failed">fallito</string> @@ -109,8 +125,8 @@ <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_report_title">Download completati</string> + <string name="download_canceled_msg">Download annullato</string> + <string name="download_report_content_title">Rapporto del downoad</string> <string name="download_error_malformed_url">URL malformato</string> <string name="download_error_io_error">Errore IO</string> <string name="download_error_request_error">Errore della richiesta</string> @@ -126,6 +142,9 @@ <string name="download_request_error_dialog_message_prefix">Rilevato errore durante il download del file:\u0020</string> <string name="authentication_notification_title">Autenticazione richiesta</string> <string name="authentication_notification_msg">La risorsa che hai richiesto richiede un nome utente e una password</string> + <string name="confirm_mobile_download_dialog_title">Conferma il download su cellulare</string> + <string name="confirm_mobile_download_dialog_only_add_to_queue">Aggiungi solo alla coda</string> + <string name="confirm_mobile_download_dialog_enable_temporarily">Abilita temporaneamente</string> <!--Mediaplayer messages--> <string name="player_error_msg">Errore!</string> <string name="player_stopped_msg">Nessun media in riproduzione</string> @@ -140,6 +159,8 @@ <string name="playbackservice_notification_title">Riproduzione del podcast in corso</string> <string name="unknown_media_key">AntennaPod - Chiave dell\'elemento multimediale sconosciuta: %1$d</string> <!--Queue operations--> + <string name="lock_queue">Blocca la coda</string> + <string name="unlock_queue">Sblocca la coda</string> <string name="clear_queue_label">Svuota la coda</string> <string name="undo">Undo</string> <string name="removed_from_queue">Oggetto rimosso</string> @@ -242,8 +263,6 @@ <string name="pref_gpodnet_setlogin_information_sum">Cambia le informazioni di login per il tuo account gpodder.net.</string> <string name="pref_playback_speed_title">Velocità di riproduzione</string> <string name="pref_playback_speed_sum">Personalizza le velocità disponibili per la riproduzione audio a velocità variabile</string> - <string name="pref_seek_delta_title">Tempo di ricerca</string> - <string name="pref_seek_delta_sum">Cerca tutti questi secondi quando si riavvolge o si va avanti velocemente</string> <string name="pref_gpodnet_sethostname_title">Imposta l\'hostname</string> <string name="pref_gpodnet_sethostname_use_default_host">Usa l\'host di default</string> <string name="pref_expandNotify_title">Espandi le notifiche</string> @@ -251,6 +270,7 @@ <string name="pref_persistNotify_title">Controlli di riproduzione persistenti</string> <string name="pref_persistNotify_sum">Mantieni le notifiche e i controlli del blocco dello schermo quando la riproduzione è in pausa.</string> <string name="pref_expand_notify_unsupport_toast">Le versioni di Android prima della 4.1 non supportano le notifiche estese.</string> + <string name="pref_smart_mark_as_played_disabled">Disabilitato</string> <!--Auto-Flattr dialog--> <string name="auto_flattr_enable">Abilita l\'esecuzione automatica di Flattr</string> <string name="auto_flattr_after_percent">Carica l\'episodio su Flattr appena è stato riprodotto al %d percento</string> @@ -273,6 +293,8 @@ <string name="opml_import_error_dir_empty">La directory di importazione è vuota.</string> <string name="select_all_label">Seleziona tutti</string> <string name="deselect_all_label">Deseleziona tutti</string> + <string name="choose_file_from_filesystem">Dal filesystem locale</string> + <string name="choose_file_from_external_application">Utilizza un\'applicazione esterna</string> <string name="opml_export_label">Esportazione su OPML</string> <string name="exporting_label">Esportazione in corso...</string> <string name="export_error_label">Errore di esportazione</string> @@ -330,6 +352,7 @@ <string name="set_to_default_folder">Scegli la cartella predefinita</string> <string name="pref_pausePlaybackForFocusLoss_sum">Sospendi la riproduzione invece di abbassare il volume quando un\'altra app emette un suono</string> <string name="pref_pausePlaybackForFocusLoss_title">Pausa su interruzione</string> + <string name="pref_resumeAfterCall_title">Riprendi dopo la chiamata</string> <!--Online feed view--> <string name="subscribe_label">Abbonati</string> <string name="subscribed_label">Abbonato</string> diff --git a/core/src/main/res/values-iw-rIL/strings.xml b/core/src/main/res/values-iw-rIL/strings.xml index b52bb4144..9e9c0e6bc 100644 --- a/core/src/main/res/values-iw-rIL/strings.xml +++ b/core/src/main/res/values-iw-rIL/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">כתובת אתר שגויה</string> <string name="download_error_io_error">שגיאת קלט פלט</string> diff --git a/core/src/main/res/values-ja/strings.xml b/core/src/main/res/values-ja/strings.xml index df73db23e..79411ffc1 100644 --- a/core/src/main/res/values-ja/strings.xml +++ b/core/src/main/res/values-ja/strings.xml @@ -26,6 +26,7 @@ <!--Main activity--> <string name="drawer_open">メニューを開く</string> <string name="drawer_close">メニューを閉じる</string> + <string name="drawer_preferences">ドロワー設定</string> <!--Webview actions--> <string name="open_in_browser_label">ブラウザーで開く</string> <string name="copy_url_label">URLをコピー</string> @@ -39,6 +40,7 @@ <string name="cancel_label">キャンセル</string> <string name="author_label">作者</string> <string name="language_label">言語</string> + <string name="url_label">URL</string> <string name="podcast_settings_label">設定</string> <string name="cover_label">映像</string> <string name="error_label">エラー</string> @@ -67,10 +69,10 @@ <string name="podcastdirectories_descr">名前、カテゴリや人気で、gpodder.netディレクトリ内の新しいポッドキャストを検索することができます。</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="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">Webサイトのリンクを共有</string> @@ -78,6 +80,16 @@ <string name="feed_delete_confirmation_msg">このフィードと、このフィードのダウンロードしたすべてのエピソードを削除することを確認してください。</string> <string name="feed_remover_msg">フィードの削除中</string> <string name="load_complete_feed">フィードをすべて更新</string> + <string name="hide_episodes_title">エピソードを非表示にする</string> + <string name="hide_unplayed_episodes_label">未再生</string> + <string name="hide_paused_episodes_label">一時停止しました</string> + <string name="hide_played_episodes_label">再生しました</string> + <string name="hide_queued_episodes_label">キューに入れました</string> + <string name="hide_not_queued_episodes_label">キューに入っていません</string> + <string name="hide_downloaded_episodes_label">ダウンロードしました</string> + <string name="hide_not_downloaded_episodes_label">ダウンロードしていません</string> + <string name="filtered_label">フィルターしました</string> + <string name="refresh_failed_msg">{fa-exclamation-circle} 前回更新に失敗しました</string> <!--actions on feeditems--> <string name="download_label">ダウンロード</string> <string name="play_label">再生</string> @@ -86,16 +98,20 @@ <string name="stream_label">ストリーム</string> <string name="remove_label">削除</string> <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="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="added_to_queue_label">キューに追加しました</string> <string name="remove_from_queue_label">キューから削除</string> <string name="visit_website_label">Webサイトを訪問</string> <string name="support_label">これをFlattr</string> <string name="enqueue_all_new">すべてキューに入れる</string> <string name="download_all">すべてダウンロード</string> <string name="skip_episode_label">エピソードをスキップ</string> + <string name="activate_auto_download">自動ダウンロードを有効にする</string> + <string name="deactivate_auto_download">自動ダウンロードを無効にする</string> + <string name="reset_position">再生位置をリセット</string> <!--Download messages and labels--> <string name="download_successful">完成</string> <string name="download_failed">失敗</string> @@ -112,8 +128,10 @@ <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_report_title">ダウンロードが完了しました</string> + <string name="download_canceled_msg">ダウンロードをキャンセルしました</string> + <string name="download_canceled_autodownload_enabled_msg">ダウンロードをキャンセルしました\nこのアイテムの <i>自動ダウンロード</i> を無効にしました</string> + <string name="download_report_title">ダウンロードがエラーで完了しました</string> + <string name="download_report_content_title">ダウンロード レポート</string> <string name="download_error_malformed_url">不正な形式のURL</string> <string name="download_error_io_error">IOエラー</string> <string name="download_error_request_error">リクエストエラー</string> @@ -129,6 +147,11 @@ <string name="download_request_error_dialog_message_prefix">ファイルのダウンロード中にエラーが発生しました:\u0020</string> <string name="authentication_notification_title">認証が必要です</string> <string name="authentication_notification_msg">リクエストしたリソースは、ユーザー名とパスワードが必要です</string> + <string name="confirm_mobile_download_dialog_title">モバイルダウンロードの確認</string> + <string name="confirm_mobile_download_dialog_message_not_in_queue">モバイルデータ接続でのダウンロードは設定で無効になっています。\n\n一時的に有効にするか、またはキューに追加するだけにしますか?\n\n<small>選択は 10 分間記憶されます。</small></string> + <string name="confirm_mobile_download_dialog_message">モバイルデータ接続でのダウンロードは設定で無効になっています。\n\n一時的に有効にしますか?\n\n<small>選択は 10 分間記憶されます。</small></string> + <string name="confirm_mobile_download_dialog_only_add_to_queue">キューに追加するだけ</string> + <string name="confirm_mobile_download_dialog_enable_temporarily">一時的に有効にする</string> <!--Mediaplayer messages--> <string name="player_error_msg">エラー!</string> <string name="player_stopped_msg">再生するメディアがありません</string> @@ -143,6 +166,8 @@ <string name="playbackservice_notification_title">ポッドキャストを再生中</string> <string name="unknown_media_key">AntennaPod - 不明なメディアキー: %1$d</string> <!--Queue operations--> + <string name="lock_queue">キューをロック</string> + <string name="unlock_queue">キューのロックを解除</string> <string name="clear_queue_label">キューをクリア</string> <string name="undo">元に戻す</string> <string name="removed_from_queue">アイテムを削除しました</string> @@ -200,6 +225,8 @@ <string name="pref_followQueue_sum">再生が完了した時に次のキューのアイテムに移動します</string> <string name="pref_auto_delete_sum">再生が完了した時にエピソードを削除します</string> <string name="pref_auto_delete_title">自動削除</string> + <string name="pref_smart_mark_as_played_sum">一定の秒数未満の再生時間がまだ残っている場合でも、エピソードを再生済としてマークします</string> + <string name="pref_smart_mark_as_played_title">再生済としてスマートマーク</string> <string name="playback_pref">再生</string> <string name="network_pref">ネットワーク</string> <string name="pref_autoUpdateIntervall_title">更新間隔</string> @@ -223,6 +250,8 @@ <string name="pref_auto_flattr_sum">自動Flattrを構成</string> <string name="user_interface_label">インターフェース</string> <string name="pref_set_theme_title">テーマを選択</string> + <string name="pref_nav_drawer_items_title">ナビゲーションドロワーを変更</string> + <string name="pref_nav_drawer_items_sum">ナビゲーションドロワーに表示するアイテムを変更します。</string> <string name="pref_set_theme_sum">AntennaPodの外観を変更します。</string> <string name="pref_automatic_download_title">自動ダウンロード</string> <string name="pref_automatic_download_sum">エピソードの自動ダウンロードを構成します。</string> @@ -246,8 +275,8 @@ <string name="pref_gpodnet_setlogin_information_sum">gpodder.netアカウントのログイン情報を変更します。</string> <string name="pref_playback_speed_title">再生速度</string> <string name="pref_playback_speed_sum">可変速度音声再生に使用可能な速度をカスタマイズします</string> - <string name="pref_seek_delta_title">シーク時間</string> - <string name="pref_seek_delta_sum">巻き戻しまたは早送り時にこの秒数でシークします</string> + <string name="pref_fast_forward">早送り時間</string> + <string name="pref_rewind">巻き戻し時間</string> <string name="pref_gpodnet_sethostname_title">ホスト名をセット</string> <string name="pref_gpodnet_sethostname_use_default_host">デフォルトホストを使用</string> <string name="pref_expandNotify_title">拡張通知</string> @@ -257,6 +286,7 @@ <string name="pref_expand_notify_unsupport_toast">Androidバージョン4.1以前では、拡張通知をサポートしていません。</string> <string name="pref_queueAddToFront_sum">新しいエピソードをキューの先頭に追加します。</string> <string name="pref_queueAddToFront_title">キューの先頭に入れる</string> + <string name="pref_smart_mark_as_played_disabled">無効</string> <!--Auto-Flattr dialog--> <string name="auto_flattr_enable">自動Flattrを有効にする</string> <string name="auto_flattr_after_percent">%d %再生したらエピソードをFlattr </string> @@ -341,6 +371,8 @@ <string name="set_to_default_folder">デフォルトのフォルダーを選択</string> <string name="pref_pausePlaybackForFocusLoss_sum">他のアプリがサウンドを再生したい時に、音量を下げる代わりに再生の一時停止します</string> <string name="pref_pausePlaybackForFocusLoss_title">割り込み時に一時停止</string> + <string name="pref_resumeAfterCall_sum">着信が完了した後に再生を再開します</string> + <string name="pref_resumeAfterCall_title">着信後に再開</string> <!--Online feed view--> <string name="subscribe_label">購読</string> <string name="subscribed_label">購読しました</string> diff --git a/core/src/main/res/values-ko/strings.xml b/core/src/main/res/values-ko/strings.xml index 7132031e4..148010050 100644 --- a/core/src/main/res/values-ko/strings.xml +++ b/core/src/main/res/values-ko/strings.xml @@ -26,6 +26,7 @@ <!--Main activity--> <string name="drawer_open">메뉴 열기</string> <string name="drawer_close">메뉴 닫기</string> + <string name="drawer_preferences">드로어 기본 설정</string> <!--Webview actions--> <string name="open_in_browser_label">브라우저에서 열기</string> <string name="copy_url_label">URL 복사</string> @@ -39,6 +40,7 @@ <string name="cancel_label">취소</string> <string name="author_label">저자</string> <string name="language_label">언어</string> + <string name="url_label">URL</string> <string name="podcast_settings_label">설정</string> <string name="cover_label">그림</string> <string name="error_label">오류</string> @@ -67,10 +69,10 @@ <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="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> @@ -78,6 +80,16 @@ <string name="feed_delete_confirmation_msg">이 피드와 이 피드에서 다운로드한 모든 에피소드를 삭제하시려면 확인을 누르십시오.</string> <string name="feed_remover_msg">피드 삭제하는 중</string> <string name="load_complete_feed">전체 피드 새로고침</string> + <string name="hide_episodes_title">에피소드 감추기</string> + <string name="hide_unplayed_episodes_label">재생 안 함</string> + <string name="hide_paused_episodes_label">일시 중지</string> + <string name="hide_played_episodes_label">재생함</string> + <string name="hide_queued_episodes_label">대기열 추가</string> + <string name="hide_not_queued_episodes_label">대기열 추가 안 함</string> + <string name="hide_downloaded_episodes_label">다운로드함</string> + <string name="hide_not_downloaded_episodes_label">다운로드 안 함</string> + <string name="filtered_label">필터링함</string> + <string name="refresh_failed_msg">{fa-exclamation-circle} 최근 새로 고침 실패</string> <!--actions on feeditems--> <string name="download_label">다운로드</string> <string name="play_label">재생</string> @@ -86,16 +98,20 @@ <string name="stream_label">스트리밍</string> <string name="remove_label">제거</string> <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="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="added_to_queue_label">대기열에 추가함</string> <string name="remove_from_queue_label">대기열에서 제거</string> <string name="visit_website_label">홈페이지 보기</string> <string name="support_label">Flattr하기</string> <string name="enqueue_all_new">모두 대기열에 추가</string> <string name="download_all">모두 다운로드</string> <string name="skip_episode_label">에피소드 건너뛰기</string> + <string name="activate_auto_download">자동 다운로드 활성화</string> + <string name="deactivate_auto_download">자동 다운로드 해제</string> + <string name="reset_position">재생 위치 초기화</string> <!--Download messages and labels--> <string name="download_successful">성공</string> <string name="download_failed">실패</string> @@ -112,8 +128,10 @@ <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_report_title">다운로드 마침</string> + <string name="download_canceled_msg">다운로드 취소함</string> + <string name="download_canceled_autodownload_enabled_msg">다운로드 취소함\n이 항목에 <i>자동 다운로드</i>를 해제합니다</string> + <string name="download_report_title">다운로드 마침 (오류 있음)</string> + <string name="download_report_content_title">다운로드 보고서</string> <string name="download_error_malformed_url">URL 형식 틀림</string> <string name="download_error_io_error">입출력 오류</string> <string name="download_error_request_error">요청 오류</string> @@ -129,6 +147,11 @@ <string name="download_request_error_dialog_message_prefix">파일을 다운로드하는 중 오류가 발생했습니다:\u0020</string> <string name="authentication_notification_title">인증이 필요합니다</string> <string name="authentication_notification_msg">요청한 자원은 사용자 이름과 암호가 필요합니다</string> + <string name="confirm_mobile_download_dialog_title">휴대전화망 다운로드 확인</string> + <string name="confirm_mobile_download_dialog_message_not_in_queue">휴대전화망 데이터 연결을 통한 다운로드는 설정에서 막혀 있습니다.\n\n임시로 다운로드를 허용할 수도 있고, 대기열에 추가만 할 수도 있습니다.\n\n<small>여기서 선택한 사항은 10분 동안 유지됩니다.</small></string> + <string name="confirm_mobile_download_dialog_message">휴대전화망 데이터 연결을 통한 다운로드는 설정에서 막혀 있습니다.\n\n임시로 다운로드를 열 수 있습니다\n\n<small>여기서 선택한 사항은 10분 동안 유지됩니다.</small></string> + <string name="confirm_mobile_download_dialog_only_add_to_queue">대기열에 추가만</string> + <string name="confirm_mobile_download_dialog_enable_temporarily">임시로 허용</string> <!--Mediaplayer messages--> <string name="player_error_msg">오류!</string> <string name="player_stopped_msg">재생 중인 미디어 없음</string> @@ -143,6 +166,8 @@ <string name="playbackservice_notification_title">팟캐스트 재생 중</string> <string name="unknown_media_key">안테나팟 - 알 수 없는 미디어 키: %1$d</string> <!--Queue operations--> + <string name="lock_queue">대기열 잠그기</string> + <string name="unlock_queue">대기열 잠금 해제</string> <string name="clear_queue_label">대기열 지우기</string> <string name="undo">실행 취소</string> <string name="removed_from_queue">항목을 제거했습니다</string> @@ -200,6 +225,8 @@ <string name="pref_followQueue_sum">재생을 마쳤을 때 다음 대기열로 이동</string> <string name="pref_auto_delete_sum">재생이 끝나면 에피소드 삭제</string> <string name="pref_auto_delete_title">자동 삭제</string> + <string name="pref_smart_mark_as_played_sum">재생이 일정한 시간보다 (초 단위) 적게 남으면 에피소드를 재생한 것으로 표시</string> + <string name="pref_smart_mark_as_played_title">똑똑하게 재생한 것으로 표시</string> <string name="playback_pref">재생</string> <string name="network_pref">네트워크</string> <string name="pref_autoUpdateIntervall_title">업데이트 주기</string> @@ -223,6 +250,8 @@ <string name="pref_auto_flattr_sum">자동 flattr 설정</string> <string name="user_interface_label">사용자 인터페이스</string> <string name="pref_set_theme_title">테마 선택</string> + <string name="pref_nav_drawer_items_title">네비게이션 드로어 바꾸기</string> + <string name="pref_nav_drawer_items_sum">네비게이션 드로어에 어떤 항목을 표시할지 바꿉니다.</string> <string name="pref_set_theme_sum">안테나팟의 겉모양을 바꿉니다.</string> <string name="pref_automatic_download_title">자동 다운로드</string> <string name="pref_automatic_download_sum">에피소드 자동 다운로드를 설정합니다.</string> @@ -246,8 +275,8 @@ <string name="pref_gpodnet_setlogin_information_sum">gpodder.net 계정의 로그인 정보를 바꿉니다.</string> <string name="pref_playback_speed_title">재생 속도</string> <string name="pref_playback_speed_sum">여러가지 오디오 재생 속도 직접 설정</string> - <string name="pref_seek_delta_title">넘기기 간격</string> - <string name="pref_seek_delta_sum">뒤나 앞으로 이동할 때 몇 초를 넘어갈지 지정합니다</string> + <string name="pref_fast_forward">빠르게 감기 시간</string> + <string name="pref_rewind">뒤로 감기 시간</string> <string name="pref_gpodnet_sethostname_title">호스트 이름 설정</string> <string name="pref_gpodnet_sethostname_use_default_host">기본 호스트 사용</string> <string name="pref_expandNotify_title">알림 확장</string> @@ -257,6 +286,7 @@ <string name="pref_expand_notify_unsupport_toast">안드로이드 4.1 전 버전에서는 알림 확장을 지원하지 않습니다.</string> <string name="pref_queueAddToFront_sum">새 에피소드를 대기열 앞에 추가합니다.</string> <string name="pref_queueAddToFront_title">대기열 앞에 추가</string> + <string name="pref_smart_mark_as_played_disabled">사용 안 함</string> <!--Auto-Flattr dialog--> <string name="auto_flattr_enable">자동 flattr 사용</string> <string name="auto_flattr_after_percent">%d 퍼센트를 재생하면 에피소드에 flattr합니다</string> @@ -341,6 +371,8 @@ <string name="set_to_default_folder">기본 폴더 선택</string> <string name="pref_pausePlaybackForFocusLoss_sum">다른 앱이 소리를 낼 때 볼륨을 줄이지 않고 재생을 일시 중지</string> <string name="pref_pausePlaybackForFocusLoss_title">끼어들면 일시 중지</string> + <string name="pref_resumeAfterCall_sum">전화 통화가 끝난 후에 재생 다시 시작</string> + <string name="pref_resumeAfterCall_title">통화 후에 다시 시작</string> <!--Online feed view--> <string name="subscribe_label">구독</string> <string name="subscribed_label">구독함</string> 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..abbf97de6 100644 --- a/core/src/main/res/values-pt/strings.xml +++ b/core/src/main/res/values-pt/strings.xml @@ -19,18 +19,19 @@ <string name="cancel_download_label">Cancelar transferência</string> <string name="playback_history_label">Histórico de reprodução</string> <string name="gpodnet_main_label">gpodder.net</string> - <string name="gpodnet_auth_label">Acesso gpodder.net</string> + <string name="gpodnet_auth_label">Dados gpodder.net</string> <!--New episodes fragment--> <string name="recently_published_episodes_label">Publicados recentemente</string> <string name="episode_filter_label">Mostrar apenas novos episódios</string> <!--Main activity--> <string name="drawer_open">Abrir menu</string> <string name="drawer_close">Fechar menu</string> + <string name="drawer_preferences">Preferências do menu</string> <!--Webview actions--> <string name="open_in_browser_label">Abrir no navegador</string> <string name="copy_url_label">Copiar URL</string> <string name="share_url_label">Partilhar URL</string> - <string name="copied_url_msg">URL copiado para a área de transferência.</string> + <string name="copied_url_msg">URL copiado para a área de transferência</string> <string name="go_to_position_label">Ir para esta posição</string> <!--Playback history--> <string name="clear_history_label">Limpar histórico</string> @@ -39,6 +40,7 @@ <string name="cancel_label">Cancelar</string> <string name="author_label">Autor</string> <string name="language_label">Idioma</string> + <string name="url_label">URL</string> <string name="podcast_settings_label">Definições</string> <string name="cover_label">Imagem</string> <string name="error_label">Erro</string> @@ -54,7 +56,7 @@ <string name="size_prefix">Tamanho:\u0020</string> <string name="processing_label">A processar...</string> <string name="loading_label">A carregar...</string> - <string name="save_username_password_label">Gravar utilizador e senha</string> + <string name="save_username_password_label">Guardar utilizador e palavra-passe</string> <string name="close_label">Fechar</string> <string name="retry_label">Tentar novamente</string> <string name="auto_download_label">Incluir nas transferências automáticas</string> @@ -64,20 +66,30 @@ <string name="etxtFeedurlHint">URL da fonte ou sítio web</string> <string name="txtvfeedurl_label">Adicionar podcast via URL</string> <string name="podcastdirectories_label">Localizar podcasts no diretório</string> - <string name="podcastdirectories_descr">Pode procurar os novos podcasts no gPodder.net por nome, categoria ou popularidade e também na loja iTunes.</string> + <string name="podcastdirectories_descr">Pode procurar por novos podcasts no gPodder.net e também na loja iTunes. Pode procurar por nome, categoria e popularidade.</string> <string name="browse_gpoddernet_label">Procurar no gPodder.net</string> <!--Actions on feeds--> - <string name="mark_all_read_label">Marcar tudo como lido</string> - <string name="mark_all_read_msg">Marcar todos os episódios como lidos</string> - <string name="mark_all_read_confirmation_msg">Por favor confirme que deseja marcar todos os episódios como lidos.</string> - <string name="mark_all_read_feed_confirmation_msg">Por favor confirme que deseja marcar todos os episódios desta fonte como lidos.</string> + <string name="mark_all_read_label">Marcar tudo como reproduzido</string> + <string name="mark_all_read_msg">Marcar todos os episódios como reproduzidos</string> + <string name="mark_all_read_confirmation_msg">Por favor confirme que deseja marcar todos os episódios como reproduzidos</string> + <string name="mark_all_read_feed_confirmation_msg">Por favor confirme que deseja marcar todos os episódios desta fonte como reproduzidos</string> <string name="show_info_label">Mostrar informações</string> <string name="remove_feed_label">Remover podcast</string> <string name="share_link_label">Partilhar ligação do sítio web</string> <string name="share_source_label">Partilhar ligação da fonte</string> - <string name="feed_delete_confirmation_msg">Confirme a eliminação desta fonte e de todos os episódios a ela petencentes.</string> + <string name="feed_delete_confirmation_msg">Confirme a eliminação desta fonte e de todos os episódios a ela pertencentes</string> <string name="feed_remover_msg">Remover fonte</string> <string name="load_complete_feed">Atualizar todas as páginas da fonte</string> + <string name="hide_episodes_title">Ocultar episódios</string> + <string name="hide_unplayed_episodes_label">Não reproduzidos</string> + <string name="hide_paused_episodes_label">Em pausa</string> + <string name="hide_played_episodes_label">Reproduzidos</string> + <string name="hide_queued_episodes_label">Na fila</string> + <string name="hide_not_queued_episodes_label">Não na fila</string> + <string name="hide_downloaded_episodes_label">Transferidos</string> + <string name="hide_not_downloaded_episodes_label">Não transferidos</string> + <string name="filtered_label">Filtrados</string> + <string name="refresh_failed_msg">{fa-exclamation-circle} Última atualização falhada</string> <!--actions on feeditems--> <string name="download_label">Transferir</string> <string name="play_label">Reproduzir</string> @@ -86,16 +98,20 @@ <string name="stream_label">Emitir</string> <string name="remove_label">Remover</string> <string name="remove_episode_lable">Remover episódio</string> - <string name="mark_read_label">Marcar como lido</string> - <string name="mark_unread_label">Marcar como novo</string> - <string name="marked_as_read_label">Marcar como lido</string> + <string name="mark_read_label">Marcar como reproduzido</string> + <string name="mark_unread_label">Marcar como não reproduzido</string> + <string name="marked_as_read_label">Marcado como reproduzido</string> <string name="add_to_queue_label">Adicionar à fila</string> + <string name="added_to_queue_label">Adicionado à fila</string> <string name="remove_from_queue_label">Remover da fila</string> <string name="visit_website_label">Aceder ao sítio web</string> <string name="support_label">Flattr</string> <string name="enqueue_all_new">Colocar tudo na fila</string> <string name="download_all">Transferir tudo</string> <string name="skip_episode_label">Ignorar episódio</string> + <string name="activate_auto_download">Ativar transferência automática</string> + <string name="deactivate_auto_download">Desativar transferência automática</string> + <string name="reset_position">Repor posição de reprodução</string> <!--Download messages and labels--> <string name="download_successful">sucesso</string> <string name="download_failed">falha</string> @@ -112,8 +128,10 @@ <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_report_title">Transferências terminadas</string> + <string name="download_canceled_msg">Transferência cancelada</string> + <string name="download_canceled_autodownload_enabled_msg">Transferência cancelada\n<i>Transferência automática</i> desativada para este item</string> + <string name="download_report_title">Transferências terminadas com erros</string> + <string name="download_report_content_title">Relatório de transferências</string> <string name="download_error_malformed_url">URL inválido</string> <string name="download_error_io_error">Erro I/O</string> <string name="download_error_request_error">Erro de pedido</string> @@ -128,13 +146,18 @@ <string name="download_type_image">Imagem</string> <string name="download_request_error_dialog_message_prefix">Ocorreu um erro ao transferir o ficheiro:\u0020</string> <string name="authentication_notification_title">Requer autenticação</string> - <string name="authentication_notification_msg">O recurso solicitado requer um utilizador e uma senha</string> + <string name="authentication_notification_msg">O recurso solicitado requer um utilizador e uma palavra-passe</string> + <string name="confirm_mobile_download_dialog_title">Confirmação de transferência</string> + <string name="confirm_mobile_download_dialog_message_not_in_queue">A transferência através de dados móveis está desativada nas definições.\n\nAtivar temporariamente ou apenas adicionar à fila?\n\n<small>A sua decisão será memorizada durante 10 minutos.</small></string> + <string name="confirm_mobile_download_dialog_message">A transferência através de dados móveis está desativada nas definições.\n\nAtivar temporariamente?\n\n<small>A sua decisão será memorizada durante 10 minutos.</small></string> + <string name="confirm_mobile_download_dialog_only_add_to_queue">Apenas adicionados à fila</string> + <string name="confirm_mobile_download_dialog_enable_temporarily">Ativar temporariamente</string> <!--Mediaplayer messages--> <string name="player_error_msg">Erro!</string> <string name="player_stopped_msg">Nada em reprodução</string> - <string name="player_preparing_msg">A preparar</string> + <string name="player_preparing_msg">A preparar...</string> <string name="player_ready_msg">Pronto</string> - <string name="player_seeking_msg">A procurar</string> + <string name="player_seeking_msg">A procurar...</string> <string name="playback_error_server_died">Erro de servidor</string> <string name="playback_error_unknown">Erro desconhecido</string> <string name="no_media_playing_label">Nada em reprodução</string> @@ -143,6 +166,8 @@ <string name="playbackservice_notification_title">Reproduzir podcast</string> <string name="unknown_media_key">Tecla multimédia desconhecida: %1$d</string> <!--Queue operations--> + <string name="lock_queue">Bloquear fila</string> + <string name="unlock_queue">Desbloquear fila</string> <string name="clear_queue_label">Limpar fila</string> <string name="undo">Anular</string> <string name="removed_from_queue">Item removido</string> @@ -154,7 +179,7 @@ <string name="duration">Duração</string> <string name="ascending">Crescente</string> <string name="descending">Decrescente</string> - <string name="clear_queue_confirmation_msg">Por favor confirme que deseja limpar todos os episódios da fila de reprodução.</string> + <string name="clear_queue_confirmation_msg">Tem a certeza de que deseja remover todos os episódios da lista de reprodução?</string> <!--Flattr--> <string name="flattr_auth_label">Sessão Flattr</string> <string name="flattr_auth_explanation">Prima o botão abaixo para iniciar a autenticação. O seu navegador web abrirá o ecrã da sessão flattr e ser-lhe-á solicitada a permissão para o AntennaPod efetuar as alterações. Após ser dada a permissão, voltará novamente a este ecrã.</string> @@ -165,7 +190,7 @@ <string name="no_flattr_token_notification_msg">Parece que a sua conta flattr não está integrada ao AntennaPod. Clique aqui para autenticar.</string> <string name="no_flattr_token_msg">Parece que a sua conta flattr não está vinculada ao AntennaPod. Pode vincular a sua conta ao AntennaPod ou aceder ao sítio web para fazer o flattr.</string> <string name="authenticate_now_label">Autenticar</string> - <string name="action_forbidden_title">Ação negada</string> + <string name="action_forbidden_title">Ação proibida</string> <string name="action_forbidden_msg">O AntennaPod não possui as permissões para esta ação. É possível que o token de acesso ao flattr via AntennaPod tenha sido revogado. Pode efetuar nova autenticação ou aceder ao sítio web do item.</string> <string name="access_revoked_title">Acesso revogado</string> <string name="access_revoked_info">Você revogou o token de acesso do AntennaPod à sua conta. Para concluir o processo, tem que remover esta aplicação da lista de aplicações presentes nas definições de conta no sítio web do flattr.</string> @@ -187,19 +212,21 @@ <string name="no_playback_plugin_msg">Para que a velocidade de reprodução variável funcione, tem que instalar um biblioteca de terceiros.\n\nClique em Transferir extra para a transferir no Google Play.\n\nQuaisquer problemas que ocorram na utilização do extra devem ser reportados diretamente ao seu programador.</string> <string name="set_playback_speed_label">Velocidades de reprodução</string> <!--Empty list labels--> - <string name="no_items_label">Não existem itens na lista.</string> - <string name="no_feeds_label">Ainda não possui quaisquer fontes.</string> + <string name="no_items_label">Não existem itens nesta lista</string> + <string name="no_feeds_label">Ainda não possui quaisquer fontes</string> <!--Preferences--> <string name="other_pref">Outras</string> <string name="about_pref">Sobre</string> <string name="queue_label">Fila</string> <string name="services_label">Serviços</string> <string name="flattr_label">Flattr</string> - <string name="pref_pauseOnHeadsetDisconnect_sum">Parar reprodução ao remover os auscultadores</string> + <string name="pref_pauseOnHeadsetDisconnect_sum">Pausa na reprodução ao remover os auscultadores</string> <string name="pref_unpauseOnHeadsetReconnect_sum">Continuar reprodução ao ligar os auscultadores</string> <string name="pref_followQueue_sum">Ir para a faixa seguinte ao terminar a reprodução</string> <string name="pref_auto_delete_sum">Eliminar episódio ao terminar a reprodução</string> <string name="pref_auto_delete_title">Eliminação automática</string> + <string name="pref_smart_mark_as_played_sum">Marcar episódios como reproduzidos mesmo que restem alguns segundos de reprodução</string> + <string name="pref_smart_mark_as_played_title">Marcar como reproduzido (inteligente)</string> <string name="playback_pref">Reprodução</string> <string name="network_pref">Rede</string> <string name="pref_autoUpdateIntervall_title">Intervalo entre atualizações</string> @@ -210,24 +237,26 @@ <string name="pref_pauseOnHeadsetDisconnect_title">Auscultadores removidos</string> <string name="pref_unpauseOnHeadsetReconnect_title">Auscultadores ligados</string> <string name="pref_mobileUpdate_title">Atualizações móveis</string> - <string name="pref_mobileUpdate_sum">Permitir atualizações através da rede de dados</string> + <string name="pref_mobileUpdate_sum">Permitir atualizações através da rede de dados móveis</string> <string name="refreshing_label">A atualizar</string> <string name="flattr_settings_label">Definições flattr</string> <string name="pref_flattr_auth_title">Sessão flattr</string> - <string name="pref_flattr_auth_sum">Inicie sessão na sua conta flattr para fazer o flattr no AntennaPod.</string> + <string name="pref_flattr_auth_sum">Inicie sessão na sua conta flattr para fazer o flattr no AntennaPod</string> <string name="pref_flattr_this_app_title">Flattr desta aplicação</string> <string name="pref_flattr_this_app_sum">Ajude no desenvolvimento do AntennaPod através do Flattr. Obrigado!</string> <string name="pref_revokeAccess_title">Revogar acesso</string> - <string name="pref_revokeAccess_sum">Revogar permissões de acesso da aplicação à sua conta flattr.</string> + <string name="pref_revokeAccess_sum">Revogar permissões de acesso da aplicação à sua conta flattr</string> <string name="pref_auto_flattr_title">Flattr automático</string> <string name="pref_auto_flattr_sum">Configurar flattr automático</string> <string name="user_interface_label">Interface</string> <string name="pref_set_theme_title">Tema</string> - <string name="pref_set_theme_sum">Mudar o aspeto do AntennaPod.</string> + <string name="pref_nav_drawer_items_title">Alterar itens do menu</string> + <string name="pref_nav_drawer_items_sum">Alterar os itens que aparecem no menu de navegação</string> + <string name="pref_set_theme_sum">Mudar o aspeto do AntennaPod</string> <string name="pref_automatic_download_title">Transferência automática</string> - <string name="pref_automatic_download_sum">Configure a transferência automática dos episódios.</string> + <string name="pref_automatic_download_sum">Configure a transferência automática dos episódios</string> <string name="pref_autodl_wifi_filter_title">Ativar filtro Wi-Fi</string> - <string name="pref_autodl_wifi_filter_sum">Apenas permitir transferências automáticas através de redes sem fios.</string> + <string name="pref_autodl_wifi_filter_sum">Apenas permitir transferências automáticas através de redes sem fios</string> <string name="pref_automatic_download_on_battery_title">Transferência se não estiver a carregar</string> <string name="pref_automatic_download_on_battery_sum">Permitir transferência automática se a bateria não estiver a ser carregada</string> <string name="pref_parallel_downloads_title">Transferências simultâneas</string> @@ -239,24 +268,25 @@ <string name="pref_update_interval_hours_singular">hora</string> <string name="pref_update_interval_hours_manual">Manual</string> <string name="pref_gpodnet_authenticate_title">Acesso</string> - <string name="pref_gpodnet_authenticate_sum">Aceda à sua conta gpodder.net para poder sincronizar as subscrições.</string> + <string name="pref_gpodnet_authenticate_sum">Aceda à sua conta gpodder.net para poder sincronizar as subscrições</string> <string name="pref_gpodnet_logout_title">Sair</string> <string name="pref_gpodnet_logout_toast">Sessão terminada</string> <string name="pref_gpodnet_setlogin_information_title">Mudar informação de acesso</string> - <string name="pref_gpodnet_setlogin_information_sum">Mudar informação de acesso à sua conta gpodder.net.</string> + <string name="pref_gpodnet_setlogin_information_sum">Mudar informação de acesso à sua conta gpodder.net</string> <string name="pref_playback_speed_title">Velocidades de reprodução</string> - <string name="pref_playback_speed_sum">Personalize as velocidades de reprodução disponíveis.</string> - <string name="pref_seek_delta_title">Intervalo de procura</string> - <string name="pref_seek_delta_sum">Ao recuar ou avançar, procurar este valor de segundos</string> + <string name="pref_playback_speed_sum">Personalize as velocidades de reprodução disponíveis</string> + <string name="pref_fast_forward">Tempo a avançar</string> + <string name="pref_rewind">Tempo a recuar</string> <string name="pref_gpodnet_sethostname_title">Definir nome de servidor</string> - <string name="pref_gpodnet_sethostname_use_default_host">Utilizar pré-definição</string> + <string name="pref_gpodnet_sethostname_use_default_host">Utilizar predefinições</string> <string name="pref_expandNotify_title">Expansão de notificação</string> - <string name="pref_expandNotify_sum">Expandir sempre a notificação para mostrar os botões de reprodução.</string> + <string name="pref_expandNotify_sum">Expandir sempre a notificação para mostrar os botões de reprodução</string> <string name="pref_persistNotify_title">Controlos de reprodução persistentes</string> - <string name="pref_persistNotify_sum">Manter controlos de notificação e ecrã de bloqueio ao colocar a reprodução em pausa.</string> - <string name="pref_expand_notify_unsupport_toast">As versões Android anteriores à 4.1 não possuem suporte à expansão de notificações.</string> - <string name="pref_queueAddToFront_sum">Colocar novos episódios no inicio da fila.</string> + <string name="pref_persistNotify_sum">Manter controlos de notificação e ecrã de bloqueio ao colocar a reprodução em pausa</string> + <string name="pref_expand_notify_unsupport_toast">As versões Android anteriores à 4.1 não possuem suporte à expansão de notificações</string> + <string name="pref_queueAddToFront_sum">Colocar novos episódios no inicio da fila</string> <string name="pref_queueAddToFront_title">Novos episódios no inicio</string> + <string name="pref_smart_mark_as_played_disabled">Desativada</string> <!--Auto-Flattr dialog--> <string name="auto_flattr_enable">Ativar flattr automático</string> <string name="auto_flattr_after_percent">Flattr de episódios ao atingir %d porcento de reprodução</string> @@ -270,16 +300,16 @@ <string name="search_label">Procura</string> <string name="found_in_title_label">Encontrado no título</string> <!--OPML import and export--> - <string name="opml_import_txtv_button_lable">Os ficheiros OPML permitem-lhe mover os podcasts entre aplicações.</string> - <string name="opml_import_explanation_1">Escolha um caminho especifico no sistema local de ficheiros.</string> - <string name="opml_import_explanation_2">Utilize aplicações externas como o Dropbox, Google Drive ou o seu gestor de ficheiros preferido para abrir o ficheiro OPML.</string> - <string name="opml_import_explanation_3">As aplicações como o Google Mail, Dropbox, Google Drive ou gestores de ficheiros podem <i>abrir</i> os ficheiros OPML <i>através</i> do AntennaPod.</string> + <string name="opml_import_txtv_button_lable">Os ficheiros OPML permitem-lhe mover os podcasts entre aplicações</string> + <string name="opml_import_explanation_1">Escolha um caminho especifico no sistema local de ficheiros</string> + <string name="opml_import_explanation_2">Utilize aplicações externas como o Dropbox, Google Drive ou o seu gestor de ficheiros preferido para abrir o ficheiro OPML</string> + <string name="opml_import_explanation_3">As aplicações como o Google Mail, Dropbox, Google Drive ou gestores de ficheiros podem <i>abrir</i> os ficheiros OPML <i>através</i> do AntennaPod</string> <string name="start_import_label">Iniciar importação</string> <string name="opml_import_label">Importação OPML</string> <string name="opml_directory_error">Erro!</string> <string name="reading_opml_label">A ler ficheiro OPML</string> <string name="opml_reader_error">Ocorreu um erro ao ler o ficheiro OPML:</string> - <string name="opml_import_error_dir_empty">O diretório de importação está vazio.</string> + <string name="opml_import_error_dir_empty">O diretório de importação está vazio</string> <string name="select_all_label">Marcar tudo</string> <string name="deselect_all_label">Desmarcar tudo</string> <string name="choose_file_from_filesystem">Do sistema local de ficheiros</string> @@ -287,15 +317,15 @@ <string name="opml_export_label">Exportação OPML</string> <string name="exporting_label">Exportação...</string> <string name="export_error_label">Erro de exportação</string> - <string name="opml_export_success_title">Exportação efetuada.</string> - <string name="opml_export_success_sum">O ficheiro .opml foi gravado em:\u0020</string> + <string name="opml_export_success_title">Exportação efetuada</string> + <string name="opml_export_success_sum">O ficheiro .opml foi guardado em:\u0020</string> <!--Sleep timer--> <string name="set_sleeptimer_label">Definir temporizador</string> <string name="disable_sleeptimer_label">Desativar temporizador</string> - <string name="enter_time_here_label">Introduza o tempo</string> + <string name="enter_time_here_label">Tempo</string> <string name="sleep_timer_label">Temporizador</string> <string name="time_left_label">Tempo restante:\u0020</string> - <string name="time_dialog_invalid_input">Valor inválido. Tem que ser um inteiro.</string> + <string name="time_dialog_invalid_input">Tem que introduzir um número inteiro</string> <string name="time_unit_seconds">segundos</string> <string name="time_unit_minutes">minutos</string> <string name="time_unit_hours">horas</string> @@ -309,11 +339,11 @@ <string name="gpodnetauth_login_butLabel">Acesso</string> <string name="gpodnetauth_login_register">Se ainda não possui uma conta, pode criar uma em:\nhttps://gpodder.net/register/</string> <string name="username_label">Utilizador</string> - <string name="password_label">Senha</string> + <string name="password_label">Palavra-passe</string> <string name="gpodnetauth_device_title">Seleção de dispositivo</string> <string name="gpodnetauth_device_descr">Criar um novo dispositivo ou escolher um existente para aceder à sua conta gpodder.net</string> <string name="gpodnetauth_device_deviceID">ID do dispositivo:\u0020</string> - <string name="gpodnetauth_device_caption">Legenda</string> + <string name="gpodnetauth_device_caption">Nome</string> <string name="gpodnetauth_device_butCreateNewDevice">Criar novo dispositivo</string> <string name="gpodnetauth_device_chooseExistingDevice">Escolher dispositivo:</string> <string name="gpodnetauth_device_errorEmpty">ID do dispositivo não pode estar vazia</string> @@ -323,33 +353,35 @@ <string name="gpodnetauth_finish_descr">Parabéns! A sua conta gpodder.net está vinculada ao seu dispositivo. Agora, já pode sincronizar as subscrições no dispositivo com a sua conta gpodder.net.</string> <string name="gpodnetauth_finish_butsyncnow">Sincronizar agora</string> <string name="gpodnetauth_finish_butgomainscreen">Ir para o ecrã principal</string> - <string name="gpodnetsync_auth_error_title">Erro de autenticação gpodder.net</string> - <string name="gpodnetsync_auth_error_descr">Utilizador ou senha inválido</string> + <string name="gpodnetsync_auth_error_title">Erro de autenticação no gpodder.net</string> + <string name="gpodnetsync_auth_error_descr">Utilizador ou palavra-passe inválida</string> <string name="gpodnetsync_error_title">Erro de sincronização gpodder.net</string> <string name="gpodnetsync_error_descr">Ocorreu um erro ao sincronizar:\u0020</string> <!--Directory chooser--> <string name="selected_folder_label">Diretório escolhido:</string> - <string name="create_folder_label">Criar diretório</string> - <string name="choose_data_directory">Escolha o diretório</string> - <string name="create_folder_msg">Criar um diretório com o nome \"%1$s\"?</string> - <string name="create_folder_success">Novo diretório criado</string> - <string name="create_folder_error_no_write_access">Não é possível gravar neste diretório</string> - <string name="create_folder_error_already_exists">O diretório já existe</string> - <string name="create_folder_error">Não é possível criar o diretório</string> - <string name="folder_not_empty_dialog_title">Diretório não vazio</string> - <string name="folder_not_empty_dialog_msg">O diretório escolhido não está vazio. As transferências serão colocadas neste diretório. Continuar?</string> + <string name="create_folder_label">Criar pasta</string> + <string name="choose_data_directory">Escolha a pasta de dados</string> + <string name="create_folder_msg">Criar uma pasta com o nome \"%1$s\"?</string> + <string name="create_folder_success">Nova pasta criada</string> + <string name="create_folder_error_no_write_access">Não é possível guardar nesta pasta</string> + <string name="create_folder_error_already_exists">A pasta já existe</string> + <string name="create_folder_error">Não é possível criar a pasta</string> + <string name="folder_not_empty_dialog_title">A pasta não está vazia</string> + <string name="folder_not_empty_dialog_msg">A pasta escolhida não está vazia. As transferências multimédia e os ficheiros serão colocados nesta pasta. Continuar?</string> <string name="set_to_default_folder">Escolha a pasta pré-definida</string> <string name="pref_pausePlaybackForFocusLoss_sum">Pausa na reprodução em vez de baixar o volume se outra aplicação quiser reproduzir sons</string> <string name="pref_pausePlaybackForFocusLoss_title">Pausa nas interrupções</string> + <string name="pref_resumeAfterCall_sum">Continuar reprodução ao terminar a chamada</string> + <string name="pref_resumeAfterCall_title">Continuar após a chamada</string> <!--Online feed view--> <string name="subscribe_label">Subscrever</string> <string name="subscribed_label">Subscrito</string> - <string name="downloading_label">Transferência...</string> + <string name="downloading_label">A transferir...</string> <!--Content descriptions for image buttons--> <string name="show_chapters_label">Mostrar capítulos</string> <string name="show_shownotes_label">Mostrar notas</string> <string name="show_cover_label">Mostrar imagem</string> - <string name="rewind_label">Recuar</string> + <string name="rewind_label">Recuo rápido</string> <string name="fast_forward_label">Avanço rápido</string> <string name="media_type_audio_label">Áudio</string> <string name="media_type_video_label">Vídeo</string> @@ -366,7 +398,7 @@ <string name="load_next_page_label">Carregar próxima página</string> <!--Feed information screen--> <string name="authentication_label">Autenticação</string> - <string name="authentication_descr">Altere o seu nome de utilizador e senha para este podcast e seus episódios.</string> + <string name="authentication_descr">Altere o seu nome de utilizador e senha para este podcast e seus episódios</string> <!--AntennaPodSP--> <string name="sp_apps_importing_feeds_msg">Importar subscrições de aplicações single-purpose...</string> <string name="search_itunes_label">Procurar no iTunes</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 e08e40e56..ae10b314f 100644 --- a/core/src/main/res/values-ru/strings.xml +++ b/core/src/main/res/values-ru/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">Неправильный адрес</string> <string name="download_error_io_error">Ошибка ввода-вывода</string> @@ -141,6 +141,7 @@ <string name="position_default_label">00:00:00</string> <string name="player_buffering_msg">Буферизация</string> <string name="playbackservice_notification_title">Воспроизведение подкаста</string> + <string name="unknown_media_key">AntennaPod - неизвестный ключ носителя: %1$d</string> <!--Queue operations--> <string name="clear_queue_label">Очистить очередь</string> <string name="undo">Отмена</string> @@ -161,6 +162,7 @@ <string name="return_home_label">Вернуться к началу</string> <string name="flattr_auth_success">Успешная авторизация. Теперь можно использовать Flattr прямо из приложения.</string> <string name="no_flattr_token_title">Токен Flattr не найден</string> + <string name="no_flattr_token_notification_msg">Ваша учетная запись Flattr не подключена к AntennaPod. Нажмите здесь, чтобы войти.</string> <string name="no_flattr_token_msg">Кажется, ваш аккаунт Flattr не подключен к AntennaPod. Можно подключить аккаунт к AntennaPod или посетить сайт канала, чтобы пожертвовать через Flattr прямо на сайте.</string> <string name="authenticate_now_label">Авторизоваться</string> <string name="action_forbidden_title">Действие запрещено</string> @@ -182,6 +184,7 @@ <!--Variable Speed--> <string name="download_plugin_label">Загрузить плагин</string> <string name="no_playback_plugin_title">Плагин не установлен</string> + <string name="no_playback_plugin_msg">Для работы переменной скорости воспроизведения нужно установить библиотеку стороннего разработчика.\n\nНажмите \"Загрузить плагин\", чтобы скачать бесплатный плагин из Play Store\n\nЛюбые вопросы по работе плагина находятся вне ответственности AntennaPod и должны быть направлены владельцу плагина.</string> <string name="set_playback_speed_label">Скорость воспроизведения</string> <!--Empty list labels--> <string name="no_items_label">Список пуст</string> @@ -217,6 +220,7 @@ <string name="pref_revokeAccess_title">Отозвать доступ</string> <string name="pref_revokeAccess_sum">Отменить доступ этого приложения к вашему аккаунту Flattr.</string> <string name="pref_auto_flattr_title">Автоматически поддерживать через Flattr</string> + <string name="pref_auto_flattr_sum">Настройка автоматической поддержки через Flattr</string> <string name="user_interface_label">Интерфейс</string> <string name="pref_set_theme_title">Выбор темы</string> <string name="pref_set_theme_sum">Изменить тему оформления AntennaPod</string> @@ -242,8 +246,6 @@ <string name="pref_gpodnet_setlogin_information_sum">Изменить информацию авторизации для аккаунта gpodder.net</string> <string name="pref_playback_speed_title">Скорость воспроизведения</string> <string name="pref_playback_speed_sum">Настроить скорости воспроизведения</string> - <string name="pref_seek_delta_title">Перемотка</string> - <string name="pref_seek_delta_sum">Пропускать секунд при перемотке назад или вперед</string> <string name="pref_gpodnet_sethostname_title">Задать имя узла</string> <string name="pref_gpodnet_sethostname_use_default_host">Использовать узел по умолчанию</string> <string name="pref_expandNotify_title">Расширенное уведомление</string> @@ -251,7 +253,10 @@ <string name="pref_persistNotify_title">Постоянный контрооль воспроизведения</string> <string name="pref_persistNotify_sum">Сохранять уведомление и кнопки воспроизведения на экране блокировки во время паузы.</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> <string name="auto_flattr_ater_beginning">Поддерживать эпизод через Flattr в начале воспроизведения</string> <string name="auto_flattr_ater_end">Поддерживать эпизод через Flattr в конце воспроизведения</string> @@ -265,6 +270,8 @@ <!--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">Множество приложений умеют <i>открывать</i> OPML-файлы <i>в</i> AntennaPod, например: Google Mail, Dropbox, Google Drive и большинство файловых менеджеров.</string> <string name="start_import_label">Начать импорт</string> <string name="opml_import_label">Импорт OPML</string> <string name="opml_directory_error">Ошибка</string> @@ -273,9 +280,12 @@ <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> + <string name="opml_export_success_title">OPML успешно экспортирован.</string> <string name="opml_export_success_sum">Файл OPML был записан в:\u0020</string> <!--Sleep timer--> <string name="set_sleeptimer_label">Установить таймер сна</string> @@ -354,6 +364,7 @@ <string name="load_next_page_label">Загрузить следующую страницу</string> <!--Feed information screen--> <string name="authentication_label">Авторизация</string> + <string name="authentication_descr">Изменить имя пользователя и пароль для этого подкаста и его выпусков.</string> <!--AntennaPodSP--> <string name="sp_apps_importing_feeds_msg">Импорт подписок из одноцелевых приложений…</string> <string name="search_itunes_label">Поиск в iTunes</string> diff --git a/core/src/main/res/values-sv-rSE/strings.xml b/core/src/main/res/values-sv-rSE/strings.xml index 4e468c1e1..c0c49ca7d 100644 --- a/core/src/main/res/values-sv-rSE/strings.xml +++ b/core/src/main/res/values-sv-rSE/strings.xml @@ -5,7 +5,7 @@ <string name="feeds_label">Flöden</string> <string name="add_feed_label">Lägg till podcast</string> <string name="podcasts_label">PODCASTS</string> - <string name="episodes_label">AVSNITT</string> + <string name="episodes_label">EPISODER</string> <string name="new_episodes_label">Nya episoder</string> <string name="all_episodes_label">Alla episoder</string> <string name="new_label">Ny</string> @@ -26,6 +26,7 @@ <!--Main activity--> <string name="drawer_open">Öppna meny</string> <string name="drawer_close">Stäng meny</string> + <string name="drawer_preferences">Lådinställningar</string> <!--Webview actions--> <string name="open_in_browser_label">Öppna i webbläsare</string> <string name="copy_url_label">Kopiera URL</string> @@ -39,6 +40,7 @@ <string name="cancel_label">Avbryt</string> <string name="author_label">Skapare</string> <string name="language_label">Språk</string> + <string name="url_label">URL</string> <string name="podcast_settings_label">Inställningar</string> <string name="cover_label">Bild</string> <string name="error_label">Fel</string> @@ -48,7 +50,7 @@ <string name="chapters_label">Kapitel</string> <string name="shownotes_label">Shownotes</string> <string name="description_label">Beskrivning</string> - <string name="most_recent_prefix">Senaste avsnittet:\u0020</string> + <string name="most_recent_prefix">Senaste episoden:\u0020</string> <string name="episodes_suffix">\u0020episoder</string> <string name="length_prefix">Längd:\u0020</string> <string name="size_prefix">Storlek:\u0020</string> @@ -64,13 +66,13 @@ <string name="etxtFeedurlHint">URL till flöde eller webbsida</string> <string name="txtvfeedurl_label">Lägg till podcast via URL</string> <string name="podcastdirectories_label">Hitta podcast i mapp</string> - <string name="podcastdirectories_descr">Du kan söka efter podcasts baserat på namn, kategori eller populäritet med tjänsten gpodder.net</string> + <string name="podcastdirectories_descr">Du kan söka efter podcasts baserat på namn, kategori eller populäritet med tjänsten gpodder.net eller på iTunes Store.</string> <string name="browse_gpoddernet_label">Bläddra på gpodder.net</string> <!--Actions on feeds--> - <string name="mark_all_read_label">Markera alla som lästa</string> - <string name="mark_all_read_msg">Markera alla episoder som lästa</string> - <string name="mark_all_read_confirmation_msg">Bekräfta att du vill markera alla avsnitt som lästa.</string> - <string name="mark_all_read_feed_confirmation_msg">Bekräfta att du vill markera alla avsnitt i detta flöde som lästa.</string> + <string name="mark_all_read_label">Markera alla som spelade</string> + <string name="mark_all_read_msg">Markera alla episoder som spelade</string> + <string name="mark_all_read_confirmation_msg">Bekräfta att du verkligen vill markera alla episoder som spelade.</string> + <string name="mark_all_read_feed_confirmation_msg">Bekräfta att du verkligen vill markera alla episoder i detta flöde som spelade.</string> <string name="show_info_label">Visa information</string> <string name="remove_feed_label">Ta bort podcast</string> <string name="share_link_label">Dela hemsidans länk</string> @@ -78,6 +80,16 @@ <string name="feed_delete_confirmation_msg">Bekräfta att du vill ta bort denna feed och ALLA avsnitt av denna feed som du har hämtat.</string> <string name="feed_remover_msg">Tar bort flöde</string> <string name="load_complete_feed">Uppdatera hela flödet</string> + <string name="hide_episodes_title">Dölj episoder</string> + <string name="hide_unplayed_episodes_label">Ospelade</string> + <string name="hide_paused_episodes_label">Pausad</string> + <string name="hide_played_episodes_label">Spelad</string> + <string name="hide_queued_episodes_label">Köad</string> + <string name="hide_not_queued_episodes_label">Inte köad</string> + <string name="hide_downloaded_episodes_label">Nedladdad</string> + <string name="hide_not_downloaded_episodes_label">Inte nedladdad</string> + <string name="filtered_label">Filtrerad</string> + <string name="refresh_failed_msg">{fa-exclamation-circle} Senaste uppdateringen misslyckades</string> <!--actions on feeditems--> <string name="download_label">Ladda ned</string> <string name="play_label">Spela</string> @@ -86,16 +98,20 @@ <string name="stream_label">Strömma</string> <string name="remove_label">Ta bort</string> <string name="remove_episode_lable">Ta bort episod</string> - <string name="mark_read_label">Markera som läst</string> - <string name="mark_unread_label">Markera som oläst</string> - <string name="marked_as_read_label">Markerad som läst</string> + <string name="mark_read_label">Markera som spelad</string> + <string name="mark_unread_label">Markera som ospelad</string> + <string name="marked_as_read_label">Markera som spelad</string> <string name="add_to_queue_label">Lägg till i kön</string> + <string name="added_to_queue_label">Lägg till i Kö</string> <string name="remove_from_queue_label">Ta bort från Kön</string> <string name="visit_website_label">Besök websidan</string> <string name="support_label">Flattra det här</string> <string name="enqueue_all_new">Lägg till alla i kön</string> <string name="download_all">Ladda ner alla</string> <string name="skip_episode_label">Hoppa över episod</string> + <string name="activate_auto_download">Aktivera automatisk nedladdning</string> + <string name="deactivate_auto_download">Avaktivera automatisk nedladdning</string> + <string name="reset_position">Nollställ uppspelningsposition</string> <!--Download messages and labels--> <string name="download_successful">lyckades</string> <string name="download_failed">misslyckades</string> @@ -112,8 +128,10 @@ <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_report_title">Nedladdningar färdiga</string> + <string name="download_canceled_msg">Nedladdning avbruten</string> + <string name="download_canceled_autodownload_enabled_msg">Nedladdning avbruten\nStängde av <i>Automatisk nedladdning</i> för denna sak</string> + <string name="download_report_title">Nedladdningar avslutades med fel</string> + <string name="download_report_content_title">Nedladdningsrapport</string> <string name="download_error_malformed_url">Felaktig webbadress</string> <string name="download_error_io_error">IO fel</string> <string name="download_error_request_error">Request fel</string> @@ -129,6 +147,11 @@ <string name="download_request_error_dialog_message_prefix">Ett fel uppstod vid försöket att ladda ner filen:\u0020</string> <string name="authentication_notification_title">Autentisering krävs</string> <string name="authentication_notification_msg">Resursen du begärde kräver ett användarnamn och ett lösenord</string> + <string name="confirm_mobile_download_dialog_title">Bekräfta mobil nedladdning</string> + <string name="confirm_mobile_download_dialog_message_not_in_queue">Nedladdning över mobil dataanslutning är avaktiverat i inställningarna.\n\nAktivera tillfälligt eller bara lägg till i kön?\n\n<small>Ditt val gäller i 10 minuter.</small></string> + <string name="confirm_mobile_download_dialog_message">Nedladdning över mobil dataanslutning är avstängt i inställningarna.\n\nAktivera tillfälligt?\n\n<small>Ditt val gäller i 10 minuter.</small></string> + <string name="confirm_mobile_download_dialog_only_add_to_queue">Lägg bara till i kön</string> + <string name="confirm_mobile_download_dialog_enable_temporarily">Aktivera tillfälligt</string> <!--Mediaplayer messages--> <string name="player_error_msg">Fel! </string> <string name="player_stopped_msg">Inget media spelar</string> @@ -143,6 +166,8 @@ <string name="playbackservice_notification_title">Spelar podcast</string> <string name="unknown_media_key">AntannaPod - Okänd mediaknapp: %1$d</string> <!--Queue operations--> + <string name="lock_queue">Lås kön</string> + <string name="unlock_queue">Lås upp kön</string> <string name="clear_queue_label">Rensa kön</string> <string name="undo">Ångra</string> <string name="removed_from_queue">Föremålet avlägsnades</string> @@ -154,7 +179,7 @@ <string name="duration">Längd</string> <string name="ascending">Stigande</string> <string name="descending">Fallande</string> - <string name="clear_queue_confirmation_msg">Bekräfta att du vill rensa kön från ALLA avsnitt.</string> + <string name="clear_queue_confirmation_msg">Bekräfta att du vill rensa kön från ALLA episoder.</string> <!--Flattr--> <string name="flattr_auth_label">Flattr inloggning</string> <string name="flattr_auth_explanation">Tryck på knappen nedan för att starta autentiseringen. Du kommer att vidarebefordras till Flattrs inloggningsskärm i din webbläsare och uppmanas att ge AntennaPod tillstånd att Flattra saker. Efter att du har gett tillstånd, kommer du automatiskt tillbaka till den här skärmen.</string> @@ -200,6 +225,8 @@ <string name="pref_followQueue_sum">Hoppa till nästa i kön när uppspelningen är klar</string> <string name="pref_auto_delete_sum">Ta bort episoden när uppspelningen är klar</string> <string name="pref_auto_delete_title">Automatisk borttagning</string> + <string name="pref_smart_mark_as_played_sum">Markera episoder som spelade även om mindre än ett visst antal sekunder är kvar</string> + <string name="pref_smart_mark_as_played_title">Smart markering av uppspelat innehåll</string> <string name="playback_pref">Uppspelning</string> <string name="network_pref">Nätverk </string> <string name="pref_autoUpdateIntervall_title">Uppdateringsintervall</string> @@ -223,6 +250,8 @@ <string name="pref_auto_flattr_sum">Konfigurerar automatisk Flattring</string> <string name="user_interface_label">Användargränssnitt</string> <string name="pref_set_theme_title">Välj tema</string> + <string name="pref_nav_drawer_items_title">Ändra navigationslådan</string> + <string name="pref_nav_drawer_items_sum">Ändra vilka saker som visas i navigationslådan.</string> <string name="pref_set_theme_sum">Ändra utseendet på AntennaPod.</string> <string name="pref_automatic_download_title">Automatisk nedladdning</string> <string name="pref_automatic_download_sum">Konfigurera automatisk nedladdning av episoder.</string> @@ -231,7 +260,7 @@ <string name="pref_automatic_download_on_battery_title">Nedladdning vid batteridrift</string> <string name="pref_automatic_download_on_battery_sum">Tillåt automatisk nedladdning när batteriet inte laddas</string> <string name="pref_parallel_downloads_title">Parallella nedladdningar</string> - <string name="pref_episode_cache_title">Avsnittscache</string> + <string name="pref_episode_cache_title">Episodcache</string> <string name="pref_theme_title_light">Ljust</string> <string name="pref_theme_title_dark">Mörkt</string> <string name="pref_episode_cache_unlimited">Obegränsat</string> @@ -246,8 +275,8 @@ <string name="pref_gpodnet_setlogin_information_sum">Ändra inloggningsinformationen för ditt gpodder.net konto.</string> <string name="pref_playback_speed_title">Uppspelningshastigheter</string> <string name="pref_playback_speed_sum">Anpassa de tillgängliga hastigheterna för variabel uppspelningshastighet.</string> - <string name="pref_seek_delta_title">Söklängd</string> - <string name="pref_seek_delta_sum">Sök så här många sekunder vid snabbspolning bakåt eller framåt</string> + <string name="pref_fast_forward">Spola framåt</string> + <string name="pref_rewind">Spola bakåt</string> <string name="pref_gpodnet_sethostname_title">Sätt värdnamn</string> <string name="pref_gpodnet_sethostname_use_default_host">Använd standardvärden</string> <string name="pref_expandNotify_title">Expandera notifieringar</string> @@ -255,15 +284,16 @@ <string name="pref_persistNotify_title">Bestående uppspelningskontroller</string> <string name="pref_persistNotify_sum">Behåll notifiering och kontroller på låsskärmen när uppspelningen pausas.</string> <string name="pref_expand_notify_unsupport_toast">Androidversioner före 4.1 har inte stöd för expanderade notifieringar.</string> - <string name="pref_queueAddToFront_sum">Lägg till avsnitt först i kön.</string> + <string name="pref_queueAddToFront_sum">Lägg till episoder först i kön.</string> <string name="pref_queueAddToFront_title">Köa först.</string> + <string name="pref_smart_mark_as_played_disabled">Avaktiverad</string> <!--Auto-Flattr dialog--> <string name="auto_flattr_enable">Aktivera automatisk Flattring</string> <string name="auto_flattr_after_percent">Flattra episoden så snart %d procent har spelats</string> <string name="auto_flattr_ater_beginning">Flattra episoden när den startas</string> <string name="auto_flattr_ater_end">Flattra episoden när den spelats klart</string> <!--Search--> - <string name="search_hint">Sök efter flöden eller avsnitt</string> + <string name="search_hint">Sök efter flöden eller episoder</string> <string name="found_in_shownotes_label">Hittad i shownotes</string> <string name="found_in_chapters_label">Hittad i kapitel</string> <string name="search_status_no_results">Inga resultat hittades</string> @@ -341,6 +371,8 @@ <string name="set_to_default_folder">Välj standardmapp</string> <string name="pref_pausePlaybackForFocusLoss_sum">Pausa uppspelning istället för att sänka volymen när en annan app vill spela ljud</string> <string name="pref_pausePlaybackForFocusLoss_title">Pausa för avbrott</string> + <string name="pref_resumeAfterCall_sum">Återuppta uppspelning när ett telefonsamtal avslutas</string> + <string name="pref_resumeAfterCall_title">Återuppta efter samtal</string> <!--Online feed view--> <string name="subscribe_label">Prenumerera</string> <string name="subscribed_label">Prenumererar</string> diff --git a/core/src/main/res/values-tr/strings.xml b/core/src/main/res/values-tr/strings.xml index 265a9025c..e83c9b48e 100644 --- a/core/src/main/res/values-tr/strings.xml +++ b/core/src/main/res/values-tr/strings.xml @@ -112,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> diff --git a/core/src/main/res/values-uk-rUA/strings.xml b/core/src/main/res/values-uk-rUA/strings.xml index 1602e6253..b6cd8ca98 100644 --- a/core/src/main/res/values-uk-rUA/strings.xml +++ b/core/src/main/res/values-uk-rUA/strings.xml @@ -26,6 +26,7 @@ <!--Main activity--> <string name="drawer_open">Показати меню</string> <string name="drawer_close">Сховати меню</string> + <string name="drawer_preferences">Настройки навігації</string> <!--Webview actions--> <string name="open_in_browser_label">Відкрити в браузері</string> <string name="copy_url_label">Копіювати URL</string> @@ -39,6 +40,7 @@ <string name="cancel_label">Скасувати</string> <string name="author_label">Автор</string> <string name="language_label">Мова</string> + <string name="url_label">URL</string> <string name="podcast_settings_label">Налаштування</string> <string name="cover_label">Зображення</string> <string name="error_label">Помилка</string> @@ -67,10 +69,10 @@ <string name="podcastdirectories_descr">В каталозі gpodder.net можливий пошук за назвою, категорією або популярністю.</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="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> @@ -78,6 +80,16 @@ <string name="feed_delete_confirmation_msg">Ви впенені що хочете видаліти канал та всі завантажені епізоди</string> <string name="feed_remover_msg">Удаляю канал</string> <string name="load_complete_feed">Оновити канал цілком</string> + <string name="hide_episodes_title">Приховати епізоди</string> + <string name="hide_unplayed_episodes_label">Неграні</string> + <string name="hide_paused_episodes_label">На паузі</string> + <string name="hide_played_episodes_label">Грані</string> + <string name="hide_queued_episodes_label">В черзі</string> + <string name="hide_not_queued_episodes_label">Не в черзі</string> + <string name="hide_downloaded_episodes_label">Завантажені</string> + <string name="hide_not_downloaded_episodes_label">Не завантажені</string> + <string name="filtered_label">Фільтровані</string> + <string name="refresh_failed_msg">{fa-exclamation-circle} Останнє оновлення було невдалим</string> <!--actions on feeditems--> <string name="download_label">Завантажити</string> <string name="play_label">Грати</string> @@ -86,16 +98,20 @@ <string name="stream_label">Прослухати без завантаження</string> <string name="remove_label">Видалити</string> <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="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="added_to_queue_label">Додано до черги</string> <string name="remove_from_queue_label">Видалити з черги</string> <string name="visit_website_label">Відкрити сайт</string> <string name="support_label">Підтримати за допомогою Flattr</string> <string name="enqueue_all_new">Додати до черги</string> <string name="download_all">Завантажити все</string> <string name="skip_episode_label">Пропустити епізод</string> + <string name="activate_auto_download">Включити автоматичне завантаження</string> + <string name="deactivate_auto_download">Виключити автозавантаження</string> + <string name="reset_position">Вернути початкову позицію відтворення</string> <!--Download messages and labels--> <string name="download_successful">успішно</string> <string name="download_failed">з помилками</string> @@ -112,8 +128,10 @@ <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_report_title">Завантажили</string> + <string name="download_canceled_msg">Завантаження скасоване</string> + <string name="download_canceled_autodownload_enabled_msg">Завантаження скасоване\nВимкнуто <i>автоматичне завантаження</i> для цього елемента</string> + <string name="download_report_title">Завантаження завершені з помилками</string> + <string name="download_report_content_title">Звіт про завантаження</string> <string name="download_error_malformed_url">Невірний URL</string> <string name="download_error_io_error">Помилка IO</string> <string name="download_error_request_error">Помилка запиту</string> @@ -129,6 +147,11 @@ <string name="download_request_error_dialog_message_prefix">Помилка при завантажені файлу:\u0020</string> <string name="authentication_notification_title">Потрібна автентифікація</string> <string name="authentication_notification_msg">Для доступа до цього ресурса потрібні ім\'я та пароль </string> + <string name="confirm_mobile_download_dialog_title">Підтвердження мобільних завантажень</string> + <string name="confirm_mobile_download_dialog_message_not_in_queue">Завантаження через мобільний зв\'язок вимкнено в настройках.\n\nУвімкнути тимчасово або тільки додати до черги?\n\n<small>Ваш вибір буде запам\'ятовано на 10 хвилин.</small></string> + <string name="confirm_mobile_download_dialog_message">Завантаження через мобільний зв\'язок вимкнено в настройках.\n\nУвімкнути тимчасово?\n\n<small>Ваш вибір буде запам\'ятовано на 10 хвилин.</small></string> + <string name="confirm_mobile_download_dialog_only_add_to_queue">Лише додати до черги</string> + <string name="confirm_mobile_download_dialog_enable_temporarily">Увімкнути тимчасово</string> <!--Mediaplayer messages--> <string name="player_error_msg">Помилка!</string> <string name="player_stopped_msg"> Нічого грати</string> @@ -143,6 +166,8 @@ <string name="playbackservice_notification_title">Грає подкаст</string> <string name="unknown_media_key">AntennaPod - Невідомий медіа ключ: %1$d</string> <!--Queue operations--> + <string name="lock_queue">Заблокувати чергу</string> + <string name="unlock_queue">Розблокувати чергу</string> <string name="clear_queue_label">Очистити чергу</string> <string name="undo">Скасувати</string> <string name="removed_from_queue">Видалено</string> @@ -200,6 +225,8 @@ <string name="pref_followQueue_sum">Перейти до наступного епізода в черзі коли поточний закінчено</string> <string name="pref_auto_delete_sum">Видалити епізод після повного відтворення</string> <string name="pref_auto_delete_title">Автовидалення</string> + <string name="pref_smart_mark_as_played_sum">Позначити епізоди як грані навіть якщо залишилось менш ніж зазначене число секунд до кінця відтворення</string> + <string name="pref_smart_mark_as_played_title">Розумне позначення граних епізодів</string> <string name="playback_pref">Відтворення</string> <string name="network_pref">Мережа</string> <string name="pref_autoUpdateIntervall_title">Частота оновлень</string> @@ -223,6 +250,8 @@ <string name="pref_auto_flattr_sum">Налаштування автоматичного заохочення авторів через сервіс flattr</string> <string name="user_interface_label">Вигляд</string> <string name="pref_set_theme_title">Обрати тему</string> + <string name="pref_nav_drawer_items_title">Змінити настройки навігації</string> + <string name="pref_nav_drawer_items_sum">Вибрати елементи для використання у навігації</string> <string name="pref_set_theme_sum">Змінити вигляд AntennaPod</string> <string name="pref_automatic_download_title">Автоматичне завантаження</string> <string name="pref_automatic_download_sum">Налаштування автоматичного завантаження епізодів</string> @@ -246,8 +275,8 @@ <string name="pref_gpodnet_setlogin_information_sum">Змінити інформацію щодо облікового запису gpodder.net</string> <string name="pref_playback_speed_title">Швидкість програвання</string> <string name="pref_playback_speed_sum">Налаштування швідкості доступно для змінної швидкості програвання</string> - <string name="pref_seek_delta_title">Час перемотки</string> - <string name="pref_seek_delta_sum">Перейти на таку кількість секунд при перемотуванні назад або вперед</string> + <string name="pref_fast_forward">Час перемотки вперед</string> + <string name="pref_rewind">Час перемотки назад</string> <string name="pref_gpodnet_sethostname_title">Встановити ім\'я хоста</string> <string name="pref_gpodnet_sethostname_use_default_host">Використати хост по замовчанню</string> <string name="pref_expandNotify_title">Розгорнути повідомлення</string> @@ -257,6 +286,7 @@ <string name="pref_expand_notify_unsupport_toast">Android до версії 4.1 не підтримує розширені повідомлення.</string> <string name="pref_queueAddToFront_sum">Додавати нові епізоди до початку черги.</string> <string name="pref_queueAddToFront_title">Додавати в початок черги.</string> + <string name="pref_smart_mark_as_played_disabled">Вимкнено</string> <!--Auto-Flattr dialog--> <string name="auto_flattr_enable">Включити автоматичне заохочення авторів через сервіс flattr</string> <string name="auto_flattr_after_percent">Заохотити автора через Flattr щойно %d відсотків епізода було відтворено</string> @@ -340,7 +370,9 @@ <string name="folder_not_empty_dialog_msg">В папці щось є. Всі завантаження зберігаються в цю папку. Все рівно продовжувати?</string> <string name="set_to_default_folder">Обрати папку по замовчанню</string> <string name="pref_pausePlaybackForFocusLoss_sum">Призупиняти програвання замість зниження гучності коли інша програма хоче програти звук</string> - <string name="pref_pausePlaybackForFocusLoss_title">Пауза для перевивання</string> + <string name="pref_pausePlaybackForFocusLoss_title">Пауза в разі переривання</string> + <string name="pref_resumeAfterCall_sum">Відновити відтворення після закінчення дзвінка</string> + <string name="pref_resumeAfterCall_title">Відновити після дзвінка</string> <!--Online feed view--> <string name="subscribe_label">Підписатися</string> <string name="subscribed_label">Підписано</string> diff --git a/core/src/main/res/values-zh-rCN/strings.xml b/core/src/main/res/values-zh-rCN/strings.xml index d857ea194..594249a31 100644 --- a/core/src/main/res/values-zh-rCN/strings.xml +++ b/core/src/main/res/values-zh-rCN/strings.xml @@ -31,6 +31,7 @@ <string name="copy_url_label">复制 URL</string> <string name="share_url_label">分享 URL</string> <string name="copied_url_msg">复制 URL 到剪贴板.</string> + <string name="go_to_position_label">去往该位置</string> <!--Playback history--> <string name="clear_history_label">清空历史</string> <!--Other--> @@ -57,9 +58,12 @@ <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 通过名称、类别或热门来搜索新播客</string> <string name="browse_gpoddernet_label">浏览 gpodder.net</string> <!--Actions on feeds--> @@ -71,10 +75,12 @@ <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> <string name="pause_label">暂停</string> + <string name="stop_label">停止</string> <string name="stream_label">流媒体</string> <string name="remove_label">删除</string> <string name="remove_episode_lable">移除曲目</string> @@ -103,7 +109,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> @@ -132,12 +138,20 @@ <string name="position_default_label">00:00:00</string> <string name="player_buffering_msg">缓冲中</string> <string name="playbackservice_notification_title">播客播放中</string> + <string name="unknown_media_key">AntennaPod - 未知媒体密钥: %1$d</string> <!--Queue operations--> <string name="clear_queue_label">清空播放列表</string> <string name="undo">撤消</string> <string name="removed_from_queue">已删除项</string> <string name="move_to_top_label">移到顶端</string> <string name="move_to_bottom_label">移到下部</string> + <string name="sort">排序</string> + <string name="alpha">按字母</string> + <string name="date">按日期</string> + <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> @@ -145,16 +159,29 @@ <string name="return_home_label">返回主页</string> <string name="flattr_auth_success">验证成功! 现在可以使用应用内 Flattr 相关功能了.</string> <string name="no_flattr_token_title">没有找到 Flattr 验证令牌信息</string> - <string name="no_flattr_token_msg">您的 flattr 账户似乎并没有连接到 AntennaPod. You can either connect your account to AntennaPod to flattr things within the app or you can visit the website of the thing to flattr it there.</string> + <string name="no_flattr_token_notification_msg">您的 flattr 账户似乎并没有连接到 AntennaPod,点这里验证。</string> + <string name="no_flattr_token_msg">您的 flattr 账户似乎并没有连接到 AntennaPod. 您可以关联您的账户到AntennaPod以便在应用内flattr条目,或者您可以自己访问条目的网站完成flattr</string> <string name="authenticate_now_label">验证</string> <string name="action_forbidden_title">被禁止</string> <string name="action_forbidden_msg">AntennaPod 没有权限执行本动作. 原因可能是: AntennaPod 对您账户的访问令牌被撤销. 你可以重新\"验证\"或访问该网站来授权.</string> <string name="access_revoked_title">撤销访问</string> <string name="access_revoked_info">您已经成功撤销 AntennaPod 对账户令牌的访问. 为了完成这个过程, 您必须到 Flattr 网站 \"账户设置->已批准应用\" 列表内删除本应用.</string> <!--Flattr--> + <string name="flattr_click_success">成功Flattr一个条目</string> + <string name="flattr_click_success_count">成功Flattr%d个条目</string> + <string name="flattr_click_success_queue">Flattr%s成功.</string> + <string name="flattr_click_failure_count">Flattr失败%d个条目</string> + <string name="flattr_click_failure">%sFlattr失败.</string> + <string name="flattr_click_enqueued">稍后将Flattr该条目</string> + <string name="flattring_thing">正在Flattr%s</string> + <string name="flattring_label">AntennaPod正在Flattr</string> + <string name="flattrd_label">AntennaPod已经Flattr</string> + <string name="flattrd_failed_label">AntennaPod Flattr失败</string> + <string name="flattr_retrieving_status">查询Flattr结果状态</string> <!--Variable Speed--> <string name="download_plugin_label">插件下载</string> <string name="no_playback_plugin_title">插件没有安装</string> + <string name="no_playback_plugin_msg">要使播放速度可变,必须安装第三方库。\n\n点击 \'插件下载\' 从 \'Play 商店\' 下载免费插件\n\n使用这些插件中碰到的任何问题请报告给插件作者,与 AntennaPod 无关。</string> <string name="set_playback_speed_label">播放速度</string> <!--Empty list labels--> <string name="no_items_label">列表为空.</string> @@ -168,7 +195,7 @@ <string name="pref_pauseOnHeadsetDisconnect_sum">耳机断开时暂停播放 </string> <string name="pref_unpauseOnHeadsetReconnect_sum">当耳机重新连接时恢复播放</string> <string name="pref_followQueue_sum">播放完成跳转到播放列表下一项</string> - <string name="pref_auto_delete_sum">当播放完成后删除单集</string> + <string name="pref_auto_delete_sum">当播放完成后删除曲目</string> <string name="pref_auto_delete_title">自动删除</string> <string name="playback_pref">播放</string> <string name="network_pref">网络</string> @@ -190,6 +217,8 @@ </string> <string name="pref_revokeAccess_title">撤销访问</string> <string name="pref_revokeAccess_sum">撤销访问本应用对您 Flattr 账户的访问权限.</string> + <string name="pref_auto_flattr_title">自动Flattr</string> + <string name="pref_auto_flattr_sum">设置自动 flattring</string> <string name="user_interface_label">界面</string> <string name="pref_set_theme_title">主题选择</string> <string name="pref_set_theme_sum">改变 AntennaPod 外观</string> @@ -197,6 +226,9 @@ <string name="pref_automatic_download_sum">配置自动下载的曲目</string> <string name="pref_autodl_wifi_filter_title">打开 Wi-Fi 过滤器</string> <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> @@ -212,16 +244,20 @@ <string name="pref_gpodnet_setlogin_information_sum">改变 gpodder.net 账户登录信息.</string> <string name="pref_playback_speed_title">播放速度</string> <string name="pref_playback_speed_sum">自定义音频播放速度</string> - <string name="pref_seek_delta_title">定位时间</string> - <string name="pref_seek_delta_sum">当倒退或快速回放时以这些秒为单位</string> <string name="pref_gpodnet_sethostname_title">设置主机名</string> <string name="pref_gpodnet_sethostname_use_default_host">使用默认主机</string> <string name="pref_expandNotify_title">扩展通知</string> <string name="pref_expandNotify_sum">总是扩展通知以显示播放按钮</string> <string name="pref_persistNotify_title">保持播放控制</string> <string name="pref_persistNotify_sum">在暂停时保持通知和锁屏界面的控制。</string> - <string name="pref_expand_notify_unsupport_toast">Android 版本 4.1 之前不支持扩展通知</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">启用自动 flattring</string> + <string name="auto_flattr_after_percent">当播放到百分之%d时Flattr改曲目</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> @@ -231,6 +267,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"><i>与</i> AntennaPod 一样,许多应用例如Google Mail,Dropbox,Google Drive和大多数文件管理器均可 <i>打开</i> OPML文件。</string> <string name="start_import_label">开始导入</string> <string name="opml_import_label">OPML 导入</string> <string name="opml_directory_error">错误!</string> @@ -239,6 +278,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> @@ -262,6 +303,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> @@ -317,9 +359,11 @@ <string name="new_episodes_count_label">新曲目数</string> <string name="in_progress_episodes_count_label">已收听曲目数</string> <string name="drag_handle_content_description">拖动以变更本项目的位置</string> + <string name="load_next_page_label">载入下一页</string> <!--Feed information screen--> <string name="authentication_label">验证</string> <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/arrays.xml b/core/src/main/res/values/arrays.xml index 9b9079021..4ecf2cf61 100644 --- a/core/src/main/res/values/arrays.xml +++ b/core/src/main/res/values/arrays.xml @@ -1,7 +1,16 @@ <?xml version="1.0" encoding="utf-8"?> <resources> - <string-array name="seek_delta_values"> + <string-array name="smart_mark_as_played_values"> + <item>0</item> + <item>15</item> + <item>30</item> + <item>45</item> + <item>60</item> + </string-array> + + + <integer-array name="seek_delta_values"> <item>5</item> <item>10</item> <item>15</item> @@ -9,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> @@ -109,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> @@ -117,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 186651224..3cedfb8e5 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> @@ -98,16 +110,20 @@ <string name="stream_label">Stream</string> <string name="remove_label">Remove</string> <string name="remove_episode_lable">Remove episode</string> - <string name="mark_read_label">Mark as read</string> - <string name="mark_unread_label">Mark as unread</string> - <string name="marked_as_read_label">Marked as read</string> + <string name="mark_read_label">Mark as played</string> + <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\nYou can choose to either only add the episode to the queue or you can allow downloading temporarily.\n\n<small>Your choice will be remembered for 10 minutes.</small></string> + <string name="confirm_mobile_download_dialog_message">Downloading over mobile data connection is disabled in the settings.\n\nDo you want to allow downloading temporarily?\n\n<small>Your choice will be remembered for 10 minutes.</small></string> + <string name="confirm_mobile_download_dialog_only_add_to_queue">Enqueue</string> + <string name="confirm_mobile_download_dialog_enable_temporarily">Allow 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> @@ -220,6 +245,8 @@ <string name="pref_followQueue_sum">Jump to next queue item when playback completes</string> <string name="pref_auto_delete_sum">Delete episode when playback completes</string> <string name="pref_auto_delete_title">Auto Delete</string> + <string name="pref_smart_mark_as_played_sum">Mark episodes as played even if less than a certain amount of seconds of playing time is still left</string> + <string name="pref_smart_mark_as_played_title">Smart mark as played</string> <string name="playback_pref">Playback</string> <string name="network_pref">Network</string> <string name="pref_autoUpdateIntervall_title">Update interval</string> @@ -243,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> @@ -266,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> @@ -277,6 +306,8 @@ <string name="pref_expand_notify_unsupport_toast">Android versions before 4.1 do not support expanded notifications.</string> <string name="pref_queueAddToFront_sum">Add new episodes to the front of the queue.</string> <string name="pref_queueAddToFront_title">Enqueue at front.</string> + <string name="pref_smart_mark_as_played_disabled">Disabled</string> + <!-- Auto-Flattr dialog --> <string name="auto_flattr_enable">Enable automatic flattring</string> @@ -368,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"> |