diff options
author | Martin Fietz <martin.fietz@gmail.com> | 2018-10-20 21:55:44 +0200 |
---|---|---|
committer | Martin Fietz <martin.fietz@gmail.com> | 2018-10-20 21:55:44 +0200 |
commit | 12c58193800a24b575f0c32c8a9fff8c0f2466c2 (patch) | |
tree | 94e2ccf034e8f91eafc2977310d91b8108a53674 /core/src/main/java/de | |
parent | bc8e2bb3c1c557cbca72de268ab42f191ed38927 (diff) | |
parent | 4ba36b826893efbe14fce9da3126f89c218db82b (diff) | |
download | AntennaPod-12c58193800a24b575f0c32c8a9fff8c0f2466c2.zip |
Merge branch 'develop'
Diffstat (limited to 'core/src/main/java/de')
135 files changed, 2695 insertions, 2221 deletions
diff --git a/core/src/main/java/de/danoeh/antennapod/core/UpdateManager.java b/core/src/main/java/de/danoeh/antennapod/core/UpdateManager.java index 53c134598..a42d495ac 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/UpdateManager.java +++ b/core/src/main/java/de/danoeh/antennapod/core/UpdateManager.java @@ -14,7 +14,6 @@ import java.io.File; import java.util.List; import de.danoeh.antennapod.core.feed.Feed; -import de.danoeh.antennapod.core.feed.FeedImage; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.storage.DBReader; @@ -23,11 +22,11 @@ import de.danoeh.antennapod.core.storage.DBWriter; /* * This class's job is do perform maintenance tasks whenever the app has been updated */ -public class UpdateManager { +class UpdateManager { private UpdateManager(){} - public static final String TAG = UpdateManager.class.getSimpleName(); + private static final String TAG = UpdateManager.class.getSimpleName(); private static final String PREF_NAME = "app_version"; private static final String KEY_VERSION_CODE = "version_code"; @@ -57,41 +56,18 @@ public class UpdateManager { } } - public static int getStoredVersionCode() { + private static int getStoredVersionCode() { return prefs.getInt(KEY_VERSION_CODE, -1); } - public static void setCurrentVersionCode() { + private static void setCurrentVersionCode() { prefs.edit().putInt(KEY_VERSION_CODE, currentVersionCode).apply(); } private static void onUpgrade(final int oldVersionCode, final int newVersionCode) { - if(oldVersionCode < 1030099) { - // delete the now obsolete image cache - // from now on, Glide will handle caching images - new Thread() { - public void run() { - List<Feed> feeds = DBReader.getFeedList(); - for (Feed podcast : feeds) { - List<FeedItem> episodes = DBReader.getFeedItemList(podcast); - for (FeedItem episode : episodes) { - FeedImage image = episode.getImage(); - if (image != null && image.isDownloaded() && image.getFile_url() != null) { - File imageFile = new File(image.getFile_url()); - if (imageFile.exists()) { - imageFile.delete(); - } - image.setFile_url(null); // calls setDownloaded(false) - DBWriter.setFeedImage(image); - } - } - } - } - }.start(); - } if(oldVersionCode < 1050004) { if(MediaPlayer.isPrestoLibraryInstalled(context) && Build.VERSION.SDK_INT >= 16) { - UserPreferences.enableSonic(true); + UserPreferences.enableSonic(); } } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/asynctask/DBTaskLoader.java b/core/src/main/java/de/danoeh/antennapod/core/asynctask/DBTaskLoader.java deleted file mode 100644 index 0f402f44a..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/asynctask/DBTaskLoader.java +++ /dev/null @@ -1,29 +0,0 @@ -package de.danoeh.antennapod.core.asynctask; - -import android.content.Context; -import android.support.v4.content.AsyncTaskLoader; - -/** - * Subclass of AsyncTaskLoader that is made for loading data with one of the DB*-classes. - * This class will provide a useful default implementation that would otherwise always be necessary when interacting - * with the DB*-classes with an AsyncTaskLoader. - */ -public abstract class DBTaskLoader<D> extends AsyncTaskLoader<D> { - - public DBTaskLoader(Context context) { - super(context); - } - - @Override - protected void onStopLoading() { - super.onStopLoading(); - cancelLoad(); - } - - @Override - protected void onStartLoading() { - super.onStartLoading(); - // according to https://code.google.com/p/android/issues/detail?id=14944, this has to be called manually - forceLoad(); - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/asynctask/FeedRemover.java b/core/src/main/java/de/danoeh/antennapod/core/asynctask/FeedRemover.java index 67c460e78..74693cf21 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/asynctask/FeedRemover.java +++ b/core/src/main/java/de/danoeh/antennapod/core/asynctask/FeedRemover.java @@ -1,6 +1,5 @@ package de.danoeh.antennapod.core.asynctask; -import android.annotation.SuppressLint; import android.app.ProgressDialog; import android.content.Context; import android.content.Intent; @@ -10,14 +9,16 @@ import java.util.concurrent.ExecutionException; import de.danoeh.antennapod.core.R; import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.service.download.DownloadService; import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.util.IntentUtils; /** Removes a feed in the background. */ public class FeedRemover extends AsyncTask<Void, Void, Void> { - Context context; - ProgressDialog dialog; - Feed feed; + private final Context context; + private ProgressDialog dialog; + private final Feed feed; public boolean skipOnCompletion = false; public FeedRemover(Context context, Feed feed) { @@ -42,7 +43,7 @@ public class FeedRemover extends AsyncTask<Void, Void, Void> { dialog.dismiss(); } if(skipOnCompletion) { - context.sendBroadcast(new Intent(PlaybackService.ACTION_SKIP_CURRENT_EPISODE)); + IntentUtils.sendLocalBroadcast(context, PlaybackService.ACTION_SKIP_CURRENT_EPISODE); } } @@ -55,13 +56,8 @@ public class FeedRemover extends AsyncTask<Void, Void, Void> { dialog.show(); } - @SuppressLint("NewApi") public void executeAsync() { - if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { - executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } else { - execute(); - } + executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrClickWorker.java b/core/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrClickWorker.java index d991006e5..f4c99011a 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrClickWorker.java +++ b/core/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrClickWorker.java @@ -1,6 +1,5 @@ package de.danoeh.antennapod.core.asynctask; -import android.annotation.TargetApi; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; @@ -11,6 +10,7 @@ import android.support.v4.app.NotificationCompat; import android.util.Log; import android.widget.Toast; +import de.danoeh.antennapod.core.util.gui.NotificationUtils; import org.shredzone.flattr4j.exception.FlattrException; import java.util.LinkedList; @@ -39,7 +39,7 @@ import de.danoeh.antennapod.core.util.flattr.FlattrUtils; * to flattr something, a notification will be displayed. */ public class FlattrClickWorker extends AsyncTask<Void, Integer, FlattrClickWorker.ExitCode> { - protected static final String TAG = "FlattrClickWorker"; + private static final String TAG = "FlattrClickWorker"; private static final int NOTIFICATION_ID = 4; @@ -176,7 +176,7 @@ public class FlattrClickWorker extends AsyncTask<Void, Integer, FlattrClickWorke PendingIntent contentIntent = PendingIntent.getActivity(context, 0, ClientConfig.flattrCallbacks.getFlattrAuthenticationActivityIntent(context), 0); - Notification notification = new NotificationCompat.Builder(context) + Notification notification = new NotificationCompat.Builder(context, NotificationUtils.CHANNEL_ID_ERROR) .setStyle(new NotificationCompat.BigTextStyle().bigText(context.getString(R.string.no_flattr_token_notification_msg))) .setContentIntent(contentIntent) .setContentTitle(context.getString(R.string.no_flattr_token_title)) @@ -209,7 +209,7 @@ public class FlattrClickWorker extends AsyncTask<Void, Integer, FlattrClickWorke + context.getString(R.string.flattr_click_failure_count, failed); } - Notification notification = new NotificationCompat.Builder(context) + Notification notification = new NotificationCompat.Builder(context, NotificationUtils.CHANNEL_ID_ERROR) .setStyle(new NotificationCompat.BigTextStyle().bigText(subtext)) .setContentIntent(contentIntent) .setContentTitle(title) @@ -225,12 +225,7 @@ public class FlattrClickWorker extends AsyncTask<Void, Integer, FlattrClickWorke /** * Starts the FlattrClickWorker as an AsyncTask. */ - @TargetApi(11) public void executeAsync() { - if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { - executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } else { - execute(); - } + executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrStatusFetcher.java b/core/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrStatusFetcher.java index 4c084eaaf..420a60469 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrStatusFetcher.java +++ b/core/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrStatusFetcher.java @@ -18,8 +18,8 @@ import de.danoeh.antennapod.core.util.flattr.FlattrUtils; */ public class FlattrStatusFetcher extends Thread { - protected static final String TAG = "FlattrStatusFetcher"; - protected Context context; + private static final String TAG = "FlattrStatusFetcher"; + private final Context context; public FlattrStatusFetcher(Context context) { super(); diff --git a/core/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrTokenFetcher.java b/core/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrTokenFetcher.java index 2513d1abd..985cabbf8 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrTokenFetcher.java +++ b/core/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrTokenFetcher.java @@ -1,7 +1,6 @@ package de.danoeh.antennapod.core.asynctask; -import android.annotation.SuppressLint; import android.app.ProgressDialog; import android.content.Context; import android.net.Uri; @@ -23,12 +22,12 @@ import de.danoeh.antennapod.core.util.flattr.FlattrUtils; public class FlattrTokenFetcher extends AsyncTask<Void, Void, AccessToken> { private static final String TAG = "FlattrTokenFetcher"; - Context context; - AndroidAuthenticator auth; - AccessToken token; - Uri uri; - ProgressDialog dialog; - FlattrException exception; + private final Context context; + private final AndroidAuthenticator auth; + private AccessToken token; + private final Uri uri; + private ProgressDialog dialog; + private FlattrException exception; public FlattrTokenFetcher(Context context, AndroidAuthenticator auth, Uri uri) { super(); @@ -80,13 +79,8 @@ public class FlattrTokenFetcher extends AsyncTask<Void, Void, AccessToken> { } } - @SuppressLint("NewApi") public void executeAsync() { - if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { - executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } else { - execute(); - } + executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/dialog/ConfirmationDialog.java b/core/src/main/java/de/danoeh/antennapod/core/dialog/ConfirmationDialog.java index b14803751..c626a8189 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/dialog/ConfirmationDialog.java +++ b/core/src/main/java/de/danoeh/antennapod/core/dialog/ConfirmationDialog.java @@ -15,9 +15,9 @@ public abstract class ConfirmationDialog { private static final String TAG = ConfirmationDialog.class.getSimpleName(); - protected Context context; - private int titleId; - private String message; + private final Context context; + private final int titleId; + private final String message; private int positiveText; private int negativeText; @@ -32,7 +32,7 @@ public abstract class ConfirmationDialog { this.message = message; } - public void onCancelButtonPressed(DialogInterface dialog) { + private void onCancelButtonPressed(DialogInterface dialog) { Log.d(TAG, "Dialog was cancelled"); dialog.dismiss(); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/FavoritesEvent.java b/core/src/main/java/de/danoeh/antennapod/core/event/FavoritesEvent.java index d09f6802f..578007561 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/event/FavoritesEvent.java +++ b/core/src/main/java/de/danoeh/antennapod/core/event/FavoritesEvent.java @@ -11,8 +11,8 @@ public class FavoritesEvent { ADDED, REMOVED } - public final Action action; - public final FeedItem item; + private final Action action; + private final FeedItem item; private FavoritesEvent(Action action, FeedItem item) { this.action = action; diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/FeedItemEvent.java b/core/src/main/java/de/danoeh/antennapod/core/event/FeedItemEvent.java index 7ff241456..9db262857 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/event/FeedItemEvent.java +++ b/core/src/main/java/de/danoeh/antennapod/core/event/FeedItemEvent.java @@ -17,7 +17,8 @@ public class FeedItemEvent { UPDATE, DELETE_MEDIA } - @NonNull public final Action action; + @NonNull + private final Action action; @NonNull public final List<FeedItem> items; private FeedItemEvent(Action action, List<FeedItem> items) { diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/FeedMediaEvent.java b/core/src/main/java/de/danoeh/antennapod/core/event/FeedMediaEvent.java index 864d0a405..4a591c996 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/event/FeedMediaEvent.java +++ b/core/src/main/java/de/danoeh/antennapod/core/event/FeedMediaEvent.java @@ -8,8 +8,8 @@ public class FeedMediaEvent { UPDATE } - public final Action action; - public final FeedMedia media; + private final Action action; + private final FeedMedia media; private FeedMediaEvent(Action action, FeedMedia media) { this.action = action; diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/ServiceEvent.java b/core/src/main/java/de/danoeh/antennapod/core/event/ServiceEvent.java new file mode 100644 index 000000000..b3241a8b6 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/event/ServiceEvent.java @@ -0,0 +1,13 @@ +package de.danoeh.antennapod.core.event; + +public class ServiceEvent { + public enum Action { + SERVICE_STARTED + } + + public final Action action; + + public ServiceEvent(Action action) { + this.action = action; + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/export/html/HtmlSymbols.java b/core/src/main/java/de/danoeh/antennapod/core/export/html/HtmlSymbols.java index b8807a686..1ca126469 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/export/html/HtmlSymbols.java +++ b/core/src/main/java/de/danoeh/antennapod/core/export/html/HtmlSymbols.java @@ -22,7 +22,7 @@ class HtmlSymbols extends CommonSymbols { static final String ORDERED_LIST = "ol"; static final String LIST_ITEM = "li"; - static String HEADING = "h1"; + static final String HEADING = "h1"; static final String LINK = "a"; static final String LINK_DESTINATION = "href"; diff --git a/core/src/main/java/de/danoeh/antennapod/core/export/opml/OpmlSymbols.java b/core/src/main/java/de/danoeh/antennapod/core/export/opml/OpmlSymbols.java index 40b0e23b8..86091720d 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/export/opml/OpmlSymbols.java +++ b/core/src/main/java/de/danoeh/antennapod/core/export/opml/OpmlSymbols.java @@ -3,7 +3,7 @@ package de.danoeh.antennapod.core.export.opml; import de.danoeh.antennapod.core.export.CommonSymbols; /** Contains symbols for reading and writing OPML documents. */ -public final class OpmlSymbols extends CommonSymbols { +final class OpmlSymbols extends CommonSymbols { public static final String OPML = "opml"; static final String OUTLINE = "outline"; diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/Chapter.java b/core/src/main/java/de/danoeh/antennapod/core/feed/Chapter.java index f221ed32e..f3dfdfdb6 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/Chapter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/Chapter.java @@ -7,19 +7,19 @@ import de.danoeh.antennapod.core.storage.PodDBAdapter; public abstract class Chapter extends FeedComponent { /** Defines starting point in milliseconds. */ - protected long start; - protected String title; - protected String link; + long start; + String title; + String link; - public Chapter() { + Chapter() { } - public Chapter(long start) { + Chapter(long start) { super(); this.start = start; } - public Chapter(long start, String title, FeedItem item, String link) { + Chapter(long start, String title, FeedItem item, String link) { super(); this.start = start; this.title = title; 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 514a79fad..b769eaf55 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 @@ -27,8 +27,8 @@ public class EventDistributor extends Observable { public static final int DOWNLOAD_HANDLED = 64; public static final int PLAYER_STATUS_UPDATE = 128; - private Handler handler; - private AbstractQueue<Integer> events; + private final Handler handler; + private final AbstractQueue<Integer> events; private static EventDistributor instance; @@ -52,7 +52,7 @@ public class EventDistributor extends Observable { deleteObserver(el); } - public void addEvent(Integer i) { + private void addEvent(Integer i) { events.offer(i); handler.post(EventDistributor.this::processEventQueue); } 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 746dd43c4..3395653f3 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 @@ -44,7 +44,7 @@ public class Feed extends FeedFile implements FlattrThing, ImageResource { * Name of the author */ private String author; - private FeedImage image; + private String imageUrl; private List<FeedItem> items; /** @@ -96,7 +96,7 @@ public class Feed extends FeedFile implements FlattrThing, ImageResource { * This constructor is used for restoring a feed from the database. */ public Feed(long id, String lastUpdate, String title, String customTitle, String link, String description, String paymentLink, - String author, String language, String type, String feedIdentifier, FeedImage image, String fileUrl, + String author, String language, String type, String feedIdentifier, String imageUrl, String fileUrl, String downloadUrl, boolean downloaded, FlattrStatus status, boolean paged, String nextPageLink, String filter, boolean lastUpdateFailed) { super(fileUrl, downloadUrl, downloaded); @@ -111,7 +111,7 @@ public class Feed extends FeedFile implements FlattrThing, ImageResource { this.language = language; this.type = type; this.feedIdentifier = feedIdentifier; - this.image = image; + this.imageUrl = imageUrl; this.flattrStatus = status; this.paged = paged; this.nextPageLink = nextPageLink; @@ -128,9 +128,9 @@ public class Feed extends FeedFile implements FlattrThing, ImageResource { * This constructor is used for test purposes and uses a default flattr status object. */ public Feed(long id, String lastUpdate, String title, String link, String description, String paymentLink, - String author, String language, String type, String feedIdentifier, FeedImage image, String fileUrl, + String author, String language, String type, String feedIdentifier, String imageUrl, String fileUrl, String downloadUrl, boolean downloaded) { - this(id, lastUpdate, title, null, link, description, paymentLink, author, language, type, feedIdentifier, image, + this(id, lastUpdate, title, null, link, description, paymentLink, author, language, type, feedIdentifier, imageUrl, fileUrl, downloadUrl, downloaded, new FlattrStatus(), false, null, null, false); } @@ -191,6 +191,7 @@ public class Feed extends FeedFile implements FlattrThing, ImageResource { int indexNextPageLink = cursor.getColumnIndex(PodDBAdapter.KEY_NEXT_PAGE_LINK); int indexHide = cursor.getColumnIndex(PodDBAdapter.KEY_HIDE); int indexLastUpdateFailed = cursor.getColumnIndex(PodDBAdapter.KEY_LAST_UPDATE_FAILED); + int indexImageUrl = cursor.getColumnIndex(PodDBAdapter.KEY_IMAGE_URL); Feed feed = new Feed( cursor.getLong(indexId), @@ -204,7 +205,7 @@ public class Feed extends FeedFile implements FlattrThing, ImageResource { cursor.getString(indexLanguage), cursor.getString(indexType), cursor.getString(indexFeedIdentifier), - null, + cursor.getString(indexImageUrl), cursor.getString(indexFileUrl), cursor.getString(indexDownloadUrl), cursor.getInt(indexDownloaded) > 0, @@ -266,8 +267,8 @@ public class Feed extends FeedFile implements FlattrThing, ImageResource { public void updateFromOther(Feed other) { // don't update feed's download_url, we do that manually if redirected // see AntennapodHttpClient - if (other.image != null) { - this.image = other.image; + if (other.imageUrl != null) { + this.imageUrl = other.imageUrl; } if (other.feedTitle != null) { feedTitle = other.feedTitle; @@ -305,8 +306,10 @@ public class Feed extends FeedFile implements FlattrThing, ImageResource { if (super.compareWithOther(other)) { return true; } - if(other.image != null && !TextUtils.equals(image.download_url, other.image.download_url)) { - return true; + if (other.imageUrl != null) { + if (imageUrl == null || !TextUtils.equals(imageUrl, other.imageUrl)) { + return true; + } } if (!TextUtils.equals(feedTitle, other.feedTitle)) { return true; @@ -409,12 +412,12 @@ public class Feed extends FeedFile implements FlattrThing, ImageResource { this.description = description; } - public FeedImage getImage() { - return image; + public String getImageUrl() { + return imageUrl; } - public void setImage(FeedImage image) { - this.image = image; + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; } public List<FeedItem> getItems() { @@ -503,11 +506,7 @@ public class Feed extends FeedFile implements FlattrThing, ImageResource { @Override public String getImageLocation() { - if (image != null) { - return image.getImageLocation(); - } else { - return null; - } + return imageUrl; } public int getPageNr() { diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedComponent.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedComponent.java index 90b5e50b7..a3f91b1c9 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedComponent.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedComponent.java @@ -7,9 +7,9 @@ package de.danoeh.antennapod.core.feed; */ public abstract class FeedComponent { - protected long id; + long id; - public FeedComponent() { + FeedComponent() { super(); } @@ -26,7 +26,7 @@ public abstract class FeedComponent { * FeedComponent. This method should only update attributes which where read from * the feed. */ - public void updateFromOther(FeedComponent other) { + void updateFromOther(FeedComponent other) { } /** @@ -36,7 +36,7 @@ public abstract class FeedComponent { * * @return true if attribute values are different, false otherwise */ - public boolean compareWithOther(FeedComponent other) { + boolean compareWithOther(FeedComponent other) { return false; } 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 index d04d236e4..b790faadf 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedEvent.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedEvent.java @@ -9,7 +9,7 @@ public class FeedEvent { FILTER_CHANGED } - public final Action action; + private final Action action; public final long feedId; public FeedEvent(Action action, long feedId) { diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedFile.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedFile.java index ca9af058b..cc4dd230f 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedFile.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedFile.java @@ -9,9 +9,9 @@ import java.io.File; */ public abstract class FeedFile extends FeedComponent { - protected String file_url; + String file_url; protected String download_url; - protected boolean downloaded; + boolean downloaded; /** * Creates a new FeedFile object. @@ -40,7 +40,7 @@ public abstract class FeedFile extends FeedComponent { * FeedFile. This method should only update attributes which where read from * the feed. */ - public void updateFromOther(FeedFile other) { + void updateFromOther(FeedFile other) { super.updateFromOther(other); this.download_url = other.download_url; } @@ -52,7 +52,7 @@ public abstract class FeedFile extends FeedComponent { * * @return true if attribute values are different, false otherwise */ - public boolean compareWithOther(FeedFile other) { + boolean compareWithOther(FeedFile other) { if (super.compareWithOther(other)) { return true; } diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedFilter.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedFilter.java index 35abb8de6..28161ac9b 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedFilter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedFilter.java @@ -9,8 +9,8 @@ public class FeedFilter { private static final String TAG = "FeedFilter"; - private String includeFilter; - private String excludeFilter; + private final String includeFilter; + private final String excludeFilter; public FeedFilter() { this("", ""); diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedImage.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedImage.java deleted file mode 100644 index f0c508830..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedImage.java +++ /dev/null @@ -1,92 +0,0 @@ -package de.danoeh.antennapod.core.feed; - -import android.database.Cursor; - -import java.io.File; - -import de.danoeh.antennapod.core.asynctask.ImageResource; -import de.danoeh.antennapod.core.storage.PodDBAdapter; - - -public class FeedImage extends FeedFile implements ImageResource { - public static final int FEEDFILETYPE_FEEDIMAGE = 1; - - protected String title; - protected FeedComponent owner; - - public FeedImage(FeedComponent owner, String download_url, String title) { - super(null, download_url, false); - this.download_url = download_url; - this.title = title; - this.owner = owner; - } - - public FeedImage(long id, String title, String file_url, - String download_url, boolean downloaded) { - super(file_url, download_url, downloaded); - this.id = id; - this.title = title; - } - - public FeedImage() { - super(); - } - - public static FeedImage fromCursor(Cursor cursor) { - int indexId = cursor.getColumnIndex(PodDBAdapter.KEY_ID); - int indexTitle = cursor.getColumnIndex(PodDBAdapter.KEY_TITLE); - int indexFileUrl = cursor.getColumnIndex(PodDBAdapter.KEY_FILE_URL); - int indexDownloadUrl = cursor.getColumnIndex(PodDBAdapter.KEY_DOWNLOAD_URL); - int indexDownloaded = cursor.getColumnIndex(PodDBAdapter.KEY_DOWNLOADED); - - return new FeedImage( - cursor.getLong(indexId), - cursor.getString(indexTitle), - cursor.getString(indexFileUrl), - cursor.getString(indexDownloadUrl), - cursor.getInt(indexDownloaded) > 0 - ); - } - - - @Override - public String getHumanReadableIdentifier() { - if (owner != null && owner.getHumanReadableIdentifier() != null) { - return owner.getHumanReadableIdentifier(); - } else { - return download_url; - } - } - - @Override - public int getTypeAsInt() { - return FEEDFILETYPE_FEEDIMAGE; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public FeedComponent getOwner() { - return owner; - } - - public void setOwner(FeedComponent owner) { - this.owner = owner; - } - - @Override - public String getImageLocation() { - if (file_url != null && downloaded) { - return new File(file_url).getAbsolutePath(); - } else if(download_url != null) { - return download_url; - } else { - return null; - } - } -} 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 d497d4949..b0a87c885 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java @@ -1,9 +1,10 @@ package de.danoeh.antennapod.core.feed; import android.database.Cursor; -import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.text.TextUtils; +import de.danoeh.antennapod.core.asynctask.ImageResource; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; @@ -14,7 +15,6 @@ import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; -import de.danoeh.antennapod.core.asynctask.ImageResource; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.PodDBAdapter; import de.danoeh.antennapod.core.util.ShownotesProvider; @@ -60,7 +60,7 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr public static final int PLAYED = 1; private String paymentLink; - private FlattrStatus flattrStatus; + private final FlattrStatus flattrStatus; /** * Is true if the database contains any chapters that belong to this item. This attribute is only @@ -75,7 +75,7 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr * in the database. The 'hasChapters' attribute should be used to check if this item has any chapters. * */ private List<Chapter> chapters; - private FeedImage image; + private String imageUrl; /* * 0: auto download disabled @@ -88,7 +88,7 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr /** * Any tags assigned to this item */ - private Set<String> tags = new HashSet<>(); + private final Set<String> tags = new HashSet<>(); public FeedItem() { this.state = UNPLAYED; @@ -100,7 +100,7 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr * This constructor is used by DBReader. * */ public FeedItem(long id, String title, String link, Date pubDate, String paymentLink, long feedId, - FlattrStatus flattrStatus, boolean hasChapters, FeedImage image, int state, + FlattrStatus flattrStatus, boolean hasChapters, String imageUrl, int state, String itemIdentifier, long autoDownload) { this.id = id; this.title = title; @@ -110,7 +110,7 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr this.feedId = feedId; this.flattrStatus = flattrStatus; this.hasChapters = hasChapters; - this.image = image; + this.imageUrl = imageUrl; this.state = state; this.itemIdentifier = itemIdentifier; this.autoDownload = autoDownload; @@ -158,9 +158,9 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr int indexRead = cursor.getColumnIndex(PodDBAdapter.KEY_READ); int indexItemIdentifier = cursor.getColumnIndex(PodDBAdapter.KEY_ITEM_IDENTIFIER); int indexAutoDownload = cursor.getColumnIndex(PodDBAdapter.KEY_AUTO_DOWNLOAD); + int indexImageUrl = cursor.getColumnIndex(PodDBAdapter.KEY_IMAGE_URL); long id = cursor.getInt(indexId); - assert(id > 0); String title = cursor.getString(indexTitle); String link = cursor.getString(indexLink); Date pubDate = new Date(cursor.getLong(indexPubDate)); @@ -171,15 +171,16 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr int state = cursor.getInt(indexRead); String itemIdentifier = cursor.getString(indexItemIdentifier); long autoDownload = cursor.getLong(indexAutoDownload); + String imageUrl = cursor.getString(indexImageUrl); return new FeedItem(id, title, link, pubDate, paymentLink, feedId, flattrStatus, - hasChapters, null, state, itemIdentifier, autoDownload); + hasChapters, imageUrl, state, itemIdentifier, autoDownload); } public void updateFromOther(FeedItem other) { super.updateFromOther(other); - if (other.image != null) { - this.image = other.image; + if (other.imageUrl != null) { + this.imageUrl = other.imageUrl; } if (other.title != null) { title = other.title; @@ -213,9 +214,6 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr chapters = other.chapters; } } - if (image == null) { - image = other.image; - } } /** @@ -374,7 +372,15 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr if (contentEncoded == null || description == null) { DBReader.loadExtraInformationOfFeedItem(FeedItem.this); } - return (contentEncoded != null) ? contentEncoded : description; + if (TextUtils.isEmpty(contentEncoded)) { + return description; + } else if (TextUtils.isEmpty(description)) { + return contentEncoded; + } else if (description.length() > 1.25 * contentEncoded.length()) { + return description; + } else { + return contentEncoded; + } }; } @@ -382,8 +388,8 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr public String getImageLocation() { if(media != null && media.hasEmbeddedPicture()) { return media.getImageLocation(); - } else if (image != null) { - return image.getImageLocation(); + } else if (imageUrl != null) { + return imageUrl; } else if (feed != null) { return feed.getImageLocation(); } else { @@ -419,29 +425,12 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr * Returns the image of this item or the image of the feed if this item does * not have its own image. */ - public FeedImage getImage() { - return (hasItemImage()) ? image : feed.getImage(); - } - - public void setImage(FeedImage image) { - this.image = image; - if (image != null) { - image.setOwner(this); - } + public String getImageUrl() { + return (imageUrl != null) ? imageUrl : feed.getImageUrl(); } - /** - * Returns true if this FeedItem has its own image, false otherwise. - */ - public boolean hasItemImage() { - return image != null; - } - - /** - * Returns true if this FeedItem has its own image and the image has been downloaded. - */ - public boolean hasItemImageDownloaded() { - return image != null && image.isDownloaded(); + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; } @Override diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilter.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilter.java index 200153876..719383d23 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilter.java @@ -8,6 +8,8 @@ import java.util.List; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.util.LongList; +import static de.danoeh.antennapod.core.feed.FeedItem.TAG_FAVORITE; + public class FeedItemFilter { private final String[] mProperties; @@ -19,6 +21,7 @@ public class FeedItemFilter { private boolean showDownloaded = false; private boolean showNotDownloaded = false; private boolean showHasMedia = false; + private boolean showIsFavorite = false; public FeedItemFilter(String properties) { this(TextUtils.split(properties, ",")); @@ -53,6 +56,9 @@ public class FeedItemFilter { case "has_media": showHasMedia = true; break; + case "is_favorite": + showIsFavorite = true; + break; } } } @@ -88,6 +94,8 @@ public class FeedItemFilter { if (showHasMedia && !item.hasMedia()) continue; + if (showIsFavorite && !item.isTagged(TAG_FAVORITE)) continue; + // If the item reaches here, it meets all criteria result.add(item); } 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 5eea4f3da..73d2bb34d 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 @@ -19,6 +19,7 @@ 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.playback.PlaybackService; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.storage.DBWriter; @@ -34,7 +35,7 @@ public class FeedMedia extends FeedFile implements Playable { public static final int PLAYABLE_TYPE_FEEDMEDIA = 1; public static final String PREF_MEDIA_ID = "FeedMedia.PrefMediaId"; - public static final String PREF_FEED_ID = "FeedMedia.PrefFeedId"; + private static final String PREF_FEED_ID = "FeedMedia.PrefFeedId"; /** * Indicates we've checked on the size of the item via the network @@ -88,10 +89,10 @@ public class FeedMedia extends FeedFile implements Playable { this.lastPlayedTime = lastPlayedTime; } - public FeedMedia(long id, FeedItem item, int duration, int position, - long size, String mime_type, String file_url, String download_url, - boolean downloaded, Date playbackCompletionDate, int played_duration, - Boolean hasEmbeddedPicture, long lastPlayedTime) { + private FeedMedia(long id, FeedItem item, int duration, int position, + long size, String mime_type, String file_url, String download_url, + boolean downloaded, Date playbackCompletionDate, int played_duration, + Boolean hasEmbeddedPicture, long lastPlayedTime) { this(id, item, duration, position, size, mime_type, file_url, download_url, downloaded, playbackCompletionDate, played_duration, lastPlayedTime); this.hasEmbeddedPicture = hasEmbeddedPicture; @@ -218,7 +219,7 @@ public class FeedMedia extends FeedFile implements Playable { * currently being played and the current player status is playing. */ public boolean isCurrentlyPlaying() { - return isPlaying() && + return isPlaying() && PlaybackService.isRunning && ((PlaybackPreferences.getCurrentPlayerStatus() == PlaybackPreferences.PLAYER_STATUS_PLAYING)); } @@ -554,15 +555,9 @@ public class FeedMedia extends FeedFile implements Playable { public Callable<String> loadShownotes() { return () -> { if (item == null) { - item = DBReader.getFeedItem( - itemID); - } - if (item.getContentEncoded() == null || item.getDescription() == null) { - DBReader.loadExtraInformationOfFeedItem( - item); - + item = DBReader.getFeedItem(itemID); } - return (item.getContentEncoded() != null) ? item.getContentEncoded() : item.getDescription(); + return item.loadShownotes().call(); }; } diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedPreferences.java index 66fc4024b..3285ad7cb 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedPreferences.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedPreferences.java @@ -33,7 +33,7 @@ public class FeedPreferences { this(feedID, autoDownload, true, auto_delete_action, username, password, new FeedFilter()); } - public FeedPreferences(long feedID, boolean autoDownload, boolean keepUpdated, AutoDeleteAction auto_delete_action, String username, String password, @NonNull FeedFilter filter) { + private FeedPreferences(long feedID, boolean autoDownload, boolean keepUpdated, AutoDeleteAction auto_delete_action, String username, String password, @NonNull FeedFilter filter) { this.feedID = feedID; this.autoDownload = autoDownload; this.keepUpdated = keepUpdated; diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/SearchResult.java b/core/src/main/java/de/danoeh/antennapod/core/feed/SearchResult.java index 9aa8d3170..ea8eb7871 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/SearchResult.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/SearchResult.java @@ -1,11 +1,11 @@ package de.danoeh.antennapod.core.feed; public class SearchResult { - private FeedComponent component; + private final FeedComponent component; /** Additional information (e.g. where it was found) */ private String subtitle; /** Higher value means more importance */ - private int value; + private final int value; public SearchResult(FeedComponent component, int value, String subtitle) { super(); diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/VorbisCommentChapter.java b/core/src/main/java/de/danoeh/antennapod/core/feed/VorbisCommentChapter.java index 5b54a2d59..5ab9868a6 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/VorbisCommentChapter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/VorbisCommentChapter.java @@ -1,9 +1,9 @@ package de.danoeh.antennapod.core.feed; -import de.danoeh.antennapod.core.util.vorbiscommentreader.VorbisCommentReaderException; - import java.util.concurrent.TimeUnit; +import de.danoeh.antennapod.core.util.vorbiscommentreader.VorbisCommentReaderException; + public class VorbisCommentChapter extends Chapter { public static final int CHAPTERTYPE_VORBISCOMMENT_CHAPTER = 3; diff --git a/core/src/main/java/de/danoeh/antennapod/core/glide/ApOkHttpUrlLoader.java b/core/src/main/java/de/danoeh/antennapod/core/glide/ApOkHttpUrlLoader.java index 8ca9faa0d..3e4f06a12 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/glide/ApOkHttpUrlLoader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/glide/ApOkHttpUrlLoader.java @@ -27,7 +27,7 @@ import okhttp3.Response; /** * @see com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader */ -public class ApOkHttpUrlLoader implements ModelLoader<String, InputStream> { +class ApOkHttpUrlLoader implements ModelLoader<String, InputStream> { private static final String TAG = ApOkHttpUrlLoader.class.getSimpleName(); @@ -37,7 +37,7 @@ public class ApOkHttpUrlLoader implements ModelLoader<String, InputStream> { public static class Factory implements ModelLoaderFactory<String, InputStream> { private static volatile OkHttpClient internalClient; - private OkHttpClient client; + private final OkHttpClient client; private static OkHttpClient getInternalClient() { if (internalClient == null) { @@ -80,7 +80,7 @@ public class ApOkHttpUrlLoader implements ModelLoader<String, InputStream> { private final OkHttpClient client; - public ApOkHttpUrlLoader(OkHttpClient client) { + private ApOkHttpUrlLoader(OkHttpClient client) { this.client = client; } diff --git a/core/src/main/java/de/danoeh/antennapod/core/glide/AudioCoverFetcher.java b/core/src/main/java/de/danoeh/antennapod/core/glide/AudioCoverFetcher.java index 48dadc492..8159a1b3e 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/glide/AudioCoverFetcher.java +++ b/core/src/main/java/de/danoeh/antennapod/core/glide/AudioCoverFetcher.java @@ -1,7 +1,6 @@ package de.danoeh.antennapod.core.glide; import android.media.MediaMetadataRetriever; -import android.util.Log; import com.bumptech.glide.Priority; import com.bumptech.glide.load.data.DataFetcher; 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 4459fbd08..3af5e9080 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetService.java @@ -1,6 +1,7 @@ package de.danoeh.antennapod.core.gpoddernet; import android.support.annotation.NonNull; + import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; diff --git a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceBadStatusCodeException.java b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceBadStatusCodeException.java index 16f01f0f4..84c085ed2 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceBadStatusCodeException.java +++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceBadStatusCodeException.java @@ -1,7 +1,7 @@ package de.danoeh.antennapod.core.gpoddernet; -public class GpodnetServiceBadStatusCodeException extends GpodnetServiceException { - int statusCode; +class GpodnetServiceBadStatusCodeException extends GpodnetServiceException { + private final int statusCode; public GpodnetServiceBadStatusCodeException(String message, int statusCode) { super(message); diff --git a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceException.java b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceException.java index ce704f7e3..78ddfc945 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceException.java +++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceException.java @@ -2,10 +2,10 @@ package de.danoeh.antennapod.core.gpoddernet; public class GpodnetServiceException extends Exception { - public GpodnetServiceException() { + GpodnetServiceException() { } - public GpodnetServiceException(String message) { + GpodnetServiceException(String message) { super(message); } @@ -13,7 +13,7 @@ public class GpodnetServiceException extends Exception { super(cause); } - public GpodnetServiceException(String message, Throwable cause) { + GpodnetServiceException(String message, Throwable cause) { super(message, cause); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetDevice.java b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetDevice.java index 79eb33cb5..faf4264e5 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetDevice.java +++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetDevice.java @@ -4,10 +4,10 @@ import android.support.annotation.NonNull; public class GpodnetDevice { - private String id; - private String caption; - private DeviceType type; - private int subscriptions; + private final String id; + private final String caption; + private final DeviceType type; + private final int subscriptions; public GpodnetDevice(@NonNull String id, String caption, diff --git a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetEpisodeAction.java b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetEpisodeAction.java index 9627ecae6..b76988fd8 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetEpisodeAction.java +++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetEpisodeAction.java @@ -131,7 +131,7 @@ public class GpodnetEpisodeAction { return this.action; } - public String getActionString() { + private String getActionString() { return this.action.name().toLowerCase(); } @@ -199,16 +199,14 @@ public class GpodnetEpisodeAction { } 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(); + return this.podcast + "\t" + + this.episode + "\t" + + this.deviceId + "\t" + + this.action + "\t" + + this.timestamp.getTime() + "\t" + + String.valueOf(this.started) + "\t" + + String.valueOf(this.position) + "\t" + + String.valueOf(this.total); } /** 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 index 03c33c9a1..b6efab016 100644 --- 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 @@ -21,9 +21,9 @@ public class GpodnetEpisodeActionPostResponse { * 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; + private final Map<String, String> updatedUrls; - public GpodnetEpisodeActionPostResponse(long timestamp, Map<String, String> updatedUrls) { + private GpodnetEpisodeActionPostResponse(long timestamp, Map<String, String> updatedUrls) { this.timestamp = timestamp; this.updatedUrls = updatedUrls; } diff --git a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetPodcast.java b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetPodcast.java index 191c0fa39..680dc1042 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetPodcast.java +++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetPodcast.java @@ -3,13 +3,13 @@ package de.danoeh.antennapod.core.gpoddernet.model; import android.support.annotation.NonNull; public class GpodnetPodcast { - private String url; - private String title; - private String description; - private int subscribers; - private String logoUrl; - private String website; - private String mygpoLink; + private final String url; + private final String title; + private final String description; + private final int subscribers; + private final String logoUrl; + private final String website; + private final String mygpoLink; public GpodnetPodcast(@NonNull String url, @NonNull String title, diff --git a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetSubscriptionChange.java b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetSubscriptionChange.java index 6cc9b79a3..0f1961bef 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetSubscriptionChange.java +++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetSubscriptionChange.java @@ -5,9 +5,9 @@ import android.support.annotation.NonNull; import java.util.List; public class GpodnetSubscriptionChange { - private List<String> added; - private List<String> removed; - private long timestamp; + private final List<String> added; + private final List<String> removed; + private final long timestamp; public GpodnetSubscriptionChange(@NonNull List<String> added, @NonNull List<String> removed, diff --git a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetTag.java b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetTag.java index 42a31afc5..40543592e 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetTag.java +++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetTag.java @@ -16,7 +16,7 @@ public class GpodnetTag implements Parcelable { this.usage = usage; } - protected GpodnetTag(Parcel in) { + private GpodnetTag(Parcel in) { title = in.readString(); tag = in.readString(); usage = in.readInt(); diff --git a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetUploadChangesResponse.java b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetUploadChangesResponse.java index 9bd1881e4..9f9c3bd74 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetUploadChangesResponse.java +++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetUploadChangesResponse.java @@ -22,9 +22,9 @@ public class GpodnetUploadChangesResponse { * 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; + private final Map<String, String> updatedUrls; - public GpodnetUploadChangesResponse(long timestamp, Map<String, String> updatedUrls) { + private GpodnetUploadChangesResponse(long timestamp, Map<String, String> updatedUrls) { this.timestamp = timestamp; this.updatedUrls = updatedUrls; } 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 1e7ee0f11..5b17dd338 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 @@ -7,6 +7,7 @@ import android.util.Log; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -28,26 +29,26 @@ public class GpodnetPreferences { private static final String TAG = "GpodnetPreferences"; private static final String PREF_NAME = "gpodder.net"; - public static final String PREF_GPODNET_USERNAME = "de.danoeh.antennapod.preferences.gpoddernet.username"; - public static final String PREF_GPODNET_PASSWORD = "de.danoeh.antennapod.preferences.gpoddernet.password"; - public static final String PREF_GPODNET_DEVICEID = "de.danoeh.antennapod.preferences.gpoddernet.deviceID"; - public static final String PREF_GPODNET_HOSTNAME = "prefGpodnetHostname"; + private static final String PREF_GPODNET_USERNAME = "de.danoeh.antennapod.preferences.gpoddernet.username"; + private static final String PREF_GPODNET_PASSWORD = "de.danoeh.antennapod.preferences.gpoddernet.password"; + private static final String PREF_GPODNET_DEVICEID = "de.danoeh.antennapod.preferences.gpoddernet.deviceID"; + private static final String PREF_GPODNET_HOSTNAME = "prefGpodnetHostname"; - 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 final String PREF_LAST_SUBSCRIPTION_SYNC_TIMESTAMP = "de.danoeh.antennapod.preferences.gpoddernet.last_sync_timestamp"; + private static final String PREF_LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP = "de.danoeh.antennapod.preferences.gpoddernet.last_episode_actions_sync_timestamp"; + private static final String PREF_SYNC_ADDED = "de.danoeh.antennapod.preferences.gpoddernet.sync_added"; + private static final String PREF_SYNC_REMOVED = "de.danoeh.antennapod.preferences.gpoddernet.sync_removed"; + private static final String PREF_SYNC_EPISODE_ACTIONS = "de.danoeh.antennapod.preferences.gpoddernet.sync_queued_episode_actions"; public static final String PREF_LAST_SYNC_ATTEMPT_TIMESTAMP = "de.danoeh.antennapod.preferences.gpoddernet.last_sync_attempt_timestamp"; - public static final String PREF_LAST_SYNC_ATTEMPT_RESULT = "de.danoeh.antennapod.preferences.gpoddernet.last_sync_attempt_result"; + private static final String PREF_LAST_SYNC_ATTEMPT_RESULT = "de.danoeh.antennapod.preferences.gpoddernet.last_sync_attempt_result"; private static String username; private static String password; private static String deviceID; private static String hostname; - private static ReentrantLock feedListLock = new ReentrantLock(); + private static final ReentrantLock feedListLock = new ReentrantLock(); private static Set<String> addedFeeds; private static Set<String> removedFeeds; @@ -318,9 +319,7 @@ public class GpodnetPreferences { private static Set<String> readListFromString(String s) { Set<String> result = new HashSet<>(); - for (String item : s.split(" ")) { - result.add(item); - } + Collections.addAll(result, s.split(" ")); return result; } 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 a3eaf187e..5eec32ebc 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 @@ -1,17 +1,21 @@ package de.danoeh.antennapod.core.preferences; -import android.app.AlarmManager; -import android.app.PendingIntent; import android.content.Context; -import android.content.Intent; import android.content.SharedPreferences; -import android.os.SystemClock; import android.preference.PreferenceManager; +import android.support.annotation.IntRange; import android.support.annotation.NonNull; import android.support.v4.app.NotificationCompat; import android.text.TextUtils; import android.util.Log; - +import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.core.service.download.ProxyConfig; +import de.danoeh.antennapod.core.storage.APCleanupAlgorithm; +import de.danoeh.antennapod.core.storage.APNullCleanupAlgorithm; +import de.danoeh.antennapod.core.storage.APQueueCleanupAlgorithm; +import de.danoeh.antennapod.core.storage.EpisodeCleanupAlgorithm; +import de.danoeh.antennapod.core.util.Converter; +import de.danoeh.antennapod.core.util.download.AutoUpdateManager; import org.json.JSONArray; import org.json.JSONException; @@ -20,19 +24,9 @@ import java.io.IOException; import java.net.Proxy; import java.util.ArrayList; import java.util.Arrays; -import java.util.Calendar; import java.util.List; import java.util.concurrent.TimeUnit; -import de.danoeh.antennapod.core.R; -import de.danoeh.antennapod.core.receiver.FeedUpdateReceiver; -import de.danoeh.antennapod.core.service.download.ProxyConfig; -import de.danoeh.antennapod.core.storage.APCleanupAlgorithm; -import de.danoeh.antennapod.core.storage.APNullCleanupAlgorithm; -import de.danoeh.antennapod.core.storage.APQueueCleanupAlgorithm; -import de.danoeh.antennapod.core.storage.EpisodeCleanupAlgorithm; -import de.danoeh.antennapod.core.util.Converter; - /** * Provides access to preferences set by the user in the settings screen. A * private instance of this class must first be instantiated via @@ -42,43 +36,44 @@ import de.danoeh.antennapod.core.util.Converter; public class UserPreferences { private UserPreferences(){} - public static final String IMPORT_DIR = "import/"; + private static final String IMPORT_DIR = "import/"; private static final String TAG = "UserPreferences"; // User Interface public static final String PREF_THEME = "prefTheme"; public static final String PREF_HIDDEN_DRAWER_ITEMS = "prefHiddenDrawerItems"; - public static final String PREF_DRAWER_FEED_ORDER = "prefDrawerFeedOrder"; - public static final String PREF_DRAWER_FEED_COUNTER = "prefDrawerFeedIndicator"; + private static final String PREF_DRAWER_FEED_ORDER = "prefDrawerFeedOrder"; + private static final String PREF_DRAWER_FEED_COUNTER = "prefDrawerFeedIndicator"; public static final String PREF_EXPANDED_NOTIFICATION = "prefExpandNotify"; - public static final String PREF_PERSISTENT_NOTIFICATION = "prefPersistNotify"; + private static final String PREF_PERSISTENT_NOTIFICATION = "prefPersistNotify"; public static final String PREF_COMPACT_NOTIFICATION_BUTTONS = "prefCompactNotificationButtons"; public static final String PREF_LOCKSCREEN_BACKGROUND = "prefLockscreenBackground"; - public static final String PREF_SHOW_DOWNLOAD_REPORT = "prefShowDownloadReport"; + private static final String PREF_SHOW_DOWNLOAD_REPORT = "prefShowDownloadReport"; // Queue - public static final String PREF_QUEUE_ADD_TO_FRONT = "prefQueueAddToFront"; + private 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_UNPAUSE_ON_BLUETOOTH_RECONNECT = "prefUnpauseOnBluetoothReconnect"; - public static final String PREF_HARDWARE_FOWARD_BUTTON_SKIPS = "prefHardwareForwardButtonSkips"; - public static final String PREF_HARDWARE_PREVIOUS_BUTTON_RESTARTS = "prefHardwarePreviousButtonRestarts"; + private static final String PREF_UNPAUSE_ON_BLUETOOTH_RECONNECT = "prefUnpauseOnBluetoothReconnect"; + private static final String PREF_HARDWARE_FOWARD_BUTTON_SKIPS = "prefHardwareForwardButtonSkips"; + private static final String PREF_HARDWARE_PREVIOUS_BUTTON_RESTARTS = "prefHardwarePreviousButtonRestarts"; public static final String PREF_FOLLOW_QUEUE = "prefFollowQueue"; - public static final String PREF_SKIP_KEEPS_EPISODE = "prefSkipKeepsEpisode"; - public static final String PREF_FAVORITE_KEEPS_EPISODE = "prefFavoriteKeepsEpisode"; - public static final String PREF_AUTO_DELETE = "prefAutoDelete"; + private static final String PREF_SKIP_KEEPS_EPISODE = "prefSkipKeepsEpisode"; + private static final String PREF_FAVORITE_KEEPS_EPISODE = "prefFavoriteKeepsEpisode"; + private static final String PREF_AUTO_DELETE = "prefAutoDelete"; public static final String PREF_SMART_MARK_AS_PLAYED_SECS = "prefSmartMarkAsPlayedSecs"; - public 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"; + private static final String PREF_PLAYBACK_SPEED_ARRAY = "prefPlaybackSpeedArray"; + private static final String PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS = "prefPauseForFocusLoss"; + private static final String PREF_RESUME_AFTER_CALL = "prefResumeAfterCall"; + public static final String PREF_VIDEO_BEHAVIOR = "prefVideoBehavior"; // Network - public static final String PREF_ENQUEUE_DOWNLOADED = "prefEnqueueDownloaded"; + private static final String PREF_ENQUEUE_DOWNLOADED = "prefEnqueueDownloaded"; public static final String PREF_UPDATE_INTERVAL = "prefAutoUpdateIntervall"; - public static final String PREF_MOBILE_UPDATE = "prefMobileUpdate"; + private static final String PREF_MOBILE_UPDATE = "prefMobileUpdate"; public static final String PREF_EPISODE_CLEANUP = "prefEpisodeCleanup"; public static final String PREF_PARALLEL_DOWNLOADS = "prefParallelDownloads"; public static final String PREF_EPISODE_CACHE_SIZE = "prefEpisodeCacheSize"; @@ -86,36 +81,35 @@ public class UserPreferences { 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_ENABLE_AUTODL_ON_MOBILE = "prefEnableAutoDownloadOnMobile"; - public static final String PREF_AUTODL_SELECTED_NETWORKS = "prefAutodownloadSelectedNetworks"; - public static final String PREF_PROXY_TYPE = "prefProxyType"; - public static final String PREF_PROXY_HOST = "prefProxyHost"; - public static final String PREF_PROXY_PORT = "prefProxyPort"; - public static final String PREF_PROXY_USER = "prefProxyUser"; - public static final String PREF_PROXY_PASSWORD = "prefProxyPassword"; + private static final String PREF_AUTODL_SELECTED_NETWORKS = "prefAutodownloadSelectedNetworks"; + private static final String PREF_PROXY_TYPE = "prefProxyType"; + private static final String PREF_PROXY_HOST = "prefProxyHost"; + private static final String PREF_PROXY_PORT = "prefProxyPort"; + private static final String PREF_PROXY_USER = "prefProxyUser"; + private static final String PREF_PROXY_PASSWORD = "prefProxyPassword"; // 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_GPODNET_NOTIFICATIONS = "pref_gpodnet_notifications"; + private static final String PREF_AUTO_FLATTR = "pref_auto_flattr"; + private static final String PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD = "prefAutoFlattrPlayedDurationThreshold"; + private static final String PREF_GPODNET_NOTIFICATIONS = "pref_gpodnet_notifications"; // Other - public static final String PREF_DATA_FOLDER = "prefDataFolder"; + private static final String PREF_DATA_FOLDER = "prefDataFolder"; public static final String PREF_IMAGE_CACHE_SIZE = "prefImageCacheSize"; // Mediaplayer - public static final String PREF_PLAYBACK_SPEED = "prefPlaybackSpeed"; + public static final String PREF_MEDIA_PLAYER = "prefMediaPlayer"; + private 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"; - public static final String IMAGE_CACHE_DEFAULT_VALUE = "100"; - public static final int IMAGE_CACHE_SIZE_MINIMUM = 20; - public static final String PREF_LEFT_VOLUME = "prefLeftVolume"; - public static final String PREF_RIGHT_VOLUME = "prefRightVolume"; + private static final String PREF_QUEUE_LOCKED = "prefQueueLocked"; + private static final String IMAGE_CACHE_DEFAULT_VALUE = "100"; + private static final int IMAGE_CACHE_SIZE_MINIMUM = 20; + private static final String PREF_LEFT_VOLUME = "prefLeftVolume"; + private static final String PREF_RIGHT_VOLUME = "prefRightVolume"; // Experimental - public static final String PREF_SONIC = "prefSonic"; - public static final String PREF_STEREO_TO_MONO = "PrefStereoToMono"; - public static final String PREF_NORMALIZER = "prefNormalizer"; + private static final String PREF_STEREO_TO_MONO = "PrefStereoToMono"; public static final String PREF_CAST_ENABLED = "prefCast"; //Used for enabling Chromecast support public static final int EPISODE_CLEANUP_QUEUE = -1; public static final int EPISODE_CLEANUP_NULL = -2; @@ -125,10 +119,9 @@ public class UserPreferences { private static final int NOTIFICATION_BUTTON_REWIND = 0; private static final int NOTIFICATION_BUTTON_FAST_FORWARD = 1; private static final int NOTIFICATION_BUTTON_SKIP = 2; - private static int EPISODE_CACHE_SIZE_UNLIMITED = -1; + private static final int EPISODE_CACHE_SIZE_UNLIMITED = -1; public static final int FEED_ORDER_COUNTER = 0; public static final int FEED_ORDER_ALPHABETICAL = 1; - public static final int FEED_ORDER_LAST_UPDATE = 2; public static final int FEED_ORDER_MOST_PLAYED = 3; public static final int FEED_COUNTER_SHOW_NEW_UNPLAYED_SUM = 0; public static final int FEED_COUNTER_SHOW_NEW = 1; @@ -167,6 +160,8 @@ public class UserPreferences { int theme = getTheme(); if (theme == R.style.Theme_AntennaPod_Dark) { return R.style.Theme_AntennaPod_Dark_NoTitle; + } else if (theme == R.style.Theme_AntennaPod_TrueBlack) { + return R.style.Theme_AntennaPod_TrueBlack_NoTitle; } else { return R.style.Theme_AntennaPod_Light_NoTitle; } @@ -183,8 +178,8 @@ public class UserPreferences { String.valueOf(NOTIFICATION_BUTTON_SKIP)), ","); List<Integer> notificationButtons = new ArrayList<>(); - for (int i=0; i<buttons.length; i++) { - notificationButtons.add(Integer.parseInt(buttons[i])); + for (String button : buttons) { + notificationButtons.add(Integer.parseInt(button)); } return notificationButtons; } @@ -514,9 +509,8 @@ public class UserPreferences { .apply(); } - public static void setVolume(int leftVolume, int rightVolume) { - assert(0 <= leftVolume && leftVolume <= 100); - assert(0 <= rightVolume && rightVolume <= 100); + public static void setVolume(@IntRange(from = 0, to = 100) int leftVolume, + @IntRange(from = 0, to = 100) int rightVolume) { prefs.edit() .putInt(PREF_LEFT_VOLUME, leftVolume) .putInt(PREF_RIGHT_VOLUME, rightVolume) @@ -604,6 +598,8 @@ public class UserPreferences { return R.style.Theme_AntennaPod_Light; case 1: return R.style.Theme_AntennaPod_Dark; + case 2: + return R.style.Theme_AntennaPod_TrueBlack; default: return R.style.Theme_AntennaPod_Light; } @@ -643,13 +639,15 @@ public class UserPreferences { } public static boolean useSonic() { - return prefs.getBoolean(PREF_SONIC, false); + return prefs.getString(PREF_MEDIA_PLAYER, "sonic").equals("sonic"); } - public static void enableSonic(boolean enable) { - prefs.edit() - .putBoolean(PREF_SONIC, enable) - .apply(); + public static boolean useExoplayer() { + return prefs.getString(PREF_MEDIA_PLAYER, "sonic").equals("exoplayer"); + } + + public static void enableSonic() { + prefs.edit().putString(PREF_MEDIA_PLAYER, "sonic").apply(); } public static boolean stereoToMono() { @@ -662,6 +660,14 @@ public class UserPreferences { .apply(); } + public static VideoBackgroundBehavior getVideoBackgroundBehavior() { + switch (prefs.getString(PREF_VIDEO_BEHAVIOR, "stop")) { + case "stop": return VideoBackgroundBehavior.STOP; + case "pip": return VideoBackgroundBehavior.PICTURE_IN_PICTURE; + case "continue": return VideoBackgroundBehavior.CONTINUE_PLAYING; + default: return VideoBackgroundBehavior.STOP; + } + } public static EpisodeCleanupAlgorithm getEpisodeCleanupAlgorithm() { int cleanupValue = Integer.parseInt(prefs.getString(PREF_EPISODE_CLEANUP, "-1")); @@ -773,61 +779,18 @@ public class UserPreferences { int[] timeOfDay = getUpdateTimeOfDay(); Log.d(TAG, "timeOfDay: " + Arrays.toString(timeOfDay)); if (timeOfDay.length == 2) { - restartUpdateTimeOfDayAlarm(timeOfDay[0], timeOfDay[1]); + AutoUpdateManager.restartUpdateTimeOfDayAlarm(context, timeOfDay[0], timeOfDay[1]); } else { long milliseconds = getUpdateInterval(); long startTrigger = milliseconds; if (now) { startTrigger = TimeUnit.SECONDS.toMillis(10); } - restartUpdateIntervalAlarm(startTrigger, milliseconds); + AutoUpdateManager.restartUpdateIntervalAlarm(context, startTrigger, milliseconds); } } /** - * Sets the interval in which the feeds are refreshed automatically - */ - public static void restartUpdateIntervalAlarm(long triggerAtMillis, long intervalMillis) { - Log.d(TAG, "Restarting update alarm."); - AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); - Intent intent = new Intent(context, FeedUpdateReceiver.class); - PendingIntent updateIntent = PendingIntent.getBroadcast(context, 0, intent, 0); - alarmManager.cancel(updateIntent); - if (intervalMillis > 0) { - alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, - SystemClock.elapsedRealtime() + triggerAtMillis, - updateIntent); - Log.d(TAG, "Changed alarm to new interval " + TimeUnit.MILLISECONDS.toHours(intervalMillis) + " h"); - } else { - Log.d(TAG, "Automatic update was deactivated"); - } - } - - /** - * Sets time of day the feeds are refreshed automatically - */ - public static void restartUpdateTimeOfDayAlarm(int hoursOfDay, int minute) { - Log.d(TAG, "Restarting update alarm."); - AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); - PendingIntent updateIntent = PendingIntent.getBroadcast(context, 0, - new Intent(context, FeedUpdateReceiver.class), 0); - alarmManager.cancel(updateIntent); - - Calendar now = Calendar.getInstance(); - Calendar alarm = (Calendar)now.clone(); - alarm.set(Calendar.HOUR_OF_DAY, hoursOfDay); - alarm.set(Calendar.MINUTE, minute); - if (alarm.before(now) || alarm.equals(now)) { - alarm.add(Calendar.DATE, 1); - } - Log.d(TAG, "Alarm set for: " + alarm.toString() + " : " + alarm.getTimeInMillis()); - alarmManager.set(AlarmManager.RTC_WAKEUP, - alarm.getTimeInMillis(), - updateIntent); - Log.d(TAG, "Changed alarm to new time of day " + hoursOfDay + ":" + minute); - } - - /** * Reads episode cache size as it is saved in the episode_cache_size_values array. */ public static int readEpisodeCacheSize(String valueFromPrefs) { @@ -840,4 +803,8 @@ public class UserPreferences { public static boolean isCastEnabled() { return prefs.getBoolean(PREF_CAST_ENABLED, false); } + + public enum VideoBackgroundBehavior { + STOP, PICTURE_IN_PICTURE, CONTINUE_PLAYING + } } 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 9bbeb7c88..05e12f6df 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 @@ -7,8 +7,7 @@ import android.util.Log; import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.core.storage.DBTasks; -import de.danoeh.antennapod.core.util.NetworkUtils; +import de.danoeh.antennapod.core.util.FeedUpdateUtils; /** * Refreshes all feeds when it receives an intent @@ -21,11 +20,7 @@ public class FeedUpdateReceiver extends BroadcastReceiver { public void onReceive(Context context, Intent intent) { Log.d(TAG, "Received intent"); ClientConfig.initialize(context); - if (NetworkUtils.networkAvailable() && NetworkUtils.isDownloadAllowed()) { - DBTasks.refreshAllFeeds(context, null); - } else { - Log.d(TAG, "Blocking automatic update: no wifi available / no mobile updates allowed"); - } + FeedUpdateUtils.startAutoUpdate(context, null); UserPreferences.restartUpdateAlarm(false); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/receiver/MediaButtonReceiver.java b/core/src/main/java/de/danoeh/antennapod/core/receiver/MediaButtonReceiver.java index 9b4b91151..b191dbf8b 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/receiver/MediaButtonReceiver.java +++ b/core/src/main/java/de/danoeh/antennapod/core/receiver/MediaButtonReceiver.java @@ -3,6 +3,7 @@ package de.danoeh.antennapod.core.receiver; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.support.v4.content.ContextCompat; import android.util.Log; import android.view.KeyEvent; @@ -29,7 +30,7 @@ public class MediaButtonReceiver extends BroadcastReceiver { Intent serviceIntent = new Intent(context, PlaybackService.class); serviceIntent.putExtra(EXTRA_KEYCODE, event.getKeyCode()); serviceIntent.putExtra(EXTRA_SOURCE, event.getSource()); - context.startService(serviceIntent); + ContextCompat.startForegroundService(context, serviceIntent); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/receiver/PlayerWidget.java b/core/src/main/java/de/danoeh/antennapod/core/receiver/PlayerWidget.java new file mode 100644 index 000000000..edc2ea3e0 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/receiver/PlayerWidget.java @@ -0,0 +1,56 @@ +package de.danoeh.antennapod.core.receiver; + +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.util.Log; +import de.danoeh.antennapod.core.service.PlayerWidgetJobService; + +import java.util.Arrays; + + +public class PlayerWidget extends AppWidgetProvider { + private static final String TAG = "PlayerWidget"; + private static final String PREFS_NAME = "PlayerWidgetPrefs"; + private static final String KEY_ENABLED = "WidgetEnabled"; + + @Override + public void onReceive(Context context, Intent intent) { + Log.d(TAG, "onReceive"); + super.onReceive(context, intent); + PlayerWidgetJobService.updateWidget(context); + } + + @Override + public void onEnabled(Context context) { + super.onEnabled(context); + Log.d(TAG, "Widget enabled"); + setEnabled(context, true); + PlayerWidgetJobService.updateWidget(context); + } + + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + Log.d(TAG, "onUpdate() called with: " + "context = [" + context + "], appWidgetManager = [" + appWidgetManager + "], appWidgetIds = [" + Arrays.toString(appWidgetIds) + "]"); + PlayerWidgetJobService.updateWidget(context); + } + + @Override + public void onDisabled(Context context) { + super.onDisabled(context); + Log.d(TAG, "Widget disabled"); + setEnabled(context, false); + } + + public static boolean isEnabled(Context context) { + SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + return prefs.getBoolean(KEY_ENABLED, false); + } + + private void setEnabled(Context context, boolean enabled) { + SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + prefs.edit().putBoolean(KEY_ENABLED, enabled).apply(); + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/FeedUpdateJobService.java b/core/src/main/java/de/danoeh/antennapod/core/service/FeedUpdateJobService.java new file mode 100644 index 000000000..55a8d6b86 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/service/FeedUpdateJobService.java @@ -0,0 +1,34 @@ +package de.danoeh.antennapod.core.service; + +import android.app.job.JobParameters; +import android.app.job.JobService; +import android.os.Build; +import android.support.annotation.RequiresApi; +import android.util.Log; +import de.danoeh.antennapod.core.ClientConfig; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.util.FeedUpdateUtils; + +@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) +public class FeedUpdateJobService extends JobService { + private static final String TAG = "FeedUpdateJobService"; + + @Override + public boolean onStartJob(JobParameters params) { + Log.d(TAG, "Job started"); + ClientConfig.initialize(getApplicationContext()); + + FeedUpdateUtils.startAutoUpdate(getApplicationContext(), () -> { + UserPreferences.restartUpdateAlarm(false); + jobFinished(params, false); // needsReschedule = false + }); + + return true; + } + + @Override + public boolean onStopJob(JobParameters params) { + return true; + } + +} 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 e9312b929..5584991ca 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 @@ -3,11 +3,11 @@ package de.danoeh.antennapod.core.service; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; -import android.app.Service; import android.content.Context; import android.content.Intent; -import android.os.IBinder; +import android.support.annotation.NonNull; import android.support.v4.app.NotificationCompat; +import android.support.v4.app.SafeJobIntentService; import android.support.v4.util.ArrayMap; import android.util.Log; import android.util.Pair; @@ -15,6 +15,7 @@ import android.util.Pair; import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.R; @@ -37,30 +38,39 @@ 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; +import de.danoeh.antennapod.core.util.gui.NotificationUtils; /** * Synchronizes local subscriptions with gpodder.net service. The service should be started with ACTION_SYNC as an action argument. * This class also provides static methods for starting the GpodnetSyncService. */ -public class GpodnetSyncService extends Service { +public class GpodnetSyncService extends SafeJobIntentService { + private static final String TAG = "GpodnetSyncService"; private static final long WAIT_INTERVAL = 5000L; - public static final String ARG_ACTION = "action"; + private 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 static final String ACTION_SYNC = "de.danoeh.antennapod.intent.action.sync"; + private static final String ACTION_SYNC_SUBSCRIPTIONS = "de.danoeh.antennapod.intent.action.sync_subscriptions"; + private static final String ACTION_SYNC_ACTIONS = "de.danoeh.antennapod.intent.action.sync_ACTIONS"; private GpodnetService service; - private boolean syncSubscriptions = false; - private boolean syncActions = false; + private static final AtomicInteger syncActionCount = new AtomicInteger(0); + private static boolean syncSubscriptions = false; + private static boolean syncActions = false; + + private static final int JOB_ID = -17000; + + private static void enqueueWork(Context context, Intent intent) { + enqueueWork(context, GpodnetSyncService.class, JOB_ID, intent); + } @Override - public int onStartCommand(Intent intent, int flags, int startId) { - final String action = (intent != null) ? intent.getStringExtra(ARG_ACTION) : null; + protected void onHandleWork(@NonNull Intent intent) { + final String action = intent.getStringExtra(ARG_ACTION); if (action != null) { switch(action) { case ACTION_SYNC: @@ -78,24 +88,20 @@ public class GpodnetSyncService extends Service { } if(syncSubscriptions || syncActions) { Log.d(TAG, String.format("Waiting %d milliseconds before uploading changes", WAIT_INTERVAL)); - syncWaiterThread.restart(); + int syncActionId = syncActionCount.incrementAndGet(); + try { + Thread.sleep(WAIT_INTERVAL); + } catch (InterruptedException e) { + e.printStackTrace(); + } + if (syncActionId == syncActionCount.get()) { + // onHandleWork was not called again in the meantime + sync(); + } } } else { Log.e(TAG, "Received invalid intent: action argument is null"); } - return START_FLAG_REDELIVERY; - } - - @Override - public void onDestroy() { - super.onDestroy(); - Log.d(TAG, "onDestroy"); - syncWaiterThread.interrupt(); - } - - @Override - public IBinder onBind(Intent intent) { - return null; } private synchronized GpodnetService tryLogin() throws GpodnetServiceException { @@ -109,6 +115,7 @@ public class GpodnetSyncService extends Service { private synchronized void sync() { if (!GpodnetPreferences.loggedIn() || !NetworkUtils.networkAvailable()) { + stopForeground(true); stopSelf(); return; } @@ -125,7 +132,6 @@ public class GpodnetSyncService extends Service { } syncActions = false; } - stopSelf(); } private synchronized void syncSubscriptionChanges() { @@ -222,14 +228,12 @@ public class GpodnetSyncService extends Service { } catch (GpodnetServiceException e) { e.printStackTrace(); updateErrorNotification(e); - } catch (DownloadRequestException e) { - e.printStackTrace(); } } private synchronized void processEpisodeActions(List<GpodnetEpisodeAction> localActions, - List<GpodnetEpisodeAction> remoteActions) throws DownloadRequestException { + List<GpodnetEpisodeAction> remoteActions) { if(remoteActions.size() == 0) { return; } @@ -321,7 +325,7 @@ public class GpodnetSyncService extends Service { } PendingIntent activityIntent = ClientConfig.gpodnetCallbacks.getGpodnetSyncServiceErrorNotificationPendingIntent(this); - Notification notification = new NotificationCompat.Builder(this) + Notification notification = new NotificationCompat.Builder(this, NotificationUtils.CHANNEL_ID_ERROR) .setContentTitle(title) .setContentText(description) .setContentIntent(activityIntent) @@ -333,69 +337,11 @@ public class GpodnetSyncService extends Service { nm.notify(id, notification); } - private WaiterThread syncWaiterThread = new WaiterThread(WAIT_INTERVAL) { - @Override - public void onWaitCompleted() { - sync(); - } - }; - - private abstract class WaiterThread { - private long waitInterval; - private Thread thread; - - private WaiterThread(long waitInterval) { - this.waitInterval = waitInterval; - reinit(); - } - - public abstract void onWaitCompleted(); - - public void exec() { - if (!thread.isAlive()) { - thread.start(); - } - } - - private void reinit() { - if (thread != null && thread.isAlive()) { - Log.d(TAG, "Interrupting waiter thread"); - thread.interrupt(); - } - thread = new Thread() { - @Override - public void run() { - try { - Thread.sleep(waitInterval); - } catch (InterruptedException e) { - e.printStackTrace(); - } - if (!isInterrupted()) { - synchronized (this) { - onWaitCompleted(); - } - } - } - }; - } - - public void restart() { - reinit(); - exec(); - } - - public void interrupt() { - if (thread != null && thread.isAlive()) { - thread.interrupt(); - } - } - } - public static void sendSyncIntent(Context context) { if (GpodnetPreferences.loggedIn()) { Intent intent = new Intent(context, GpodnetSyncService.class); intent.putExtra(ARG_ACTION, ACTION_SYNC); - context.startService(intent); + enqueueWork(context, intent); } } @@ -403,7 +349,7 @@ public class GpodnetSyncService extends Service { if (GpodnetPreferences.loggedIn()) { Intent intent = new Intent(context, GpodnetSyncService.class); intent.putExtra(ARG_ACTION, ACTION_SYNC_SUBSCRIPTIONS); - context.startService(intent); + enqueueWork(context, intent); } } @@ -411,7 +357,7 @@ public class GpodnetSyncService extends Service { if (GpodnetPreferences.loggedIn()) { Intent intent = new Intent(context, GpodnetSyncService.class); intent.putExtra(ARG_ACTION, ACTION_SYNC_ACTIONS); - context.startService(intent); + enqueueWork(context, intent); } } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/PlayerWidgetJobService.java b/core/src/main/java/de/danoeh/antennapod/core/service/PlayerWidgetJobService.java new file mode 100644 index 000000000..6dab9a561 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/service/PlayerWidgetJobService.java @@ -0,0 +1,180 @@ +package de.danoeh.antennapod.core.service; + +import android.app.PendingIntent; +import android.appwidget.AppWidgetManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.Build; +import android.os.IBinder; +import android.support.annotation.NonNull; +import android.support.v4.app.SafeJobIntentService; +import android.util.Log; +import android.view.KeyEvent; +import android.view.View; +import android.widget.RemoteViews; + +import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.core.receiver.MediaButtonReceiver; +import de.danoeh.antennapod.core.receiver.PlayerWidget; +import de.danoeh.antennapod.core.service.playback.PlaybackService; +import de.danoeh.antennapod.core.service.playback.PlayerStatus; +import de.danoeh.antennapod.core.util.Converter; +import de.danoeh.antennapod.core.util.playback.Playable; + +/** + * Updates the state of the player widget + */ +public class PlayerWidgetJobService extends SafeJobIntentService { + + private static final String TAG = "PlayerWidgetJobService"; + + private PlaybackService playbackService; + private final Object waitForService = new Object(); + + private static final int JOB_ID = -17001; + + public static void updateWidget(Context context) { + enqueueWork(context, PlayerWidgetJobService.class, JOB_ID, new Intent(context, PlayerWidgetJobService.class)); + } + + @Override + protected void onHandleWork(@NonNull Intent intent) { + if (!PlayerWidget.isEnabled(getApplicationContext())) { + return; + } + + synchronized (waitForService) { + if (PlaybackService.isRunning && playbackService == null) { + bindService(new Intent(this, PlaybackService.class), mConnection, 0); + while (playbackService == null) { + try { + waitForService.wait(); + } catch (InterruptedException e) { + return; + } + } + } + } + + updateViews(); + + if (playbackService != null) { + try { + unbindService(mConnection); + } catch (IllegalArgumentException e) { + Log.w(TAG, "IllegalArgumentException when trying to unbind service"); + } + } + } + + private void updateViews() { + + ComponentName playerWidget = new ComponentName(this, PlayerWidget.class); + AppWidgetManager manager = AppWidgetManager.getInstance(this); + RemoteViews views = new RemoteViews(getPackageName(), R.layout.player_widget); + PendingIntent startMediaplayer = PendingIntent.getActivity(this, 0, + PlaybackService.getPlayerActivityIntent(this), 0); + + final PendingIntent startAppPending = PendingIntent.getActivity(this, 0, + PlaybackService.getPlayerActivityIntent(this), + PendingIntent.FLAG_UPDATE_CURRENT); + + boolean nothingPlaying = false; + Playable media; + PlayerStatus status; + if (playbackService != null) { + media = playbackService.getPlayable(); + status = playbackService.getStatus(); + } else { + media = Playable.PlayableUtils.createInstanceFromPreferences(getApplicationContext()); + status = PlayerStatus.STOPPED; + } + + if (media != null) { + views.setOnClickPendingIntent(R.id.layout_left, startMediaplayer); + + views.setTextViewText(R.id.txtvTitle, media.getEpisodeTitle()); + + String progressString; + if (playbackService != null) { + progressString = getProgressString(playbackService.getCurrentPosition(), playbackService.getDuration()); + } else { + progressString = getProgressString(media.getPosition(), media.getDuration()); + } + + if (progressString != null) { + views.setViewVisibility(R.id.txtvProgress, View.VISIBLE); + views.setTextViewText(R.id.txtvProgress, progressString); + } + + if (status == PlayerStatus.PLAYING) { + views.setImageViewResource(R.id.butPlay, R.drawable.ic_pause_white_24dp); + if (Build.VERSION.SDK_INT >= 15) { + views.setContentDescription(R.id.butPlay, getString(R.string.pause_label)); + } + } else { + views.setImageViewResource(R.id.butPlay, R.drawable.ic_play_arrow_white_24dp); + if (Build.VERSION.SDK_INT >= 15) { + views.setContentDescription(R.id.butPlay, getString(R.string.play_label)); + } + } + views.setOnClickPendingIntent(R.id.butPlay, createMediaButtonIntent()); + } else { + nothingPlaying = true; + } + + if (nothingPlaying) { + // start the app if they click anything + views.setOnClickPendingIntent(R.id.layout_left, startAppPending); + views.setOnClickPendingIntent(R.id.butPlay, startAppPending); + views.setViewVisibility(R.id.txtvProgress, View.INVISIBLE); + views.setTextViewText(R.id.txtvTitle, + this.getString(R.string.no_media_playing_label)); + views.setImageViewResource(R.id.butPlay, R.drawable.ic_play_arrow_white_24dp); + } + + manager.updateAppWidget(playerWidget, views); + } + + /** + * Creates an intent which fakes a mediabutton press + */ + private PendingIntent createMediaButtonIntent() { + KeyEvent event = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE); + Intent startingIntent = new Intent(getBaseContext(), MediaButtonReceiver.class); + startingIntent.setAction(MediaButtonReceiver.NOTIFY_BUTTON_RECEIVER); + startingIntent.putExtra(Intent.EXTRA_KEY_EVENT, event); + + return PendingIntent.getBroadcast(this, 0, startingIntent, 0); + } + + private String getProgressString(int position, int duration) { + if (position > 0 && duration > 0) { + return Converter.getDurationStringLong(position) + " / " + + Converter.getDurationStringLong(duration); + } else { + return null; + } + } + + private final ServiceConnection mConnection = new ServiceConnection() { + public void onServiceConnected(ComponentName className, IBinder service) { + Log.d(TAG, "Connection to service established"); + if (service instanceof PlaybackService.LocalBinder) { + synchronized (waitForService) { + playbackService = ((PlaybackService.LocalBinder) service).getService(); + waitForService.notifyAll(); + } + } + } + + @Override + public void onServiceDisconnected(ComponentName name) { + playbackService = null; + Log.d(TAG, "Disconnected from service"); + } + + }; +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/AntennapodHttpClient.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/AntennapodHttpClient.java index 300f57be6..97007a214 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/AntennapodHttpClient.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/AntennapodHttpClient.java @@ -45,10 +45,10 @@ public class AntennapodHttpClient { private static final String TAG = "AntennapodHttpClient"; - public static final int CONNECTION_TIMEOUT = 30000; - public static final int READ_TIMEOUT = 30000; + private static final int CONNECTION_TIMEOUT = 30000; + private static final int READ_TIMEOUT = 30000; - public static final int MAX_CONNECTIONS = 8; + private static final int MAX_CONNECTIONS = 8; private static volatile OkHttpClient httpClient = null; diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadRequest.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadRequest.java index de91916a9..75c28564e 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadRequest.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadRequest.java @@ -17,15 +17,15 @@ public class DownloadRequest implements Parcelable { private String username; private String password; private String lastModified; - private boolean deleteOnFailure; + private final boolean deleteOnFailure; private final long feedfileId; private final int feedfileType; private final Bundle arguments; - protected int progressPercent; - protected long soFar; - protected long size; - protected int statusMsg; + private int progressPercent; + private long soFar; + private long size; + private int statusMsg; public DownloadRequest(@NonNull String destination, @NonNull String source, @@ -53,7 +53,7 @@ public class DownloadRequest implements Parcelable { this(destination, source, title, feedfileId, feedfileType, null, null, true, null); } - public DownloadRequest(Builder builder) { + private DownloadRequest(Builder builder) { this.destination = builder.destination; this.source = builder.source; this.title = builder.title; @@ -211,10 +211,6 @@ public class DownloadRequest implements Parcelable { this.size = size; } - public int getStatusMsg() { - return statusMsg; - } - public void setStatusMsg(int statusMsg) { this.statusMsg = statusMsg; } @@ -254,15 +250,15 @@ public class DownloadRequest implements Parcelable { } public static class Builder { - private String destination; - private String source; - private String title; + private final String destination; + private final String source; + private final String title; private String username; private String password; private String lastModified; private boolean deleteOnFailure = false; - private long feedfileId; - private int feedfileType; + private final long feedfileId; + private final int feedfileType; private Bundle arguments; public Builder(@NonNull String destination, @NonNull FeedFile item) { 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 0dc5c9db2..4bd2d8f19 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 @@ -7,8 +7,6 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; import android.media.MediaMetadataRetriever; import android.os.Binder; import android.os.Build; @@ -22,8 +20,9 @@ import android.util.Log; import android.util.Pair; import android.webkit.URLUtil; +import de.danoeh.antennapod.core.storage.PodDBAdapter; +import de.danoeh.antennapod.core.util.gui.NotificationUtils; import org.apache.commons.io.FileUtils; -import org.apache.commons.lang3.StringUtils; import org.xml.sax.SAXException; import java.io.File; @@ -57,7 +56,6 @@ import de.danoeh.antennapod.core.R; import de.danoeh.antennapod.core.event.DownloadEvent; import de.danoeh.antennapod.core.event.FeedItemEvent; import de.danoeh.antennapod.core.feed.Feed; -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; @@ -128,8 +126,8 @@ public class DownloadService extends Service { private NotificationCompat.Builder notificationCompatBuilder; - private int NOTIFICATION_ID = 2; - private int REPORT_ID = 3; + private static final int NOTIFICATION_ID = 2; + private static final int REPORT_ID = 3; /** * Currently running downloads. @@ -153,7 +151,7 @@ public class DownloadService extends Service { private static final int SCHED_EX_POOL_SIZE = 1; private ScheduledThreadPoolExecutor schedExecutor; - private Handler postHandler = new Handler(); + private final Handler postHandler = new Handler(); private final IBinder mBinder = new LocalBinder(); @@ -163,7 +161,7 @@ public class DownloadService extends Service { } } - private Thread downloadCompletionThread = new Thread() { + private final Thread downloadCompletionThread = new Thread() { private static final String TAG = "downloadCompletionThd"; @Override @@ -259,6 +257,7 @@ public class DownloadService extends Service { public void onCreate() { Log.d(TAG, "Service started"); isRunning = true; + PodDBAdapter.getInstance().open(); // Prevent thrashing the database by opening and closing rapidly handler = new Handler(); reportQueue = Collections.synchronizedList(new ArrayList<>()); downloads = Collections.synchronizedList(new ArrayList<>()); @@ -296,6 +295,7 @@ public class DownloadService extends Service { setupNotificationBuilders(); requester = DownloadRequester.getInstance(); + startForeground(NOTIFICATION_ID, updateNotifications()); } @Override @@ -337,15 +337,13 @@ public class DownloadService extends Service { // start auto download in case anything new has shown up DBTasks.autodownloadUndownloadedItems(getApplicationContext()); + PodDBAdapter.getInstance().close(); } private void setupNotificationBuilders() { - Bitmap icon = BitmapFactory.decodeResource(getResources(), R.drawable.stat_notify_sync); - - notificationCompatBuilder = new NotificationCompat.Builder(this) + notificationCompatBuilder = new NotificationCompat.Builder(this, NotificationUtils.CHANNEL_ID_DOWNLOADING) .setOngoing(true) .setContentIntent(ClientConfig.downloadServiceCallbacks.getNotificationContentIntent(this)) - .setLargeIcon(icon) .setSmallIcon(R.drawable.stat_notify_sync); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { notificationCompatBuilder.setVisibility(Notification.VISIBILITY_PUBLIC); @@ -356,7 +354,7 @@ public class DownloadService extends Service { /** * Updates the contents of the service's notifications. Should be called - * before setupNotificationBuilders. + * after setupNotificationBuilders. */ private Notification updateNotifications() { if (notificationCompatBuilder == null) { @@ -385,7 +383,7 @@ public class DownloadService extends Service { return null; } - private BroadcastReceiver cancelDownloadReceiver = new BroadcastReceiver() { + private final BroadcastReceiver cancelDownloadReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { @@ -424,6 +422,8 @@ public class DownloadService extends Service { "ACTION_ENQUEUE_DOWNLOAD intent needs request extra"); } + writeFileUrl(request); + Downloader downloader = getDownloader(request); if (downloader != null) { numberOfDownloads.incrementAndGet(); @@ -491,9 +491,7 @@ public class DownloadService extends Service { if (status.isSuccessful()) { successfulDownloads++; } else if (!status.isCancelled()) { - if (status.getFeedfileType() != FeedImage.FEEDFILETYPE_FEEDIMAGE) { - createReport = true; - } + createReport = true; failedDownloads++; } } @@ -501,7 +499,7 @@ public class DownloadService extends Service { if (createReport) { Log.d(TAG, "Creating report"); // create notification object - NotificationCompat.Builder builder = new NotificationCompat.Builder(this) + NotificationCompat.Builder builder = new NotificationCompat.Builder(this, NotificationUtils.CHANNEL_ID_ERROR) .setTicker(getString(R.string.download_report_title)) .setContentTitle(getString(R.string.download_report_content_title)) .setContentText( @@ -510,10 +508,6 @@ public class DownloadService extends Service { successfulDownloads, failedDownloads) ) .setSmallIcon(R.drawable.stat_notify_sync_error) - .setLargeIcon( - BitmapFactory.decodeResource(getResources(), - R.drawable.stat_notify_sync_error) - ) .setContentIntent( ClientConfig.downloadServiceCallbacks.getReportNotificationContentIntent(this) ) @@ -533,14 +527,14 @@ public class DownloadService extends Service { * Calls query downloads on the services main thread. This method should be used instead of queryDownloads if it is * used from a thread other than the main thread. */ - void queryDownloadsAsync() { + private void queryDownloadsAsync() { handler.post(DownloadService.this::queryDownloads); } /** * Check if there's something else to download, otherwise stop */ - void queryDownloads() { + private void queryDownloads() { Log.d(TAG, numberOfDownloads.get() + " downloads left"); if (numberOfDownloads.get() <= 0 && DownloadRequester.getInstance().hasNoDownloads()) { @@ -557,14 +551,13 @@ public class DownloadService extends Service { final String resourceTitle = (downloadRequest.getTitle() != null) ? downloadRequest.getTitle() : downloadRequest.getSource(); - NotificationCompat.Builder builder = new NotificationCompat.Builder(DownloadService.this); + NotificationCompat.Builder builder = new NotificationCompat.Builder(DownloadService.this, NotificationUtils.CHANNEL_ID_USER_ACTION); builder.setTicker(getText(R.string.authentication_notification_title)) .setContentTitle(getText(R.string.authentication_notification_title)) .setContentText(getText(R.string.authentication_notification_msg)) .setStyle(new NotificationCompat.BigTextStyle().bigText(getText(R.string.authentication_notification_msg) + ": " + resourceTitle)) .setSmallIcon(R.drawable.ic_stat_authentication) - .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.ic_stat_authentication)) .setAutoCancel(true) .setContentIntent(ClientConfig.downloadServiceCallbacks.getAuthentificationNotificationContentIntent(DownloadService.this, downloadRequest)); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { @@ -603,14 +596,14 @@ public class DownloadService extends Service { private class FeedSyncThread extends Thread { private static final String TAG = "FeedSyncThread"; - private BlockingQueue<DownloadRequest> completedRequests = new LinkedBlockingDeque<>(); - private CompletionService<Pair<DownloadRequest, FeedHandlerResult>> parserService = new ExecutorCompletionService<>(Executors.newSingleThreadExecutor()); - private ExecutorService dbService = Executors.newSingleThreadExecutor(); + private final BlockingQueue<DownloadRequest> completedRequests = new LinkedBlockingDeque<>(); + private final CompletionService<Pair<DownloadRequest, FeedHandlerResult>> parserService = new ExecutorCompletionService<>(Executors.newSingleThreadExecutor()); + private final ExecutorService dbService = Executors.newSingleThreadExecutor(); private Future<?> dbUpdateFuture; private volatile boolean isActive = true; private volatile boolean isCollectingRequests = false; - private final long WAIT_TIMEOUT = 3000; + private static final long WAIT_TIMEOUT = 3000; /** @@ -695,10 +688,6 @@ public class DownloadService extends Service { Log.d(TAG, "Bundling " + results.size() + " feeds"); - for (Pair<DownloadRequest, FeedHandlerResult> result : results) { - removeDuplicateImages(result.second.feed); // duplicate images have to removed because the DownloadRequester does not accept two downloads with the same download URL yet. - } - // Save information of feed in DB if (dbUpdateFuture != null) { try { @@ -765,7 +754,7 @@ public class DownloadService extends Service { private class FeedParserTask implements Callable<Pair<DownloadRequest, FeedHandlerResult>> { - private DownloadRequest request; + private final DownloadRequest request; private FeedParserTask(DownloadRequest request) { this.request = request; @@ -906,6 +895,42 @@ public class DownloadService extends Service { } /** + * Creates the destination file and writes FeedMedia File_url directly after starting download + * to make it possible to resume download after the service was killed by the system. + */ + private void writeFileUrl(DownloadRequest request) { + if (request.getFeedfileType() != FeedMedia.FEEDFILETYPE_FEEDMEDIA) { + return; + } + + File dest = new File(request.getDestination()); + if (!dest.exists()) { + try { + dest.createNewFile(); + } catch (IOException e) { + Log.e(TAG, "Unable to create file"); + } + } + + if (dest.exists()) { + Log.d(TAG, "Writing file url"); + FeedMedia media = DBReader.getFeedMedia(request.getFeedfileId()); + if (media == null) { + Log.d(TAG, "No media"); + return; + } + media.setFile_url(request.getDestination()); + try { + DBWriter.setFeedMedia(media).get(); + } catch (InterruptedException e) { + Log.e(TAG, "writeFileUrl was interrupted"); + } catch (ExecutionException e) { + Log.e(TAG, "ExecutionException in writeFileUrl: " + e.getMessage()); + } + } + } + + /** * Handles failed downloads. * <p/> * If the file has been partially downloaded, this handler will set the file_url of the FeedFile to the location @@ -913,10 +938,10 @@ public class DownloadService extends Service { * <p/> * Currently, this handler only handles FeedMedia objects, because Feeds and FeedImages are deleted if the download fails. */ - private class FailedDownloadHandler implements Runnable { + private static class FailedDownloadHandler implements Runnable { - private DownloadRequest request; - private DownloadStatus status; + private final DownloadRequest request; + private final DownloadStatus status; FailedDownloadHandler(DownloadStatus status, DownloadRequest request) { this.request = request; @@ -929,23 +954,6 @@ public class DownloadService extends Service { DBWriter.setFeedLastUpdateFailed(request.getFeedfileId(), true); } else if (request.isDeleteOnFailure()) { Log.d(TAG, "Ignoring failed download, deleteOnFailure=true"); - } 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"); - FeedMedia media = DBReader.getFeedMedia(request.getFeedfileId()); - if (media == null) { - return; - } - media.setFile_url(request.getDestination()); - try { - DBWriter.setFeedMedia(media).get(); - } catch (InterruptedException e) { - Log.e(TAG, "FailedDownloadHandler was interrupted"); - } catch (ExecutionException e) { - Log.e(TAG, "ExecutionException in FailedDownloadHandler: " + e.getMessage()); - } - } } } } @@ -955,7 +963,7 @@ public class DownloadService extends Service { */ private class MediaHandlerThread implements Runnable { - private DownloadRequest request; + private final DownloadRequest request; private DownloadStatus status; MediaHandlerThread(@NonNull DownloadStatus status, @@ -1071,7 +1079,7 @@ public class DownloadService extends Service { private long lastPost = 0; - final Runnable postDownloaderTask = new Runnable() { + private final Runnable postDownloaderTask = new Runnable() { @Override public void run() { List<Downloader> list = Collections.unmodifiableList(downloads); @@ -1089,26 +1097,6 @@ public class DownloadService extends Service { } } - /** - * Checks if the FeedItems of this feed have images that point to the same URL. If two FeedItems - * have an image that points to the same URL, the reference of the second item is removed, so - * that every image reference is unique. - */ - @VisibleForTesting - public static void removeDuplicateImages(Feed feed) { - Set<String> known = new HashSet<>(); - for (FeedItem item : feed.getItems()) { - String url = item.hasItemImage() ? item.getImage().getDownload_url() : null; - if (url != null) { - if (known.contains(url)) { - item.setImage(null); - } else { - known.add(url); - } - } - } - } - private static String compileNotificationString(List<Downloader> downloads) { List<String> lines = new ArrayList<>(downloads.size()); for (Downloader downloader : downloads) { diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadStatus.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadStatus.java index ed2b00dfe..5debc6d05 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadStatus.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadStatus.java @@ -19,37 +19,37 @@ public class DownloadStatus { // ----------------------------------- ATTRIBUTES STORED IN DB /** Unique id for storing the object in database. */ - protected long id; + private long id; /** * A human-readable string which is shown to the user so that he can * identify the download. Should be the title of the item/feed/media or the * URL if the download has no other title. */ - protected String title; - protected DownloadError reason; + private final String title; + private DownloadError reason; /** * A message which can be presented to the user to give more information. * Should be null if Download was successful. */ - protected String reasonDetailed; - protected boolean successful; - protected Date completionDate; - protected long feedfileId; + private String reasonDetailed; + private boolean successful; + private Date completionDate; + private final long feedfileId; /** * Is used to determine the type of the feedfile even if the feedfile does * not exist anymore. The value should be FEEDFILETYPE_FEED, * FEEDFILETYPE_FEEDIMAGE or FEEDFILETYPE_FEEDMEDIA */ - protected int feedfileType; + private final int feedfileType; // ------------------------------------ NOT STORED IN DB - protected boolean done; - protected boolean cancelled; + private boolean done; + private boolean cancelled; /** Constructor for restoring Download status entries from DB. */ - public DownloadStatus(long id, String title, long feedfileId, - int feedfileType, boolean successful, DownloadError reason, - Date completionDate, String reasonDetailed) { + private DownloadStatus(long id, String title, long feedfileId, + int feedfileType, boolean successful, DownloadError reason, + Date completionDate, String reasonDetailed) { this.id = id; this.title = title; this.done = true; diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/Downloader.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/Downloader.java index d8042d202..445210d3a 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/Downloader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/Downloader.java @@ -14,14 +14,14 @@ import de.danoeh.antennapod.core.R; public abstract class Downloader implements Callable<Downloader> { private static final String TAG = "Downloader"; - protected volatile boolean finished; + private volatile boolean finished; - protected volatile boolean cancelled; + volatile boolean cancelled; - protected DownloadRequest request; - protected DownloadStatus result; + final DownloadRequest request; + final DownloadStatus result; - public Downloader(DownloadRequest request) { + Downloader(DownloadRequest request) { super(); this.request = request; this.request.setStatusMsg(R.string.download_pending); @@ -33,7 +33,7 @@ public abstract class Downloader implements Callable<Downloader> { public final Downloader call() { WifiManager wifiManager = (WifiManager) - ClientConfig.applicationCallbacks.getApplicationInstance().getSystemService(Context.WIFI_SERVICE); + ClientConfig.applicationCallbacks.getApplicationInstance().getApplicationContext().getSystemService(Context.WIFI_SERVICE); WifiManager.WifiLock wifiLock = null; if (wifiManager != null) { wifiLock = wifiManager.createWifiLock(TAG); diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloaderCallback.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloaderCallback.java deleted file mode 100644 index b0829f084..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloaderCallback.java +++ /dev/null @@ -1,10 +0,0 @@ -package de.danoeh.antennapod.core.service.download; - -/** - * Callback used by the Downloader-classes to notify the requester that the - * download has completed. - */ -public interface DownloaderCallback { - - void onDownloadCompleted(Downloader downloader); -} 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 b409a419a..8cce02155 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 @@ -20,7 +20,6 @@ import java.util.Date; import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.R; -import de.danoeh.antennapod.core.feed.FeedImage; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.util.DateUtils; import de.danoeh.antennapod.core.util.DownloadError; @@ -50,13 +49,8 @@ public class HttpDownloader extends Downloader { if (request.isDeleteOnFailure() && fileExists) { Log.w(TAG, "File already exists"); - if (request.getFeedfileType() != FeedImage.FEEDFILETYPE_FEEDIMAGE) { - onFail(DownloadError.ERROR_FILE_EXISTS, null); - return; - } else { - onSuccess(); - return; - } + onSuccess(); + return; } OkHttpClient.Builder httpClientBuilder = AntennapodHttpClient.newBuilder(); @@ -93,7 +87,7 @@ public class HttpDownloader extends Downloader { // add range header if necessary - if (fileExists) { + if (fileExists && destination.length() > 0) { request.setSoFar(destination.length()); httpReq.addHeader("Range", "bytes=" + request.getSoFar() + "-"); Log.d(TAG, "Adding range header: " + request.getSoFar()); @@ -314,9 +308,9 @@ public class HttpDownloader extends Downloader { } } - private class BasicAuthorizationInterceptor implements Interceptor { + private static class BasicAuthorizationInterceptor implements Interceptor { - private DownloadRequest downloadRequest; + private final DownloadRequest downloadRequest; public BasicAuthorizationInterceptor(DownloadRequest downloadRequest) { this.downloadRequest = downloadRequest; diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java new file mode 100644 index 000000000..cc9d2ce2d --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java @@ -0,0 +1,247 @@ +package de.danoeh.antennapod.core.service.playback; + +import android.content.Context; +import android.net.Uri; +import android.view.SurfaceHolder; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.DefaultLoadControl; +import com.google.android.exoplayer2.DefaultRenderersFactory; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.ExoPlayerFactory; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.SeekParameters; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.audio.AudioAttributes; +import com.google.android.exoplayer2.source.ExtractorMediaSource; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; +import com.google.android.exoplayer2.util.Util; +import de.danoeh.antennapod.core.util.playback.IPlayer; +import org.antennapod.audio.MediaPlayer; + + +public class ExoPlayerWrapper implements IPlayer { + private final Context mContext; + private SimpleExoPlayer mExoPlayer; + private MediaSource mediaSource; + private MediaPlayer.OnSeekCompleteListener audioSeekCompleteListener; + private MediaPlayer.OnCompletionListener audioCompletionListener; + private MediaPlayer.OnErrorListener audioErrorListener; + + ExoPlayerWrapper(Context context) { + mContext = context; + mExoPlayer = createPlayer(); + } + + private SimpleExoPlayer createPlayer() { + SimpleExoPlayer p = ExoPlayerFactory.newSimpleInstance(new DefaultRenderersFactory(mContext), + new DefaultTrackSelector(), new DefaultLoadControl()); + p.setSeekParameters(SeekParameters.PREVIOUS_SYNC); + p.addListener(new Player.EventListener() { + @Override + public void onTimelineChanged(Timeline timeline, Object manifest, int reason) { + + } + + @Override + public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + + } + + @Override + public void onLoadingChanged(boolean isLoading) { + + } + + @Override + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + if (playbackState == Player.STATE_ENDED) { + audioCompletionListener.onCompletion(null); + } + } + + @Override + public void onRepeatModeChanged(int repeatMode) { + + } + + @Override + public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { + + } + + @Override + public void onPlayerError(ExoPlaybackException error) { + if (audioErrorListener != null) { + audioErrorListener.onError(null, 0, 0); + } + } + + @Override + public void onPositionDiscontinuity(int reason) { + + } + + @Override + public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + + } + + @Override + public void onSeekProcessed() { + audioSeekCompleteListener.onSeekComplete(null); + } + }); + return p; + } + + @Override + public boolean canSetSpeed() { + return true; + } + + @Override + public boolean canDownmix() { + return false; + } + + @Override + public int getCurrentPosition() { + return (int) mExoPlayer.getCurrentPosition(); + } + + @Override + public float getCurrentSpeedMultiplier() { + return mExoPlayer.getPlaybackParameters().speed; + } + + @Override + public int getDuration() { + if (mExoPlayer.getDuration() == C.TIME_UNSET) { + return PlaybackServiceMediaPlayer.INVALID_TIME; + } + return (int) mExoPlayer.getDuration(); + } + + @Override + public boolean isPlaying() { + return mExoPlayer.getPlayWhenReady(); + } + + @Override + public void pause() { + mExoPlayer.setPlayWhenReady(false); + } + + @Override + public void prepare() throws IllegalStateException { + mExoPlayer.prepare(mediaSource); + } + + @Override + public void release() { + if (mExoPlayer != null) { + mExoPlayer.release(); + } + audioSeekCompleteListener = null; + audioCompletionListener = null; + audioErrorListener = null; + } + + @Override + public void reset() { + mExoPlayer.release(); + mExoPlayer = createPlayer(); + } + + @Override + public void seekTo(int i) throws IllegalStateException { + mExoPlayer.seekTo(i); + } + + @Override + public void setAudioStreamType(int i) { + AudioAttributes a = mExoPlayer.getAudioAttributes(); + AudioAttributes.Builder b = new AudioAttributes.Builder(); + b.setContentType(i); + b.setFlags(a.flags); + b.setUsage(a.usage); + mExoPlayer.setAudioAttributes(b.build()); + } + + @Override + public void setDataSource(String s) throws IllegalArgumentException, IllegalStateException { + DataSource.Factory dataSourceFactory = + new DefaultDataSourceFactory(mContext, Util.getUserAgent(mContext, mContext.getPackageName()), null); + ExtractorMediaSource.Factory f = new ExtractorMediaSource.Factory(dataSourceFactory); + mediaSource = f.createMediaSource(Uri.parse(s)); + } + + @Override + public void setDisplay(SurfaceHolder sh) { + mExoPlayer.setVideoSurfaceHolder(sh); + } + + @Override + public void setPlaybackSpeed(float v) { + PlaybackParameters params = mExoPlayer.getPlaybackParameters(); + mExoPlayer.setPlaybackParameters(new PlaybackParameters(v, params.pitch)); + } + + @Override + public void setDownmix(boolean b) { + + } + + @Override + public void setVolume(float v, float v1) { + mExoPlayer.setVolume(v); + } + + @Override + public void setWakeMode(Context context, int i) { + + } + + @Override + public void start() { + mExoPlayer.setPlayWhenReady(true); + } + + @Override + public void stop() { + mExoPlayer.stop(); + } + + void setOnCompletionListener(MediaPlayer.OnCompletionListener audioCompletionListener) { + this.audioCompletionListener = audioCompletionListener; + } + + void setOnSeekCompleteListener(MediaPlayer.OnSeekCompleteListener audioSeekCompleteListener) { + this.audioSeekCompleteListener = audioSeekCompleteListener; + } + + void setOnErrorListener(MediaPlayer.OnErrorListener audioErrorListener) { + this.audioErrorListener = audioErrorListener; + } + + int getVideoWidth() { + if (mExoPlayer.getVideoFormat() == null) { + return 0; + } + return mExoPlayer.getVideoFormat().width; + } + + int getVideoHeight() { + if (mExoPlayer.getVideoFormat() == null) { + return 0; + } + return mExoPlayer.getVideoFormat().height; + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java index 11cd21db5..c7948b157 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java @@ -1,7 +1,10 @@ package de.danoeh.antennapod.core.service.playback; import android.content.Context; +import android.media.AudioAttributes; +import android.media.AudioFocusRequest; import android.media.AudioManager; +import android.os.Build; import android.os.PowerManager; import android.support.annotation.NonNull; import android.telephony.TelephonyManager; @@ -11,6 +14,7 @@ import android.view.SurfaceHolder; import org.antennapod.audio.MediaPlayer; +import java.io.File; import java.io.IOException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Future; @@ -32,7 +36,7 @@ import de.danoeh.antennapod.core.util.playback.VideoPlayer; * Manages the MediaPlayer object of the PlaybackService. */ public class LocalPSMP extends PlaybackServiceMediaPlayer { - public static final String TAG = "LclPlaybackSvcMPlayer"; + private static final String TAG = "LclPlaybackSvcMPlayer"; private final AudioManager audioManager; @@ -42,7 +46,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { private volatile boolean stream; private volatile MediaType mediaType; - private volatile AtomicBoolean startWhenPrepared; + private final AtomicBoolean startWhenPrepared; private volatile boolean pausedBecauseOfTransientAudiofocusLoss; private volatile Pair<Integer, Integer> videoSize; @@ -165,8 +169,10 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { callback.onMediaChanged(false); if (stream) { mediaPlayer.setDataSource(media.getStreamUrl()); - } else { + } else if (new File(media.getLocalMediaUrl()).canRead()) { mediaPlayer.setDataSource(media.getLocalMediaUrl()); + } else { + throw new IOException("Unable to read local file " + media.getLocalMediaUrl()); } setPlayerStatus(PlayerStatus.INITIALIZED, media); @@ -199,9 +205,26 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { private void resumeSync() { if (playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PREPARED) { - int focusGained = audioManager.requestAudioFocus( - audioFocusChangeListener, AudioManager.STREAM_MUSIC, - AudioManager.AUDIOFOCUS_GAIN); + int focusGained; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + AudioAttributes audioAttributes = new AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_MEDIA) + .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) + .build(); + AudioFocusRequest audioFocusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN) + .setAudioAttributes(audioAttributes) + .setOnAudioFocusChangeListener(audioFocusChangeListener) + .setAcceptsDelayedFocusGain(true) + .setWillPauseWhenDucked(true) + .build(); + focusGained = audioManager.requestAudioFocus(audioFocusRequest); + } else { + focusGained = audioManager.requestAudioFocus( + audioFocusChangeListener, AudioManager.STREAM_MUSIC, + AudioManager.AUDIOFOCUS_GAIN); + } + if (focusGained == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { Log.d(TAG, "Audiofocus successfully requested"); Log.d(TAG, "Resuming/Starting playback"); @@ -256,7 +279,13 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { setPlayerStatus(PlayerStatus.PAUSED, media, getPosition()); if (abandonFocus) { - audioManager.abandonAudioFocus(audioFocusChangeListener); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + AudioFocusRequest.Builder builder = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN) + .setOnAudioFocusChangeListener(audioFocusChangeListener); + audioManager.abandonAudioFocusRequest(builder.build()); + } else { + audioManager.abandonAudioFocus(audioFocusChangeListener); + } pausedBecauseOfTransientAudiofocusLoss = false; } if (stream && reinit) { @@ -310,7 +339,10 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { Log.d(TAG, "Resource prepared"); - if (mediaType == MediaType.VIDEO) { + if (mediaType == MediaType.VIDEO && mediaPlayer instanceof ExoPlayerWrapper) { + ExoPlayerWrapper vp = (ExoPlayerWrapper) mediaPlayer; + videoSize = new Pair<>(vp.getVideoWidth(), vp.getVideoHeight()); + } else if(mediaType == MediaType.VIDEO && mediaPlayer instanceof VideoPlayer) { VideoPlayer vp = (VideoPlayer) mediaPlayer; videoSize = new Pair<>(vp.getVideoWidth(), vp.getVideoHeight()); } @@ -444,7 +476,8 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { || playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PREPARED) { retVal = mediaPlayer.getDuration(); - } else if (media != null && media.getDuration() > 0) { + } + if (retVal <= 0 && media != null && media.getDuration() > 0) { retVal = media.getDuration(); } @@ -606,11 +639,30 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { public void shutdown() { executor.shutdown(); if (mediaPlayer != null) { + try { + removeMediaPlayerErrorListener(); + if (mediaPlayer.isPlaying()) { + mediaPlayer.stop(); + } + } catch (Exception ignore) { } mediaPlayer.release(); } releaseWifiLockIfNecessary(); } + private void removeMediaPlayerErrorListener() { + if (mediaPlayer instanceof VideoPlayer) { + VideoPlayer vp = (VideoPlayer) mediaPlayer; + vp.setOnErrorListener((mp, what, extra) -> true); + } else if (mediaPlayer instanceof AudioPlayer) { + AudioPlayer ap = (AudioPlayer) mediaPlayer; + ap.setOnErrorListener((mediaPlayer, i, i1) -> true); + } else if (mediaPlayer instanceof ExoPlayerWrapper) { + ExoPlayerWrapper ap = (ExoPlayerWrapper) mediaPlayer; + ap.setOnErrorListener((mediaPlayer, i, i1) -> true); + } + } + /** * Releases internally used resources. This method should only be called when the object is not used anymore. * This method is executed on an internal executor service. @@ -663,6 +715,10 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { Pair<Integer, Integer> res; if (mediaPlayer == null || playerStatus == PlayerStatus.ERROR || mediaType != MediaType.VIDEO) { res = null; + } else if (mediaPlayer instanceof ExoPlayerWrapper) { + ExoPlayerWrapper vp = (ExoPlayerWrapper) mediaPlayer; + videoSize = new Pair<>(vp.getVideoWidth(), vp.getVideoHeight()); + res = videoSize; } else { VideoPlayer vp = (VideoPlayer) mediaPlayer; videoSize = new Pair<>(vp.getVideoWidth(), vp.getVideoHeight()); @@ -692,15 +748,19 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { if (mediaPlayer != null) { mediaPlayer.release(); } - if(media == null) { + if (media == null) { mediaPlayer = null; return; } - if (media.getMediaType() == MediaType.VIDEO) { + + if (UserPreferences.useExoplayer()) { + mediaPlayer = new ExoPlayerWrapper(context); + } else if (media.getMediaType() == MediaType.VIDEO) { mediaPlayer = new VideoPlayer(); } else { mediaPlayer = new AudioPlayer(context); } + mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); mediaPlayer.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK); setMediaPlayerListeners(mediaPlayer); @@ -710,52 +770,49 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { @Override public void onAudioFocusChange(final int focusChange) { - executor.submit(new Runnable() { - @Override - public void run() { - playerLock.lock(); - - // If there is an incoming call, playback should be paused permanently - TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); - final int callState = (tm != null) ? tm.getCallState() : 0; - Log.i(TAG, "Call state:" + callState); - - 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) { - Log.d(TAG, "Gained audio focus"); - if (pausedBecauseOfTransientAudiofocusLoss) { // we paused => play now - resume(); - } else { // we ducked => raise audio level back - setVolumeSync(UserPreferences.getLeftVolume(), - UserPreferences.getRightVolume()); - } - } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) { - if (playerStatus == PlayerStatus.PLAYING) { - if (!UserPreferences.shouldPauseForFocusLoss()) { - Log.d(TAG, "Lost audio focus temporarily. Ducking..."); - final float DUCK_FACTOR = 0.25f; - setVolumeSync(DUCK_FACTOR * UserPreferences.getLeftVolume(), - DUCK_FACTOR * UserPreferences.getRightVolume()); - pausedBecauseOfTransientAudiofocusLoss = false; - } else { - 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) { - Log.d(TAG, "Lost audio focus temporarily. Pausing..."); + executor.submit(() -> { + playerLock.lock(); + + // If there is an incoming call, playback should be paused permanently + TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + final int callState = (tm != null) ? tm.getCallState() : 0; + Log.i(TAG, "Call state:" + callState); + + 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) { + Log.d(TAG, "Gained audio focus"); + if (pausedBecauseOfTransientAudiofocusLoss) { // we paused => play now + resume(); + } else { // we ducked => raise audio level back + setVolumeSync(UserPreferences.getLeftVolume(), + UserPreferences.getRightVolume()); + } + } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) { + if (playerStatus == PlayerStatus.PLAYING) { + if (!UserPreferences.shouldPauseForFocusLoss()) { + Log.d(TAG, "Lost audio focus temporarily. Ducking..."); + final float DUCK_FACTOR = 0.25f; + setVolumeSync(DUCK_FACTOR * UserPreferences.getLeftVolume(), + DUCK_FACTOR * UserPreferences.getRightVolume()); + pausedBecauseOfTransientAudiofocusLoss = false; + } else { + Log.d(TAG, "Lost audio focus temporarily. Could duck, but won't, pausing..."); pause(false, false); pausedBecauseOfTransientAudiofocusLoss = true; } } - playerLock.unlock(); + } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) { + if (playerStatus == PlayerStatus.PLAYING) { + Log.d(TAG, "Lost audio focus temporarily. Pausing..."); + pause(false, false); + pausedBecauseOfTransientAudiofocusLoss = true; + } } + playerLock.unlock(); }); } }; @@ -784,7 +841,14 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { if (mediaPlayer != null) { mediaPlayer.reset(); } - audioManager.abandonAudioFocus(audioFocusChangeListener); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + AudioFocusRequest.Builder builder = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN) + .setOnAudioFocusChangeListener(audioFocusChangeListener); + audioManager.abandonAudioFocusRequest(builder.build()); + } else { + audioManager.abandonAudioFocus(audioFocusChangeListener); + } final Playable currentMedia = media; Playable nextMedia = null; @@ -880,6 +944,11 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { ap.setOnBufferingUpdateListener(audioBufferingUpdateListener); ap.setOnInfoListener(audioInfoListener); ap.setOnSpeedAdjustmentAvailableChangedListener(audioSetSpeedAbilityListener); + } else if (mp instanceof ExoPlayerWrapper) { + ExoPlayerWrapper ap = (ExoPlayerWrapper) mp; + ap.setOnCompletionListener(audioCompletionListener); + ap.setOnSeekCompleteListener(audioSeekCompleteListener); + ap.setOnErrorListener(audioErrorListener); } else { Log.w(TAG, "Unknown media player: " + mp); } 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 01c5f751e..979857381 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 @@ -30,12 +30,10 @@ import android.support.v4.media.MediaDescriptionCompat; import android.support.v4.media.MediaMetadataCompat; import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.PlaybackStateCompat; -import android.support.v4.view.InputDeviceCompat; -import android.support.v7.app.NotificationCompat; +import android.support.v4.app.NotificationCompat; import android.text.TextUtils; import android.util.Log; import android.util.Pair; -import android.view.InputDevice; import android.view.KeyEvent; import android.view.SurfaceHolder; import android.widget.Toast; @@ -49,6 +47,7 @@ import java.util.List; import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.R; import de.danoeh.antennapod.core.event.MessageEvent; +import de.danoeh.antennapod.core.event.ServiceEvent; import de.danoeh.antennapod.core.feed.Chapter; import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.feed.FeedItem; @@ -60,11 +59,14 @@ import de.danoeh.antennapod.core.preferences.PlaybackPreferences; import de.danoeh.antennapod.core.preferences.SleepTimerPreferences; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.receiver.MediaButtonReceiver; +import de.danoeh.antennapod.core.service.PlayerWidgetJobService; 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.FeedSearcher; import de.danoeh.antennapod.core.util.IntList; +import de.danoeh.antennapod.core.util.IntentUtils; +import de.danoeh.antennapod.core.util.gui.NotificationUtils; import de.danoeh.antennapod.core.util.QueueAccess; import de.danoeh.antennapod.core.util.playback.ExternalMedia; import de.danoeh.antennapod.core.util.playback.Playable; @@ -74,8 +76,6 @@ import de.greenrobot.event.EventBus; * Controls the MediaPlayer that plays a FeedMedia-file */ public class PlaybackService extends MediaBrowserServiceCompat { - public static final String FORCE_WIDGET_UPDATE = "de.danoeh.antennapod.FORCE_WIDGET_UPDATE"; - public static final String STOP_WIDGET_UPDATE = "de.danoeh.antennapod.STOP_WIDGET_UPDATE"; /** * Logging tag */ @@ -88,7 +88,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { /** * True if cast session should disconnect. */ - public static final String EXTRA_CAST_DISCONNECT = "extra.de.danoeh.antennapod.core.service.castDisconnect"; + private static final String EXTRA_CAST_DISCONNECT = "extra.de.danoeh.antennapod.core.service.castDisconnect"; /** * True if media should be streamed. */ @@ -198,7 +198,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { /** * Is true if the service was running, but paused due to headphone disconnect */ - public static boolean transientPause = false; + private static boolean transientPause = false; /** * Is true if a Cast Device is connected to the service. */ @@ -263,32 +263,24 @@ public class PlaybackService extends MediaBrowserServiceCompat { Log.d(TAG, "Service created."); isRunning = true; - registerReceiver(autoStateUpdated, new IntentFilter( - "com.google.android.gms.car.media.STATUS")); - registerReceiver(headsetDisconnected, new IntentFilter( - Intent.ACTION_HEADSET_PLUG)); - registerReceiver(shutdownReceiver, new IntentFilter( - ACTION_SHUTDOWN_PLAYBACK_SERVICE)); - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { - registerReceiver(bluetoothStateUpdated, new IntentFilter( - BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED)); - } - registerReceiver(audioBecomingNoisy, new IntentFilter( - AudioManager.ACTION_AUDIO_BECOMING_NOISY)); - registerReceiver(skipCurrentEpisodeReceiver, new IntentFilter( - ACTION_SKIP_CURRENT_EPISODE)); - registerReceiver(pausePlayCurrentEpisodeReceiver, new IntentFilter( - ACTION_PAUSE_PLAY_CURRENT_EPISODE)); - registerReceiver(pauseResumeCurrentEpisodeReceiver, new IntentFilter( - ACTION_RESUME_PLAY_CURRENT_EPISODE)); + NotificationCompat.Builder notificationBuilder = createBasicNotification(); + startForeground(NOTIFICATION_ID, notificationBuilder.build()); + + registerReceiver(autoStateUpdated, new IntentFilter("com.google.android.gms.car.media.STATUS")); + registerReceiver(headsetDisconnected, new IntentFilter(Intent.ACTION_HEADSET_PLUG)); + registerReceiver(shutdownReceiver, new IntentFilter(ACTION_SHUTDOWN_PLAYBACK_SERVICE)); + registerReceiver(bluetoothStateUpdated, new IntentFilter(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED)); + registerReceiver(audioBecomingNoisy, new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY)); + registerReceiver(skipCurrentEpisodeReceiver, new IntentFilter(ACTION_SKIP_CURRENT_EPISODE)); + registerReceiver(pausePlayCurrentEpisodeReceiver, new IntentFilter(ACTION_PAUSE_PLAY_CURRENT_EPISODE)); + registerReceiver(pauseResumeCurrentEpisodeReceiver, new IntentFilter(ACTION_RESUME_PLAY_CURRENT_EPISODE)); taskManager = new PlaybackServiceTaskManager(this, taskManagerCallback); flavorHelper = new PlaybackServiceFlavorHelper(PlaybackService.this, flavorHelperCallback); PreferenceManager.getDefaultSharedPreferences(this) .registerOnSharedPreferenceChangeListener(prefListener); - ComponentName eventReceiver = new ComponentName(getApplicationContext(), - MediaButtonReceiver.class); + ComponentName eventReceiver = new ComponentName(getApplicationContext(), MediaButtonReceiver.class); Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); mediaButtonIntent.setComponent(eventReceiver); PendingIntent buttonReceiverIntent = PendingIntent.getBroadcast(this, 0, mediaButtonIntent, PendingIntent.FLAG_UPDATE_CURRENT); @@ -311,7 +303,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { List<MediaSessionCompat.QueueItem> queueItems = new ArrayList<>(); try { for (FeedItem feedItem : taskManager.getQueue()) { - if(feedItem.getMedia() != null) { + if (feedItem.getMedia() != null) { MediaDescriptionCompat mediaDescription = feedItem.getMedia().getMediaItem().getDescription(); queueItems.add(new MediaSessionCompat.QueueItem(mediaDescription, feedItem.getId())); } @@ -322,14 +314,34 @@ public class PlaybackService extends MediaBrowserServiceCompat { } flavorHelper.initializeMediaPlayer(PlaybackService.this); - mediaSession.setActive(true); + + EventBus.getDefault().post(new ServiceEvent(ServiceEvent.Action.SERVICE_STARTED)); + } + + private NotificationCompat.Builder createBasicNotification() { + final int smallIcon = ClientConfig.playbackServiceCallbacks.getNotificationIconResource(getApplicationContext()); + + final PendingIntent pIntent = PendingIntent.getActivity(this, 0, + PlaybackService.getPlayerActivityIntent(this), + PendingIntent.FLAG_UPDATE_CURRENT); + + return new NotificationCompat.Builder( + this, NotificationUtils.CHANNEL_ID_PLAYING) + .setContentTitle(getString(R.string.app_name)) + .setContentText("Service is running") // Just in case the notification is not updated (should not occur) + .setOngoing(false) + .setContentIntent(pIntent) + .setWhen(0) // we don't need the time + .setSmallIcon(smallIcon) + .setPriority(NotificationCompat.PRIORITY_MIN); } @Override public void onDestroy() { super.onDestroy(); Log.d(TAG, "Service is about to be destroyed"); + stopForeground(true); isRunning = false; started = false; currentMediaType = MediaType.UNKNOWN; @@ -342,9 +354,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { unregisterReceiver(autoStateUpdated); unregisterReceiver(headsetDisconnected); unregisterReceiver(shutdownReceiver); - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { - unregisterReceiver(bluetoothStateUpdated); - } + unregisterReceiver(bluetoothStateUpdated); unregisterReceiver(audioBecomingNoisy); unregisterReceiver(skipCurrentEpisodeReceiver); unregisterReceiver(pausePlayCurrentEpisodeReceiver); @@ -354,6 +364,11 @@ public class PlaybackService extends MediaBrowserServiceCompat { mediaPlayer.shutdown(); taskManager.shutdown(); } + + private void stopService() { + stopForeground(true); + stopSelf(); + } @Override public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid, Bundle rootHints) { @@ -379,10 +394,10 @@ public class PlaybackService extends MediaBrowserServiceCompat { .setTitle(feed.getTitle()) .setDescription(feed.getDescription()) .setSubtitle(feed.getCustomTitle()); - if(feed.getImageLocation() != null) { + if (feed.getImageLocation() != null) { builder.setIconUri(Uri.parse(feed.getImageLocation())); } - if(feed.getLink() != null) { + if (feed.getLink() != null) { builder.setMediaUri(Uri.parse(feed.getLink())); } MediaDescriptionCompat description = builder.build(); @@ -405,13 +420,13 @@ public class PlaybackService extends MediaBrowserServiceCompat { e.printStackTrace(); } List<Feed> feeds = DBReader.getFeedList(); - for (Feed feed: feeds) { + for (Feed feed : feeds) { mediaItems.add(createBrowsableMediaItemForFeed(feed)); } - } else if (parentId.equals(getResources().getString(R.string.queue_label))){ + } else if (parentId.equals(getResources().getString(R.string.queue_label))) { // Child List try { - for (FeedItem feedItem: taskManager.getQueue()) { + for (FeedItem feedItem : taskManager.getQueue()) { mediaItems.add(feedItem.getMedia().getMediaItem()); } } catch (InterruptedException e) { @@ -420,8 +435,8 @@ public class PlaybackService extends MediaBrowserServiceCompat { } else if (parentId.startsWith("FeedId:")) { Long feedId = Long.parseLong(parentId.split(":")[1]); List<FeedItem> feedItems = DBReader.getFeedItemList(DBReader.getFeed(feedId)); - for (FeedItem feedItem: feedItems) { - if(feedItem.getMedia() != null && feedItem.getMedia().getMediaItem() != null) { + for (FeedItem feedItem : feedItems) { + if (feedItem.getMedia() != null && feedItem.getMedia().getMediaItem() != null) { mediaItems.add(feedItem.getMedia().getMediaItem()); } } @@ -432,7 +447,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { @Override public IBinder onBind(Intent intent) { Log.d(TAG, "Received onBind event"); - if(intent.getAction() != null && TextUtils.equals(intent.getAction(), MediaBrowserServiceCompat.SERVICE_INTERFACE)) { + if (intent.getAction() != null && TextUtils.equals(intent.getAction(), MediaBrowserServiceCompat.SERVICE_INTERFACE)) { return super.onBind(intent); } else { return mBinder; @@ -449,20 +464,22 @@ public class PlaybackService extends MediaBrowserServiceCompat { Playable playable = intent.getParcelableExtra(EXTRA_PLAYABLE); if (keycode == -1 && playable == null && !castDisconnect) { Log.e(TAG, "PlaybackService was started with no arguments"); - stopSelf(); - return Service.START_REDELIVER_INTENT; + stopService(); + return Service.START_NOT_STICKY; } if ((flags & Service.START_FLAG_REDELIVERY) != 0) { Log.d(TAG, "onStartCommand is a redelivered intent, calling stopForeground now."); stopForeground(true); } else { - if (keycode != -1) { Log.d(TAG, "Received media button event"); - handleKeycode(keycode, intent.getIntExtra(MediaButtonReceiver.EXTRA_SOURCE, - InputDeviceCompat.SOURCE_CLASS_NONE)); - } else if (!flavorHelper.castDisconnect(castDisconnect)) { + boolean handled = handleKeycode(keycode, true); + if (!handled) { + stopService(); + return Service.START_NOT_STICKY; + } + } else if (!flavorHelper.castDisconnect(castDisconnect) && playable != null) { started = true; boolean stream = intent.getBooleanExtra(EXTRA_SHOULD_STREAM, true); @@ -471,20 +488,22 @@ public class PlaybackService extends MediaBrowserServiceCompat { sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0); //If the user asks to play External Media, the casting session, if on, should end. flavorHelper.castDisconnect(playable instanceof ExternalMedia); - if(playable instanceof FeedMedia){ - playable = (Playable) DBReader.getFeedMedia(((FeedMedia)playable).getId()); + if (playable instanceof FeedMedia) { + playable = DBReader.getFeedMedia(((FeedMedia) playable).getId()); } mediaPlayer.playMediaObject(playable, stream, startWhenPrepared, prepareImmediately); } + setupNotification(playable); } - return Service.START_REDELIVER_INTENT; + return Service.START_NOT_STICKY; } /** * Handles media button events + * return: keycode was handled */ - private void handleKeycode(int keycode, int source) { + private boolean handleKeycode(int keycode, boolean notificationButton) { Log.d(TAG, "Handling keycode: " + keycode); final PlaybackServiceMediaPlayer.PSMPInfo info = mediaPlayer.getPSMPInfo(); final PlayerStatus status = info.playerStatus; @@ -500,24 +519,28 @@ public class PlaybackService extends MediaBrowserServiceCompat { } else if (status == PlayerStatus.INITIALIZED) { mediaPlayer.setStartWhenPrepared(true); mediaPlayer.prepare(); + } else if (mediaPlayer.getPlayable() == null) { + startPlayingFromPreferences(); } - break; + return true; case KeyEvent.KEYCODE_MEDIA_PLAY: if (status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED) { mediaPlayer.resume(); } else if (status == PlayerStatus.INITIALIZED) { mediaPlayer.setStartWhenPrepared(true); mediaPlayer.prepare(); + } else if (mediaPlayer.getPlayable() == null) { + startPlayingFromPreferences(); } - break; + return true; case KeyEvent.KEYCODE_MEDIA_PAUSE: if (status == PlayerStatus.PLAYING) { mediaPlayer.pause(!UserPreferences.isPersistNotify(), true); } - break; + return true; case KeyEvent.KEYCODE_MEDIA_NEXT: - if(source == InputDevice.SOURCE_CLASS_NONE || + if (notificationButton || UserPreferences.shouldHardwareButtonSkip()) { // assume the skip command comes from a notification or the lockscreen // a >| skip button should actually skip @@ -527,22 +550,22 @@ public class PlaybackService extends MediaBrowserServiceCompat { // user actually wants to fast-forward seekDelta(UserPreferences.getFastForwardSecs() * 1000); } - break; + return true; case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: mediaPlayer.seekDelta(UserPreferences.getFastForwardSecs() * 1000); - break; + return true; case KeyEvent.KEYCODE_MEDIA_PREVIOUS: - if(UserPreferences.shouldHardwarePreviousButtonRestart()) { + if (UserPreferences.shouldHardwarePreviousButtonRestart()) { // user wants to restart current episode mediaPlayer.seekTo(0); } else { // user wants to rewind current episode mediaPlayer.seekDelta(-UserPreferences.getRewindSecs() * 1000); } - break; + return true; case KeyEvent.KEYCODE_MEDIA_REWIND: mediaPlayer.seekDelta(-UserPreferences.getRewindSecs() * 1000); - break; + return true; case KeyEvent.KEYCODE_MEDIA_STOP: if (status == PlayerStatus.PLAYING) { mediaPlayer.pause(true, true); @@ -550,14 +573,23 @@ public class PlaybackService extends MediaBrowserServiceCompat { } stopForeground(true); // gets rid of persistent notification - break; + return true; default: Log.d(TAG, "Unhandled key code: " + keycode); if (info.playable != null && info.playerStatus == PlayerStatus.PLAYING) { // only notify the user about an unknown key event if it is actually doing something String message = String.format(getResources().getString(R.string.unknown_media_key), keycode); Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); } - break; + } + return false; + } + + private void startPlayingFromPreferences() { + Playable playable = Playable.PlayableUtils.createInstanceFromPreferences(getApplicationContext()); + if (playable != null) { + mediaPlayer.playMediaObject(playable, false, true, true); + started = true; + PlaybackService.this.updateMediaSessionMetadata(playable); } } @@ -579,8 +611,10 @@ public class PlaybackService extends MediaBrowserServiceCompat { } public void notifyVideoSurfaceAbandoned() { - stopForeground(!UserPreferences.isPersistNotify()); + mediaPlayer.pause(true, false); mediaPlayer.resetVideoSurface(); + setupNotification(getPlayable()); + stopForeground(!UserPreferences.isPersistNotify()); } private final PlaybackServiceTaskManager.PSTMCallback taskManagerCallback = new PlaybackServiceTaskManager.PSTMCallback() { @@ -614,7 +648,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { @Override public void onWidgetUpdaterTick() { - updateWidget(); + PlayerWidgetJobService.updateWidget(getBaseContext()); } @Override @@ -626,7 +660,12 @@ public class PlaybackService extends MediaBrowserServiceCompat { private final PlaybackServiceMediaPlayer.PSMPCallback mediaPlayerCallback = new PlaybackServiceMediaPlayer.PSMPCallback() { @Override public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) { - currentMediaType = mediaPlayer.getCurrentMediaType(); + if (mediaPlayer != null) { + currentMediaType = mediaPlayer.getCurrentMediaType(); + } else { + currentMediaType = MediaType.UNKNOWN; + } + updateMediaSession(newInfo.playerStatus); switch (newInfo.playerStatus) { case INITIALIZED: @@ -651,8 +690,8 @@ public class PlaybackService extends MediaBrowserServiceCompat { break; case STOPPED: - //setCurrentlyPlayingMedia(PlaybackPreferences.NO_MEDIA_PLAYING); - //stopSelf(); + //writePlaybackPreferencesNoMediaPlaying(); + //stopService(); break; case PLAYING: @@ -660,8 +699,8 @@ public class PlaybackService extends MediaBrowserServiceCompat { setupNotification(newInfo); started = true; // set sleep timer if auto-enabled - if(newInfo.oldPlayerStatus != null && newInfo.oldPlayerStatus != PlayerStatus.SEEKING && - SleepTimerPreferences.autoEnable() && !sleepTimerActive()) { + if (newInfo.oldPlayerStatus != null && newInfo.oldPlayerStatus != PlayerStatus.SEEKING && + SleepTimerPreferences.autoEnable() && !sleepTimerActive()) { setSleepTimer(SleepTimerPreferences.timerMillis(), SleepTimerPreferences.shakeToReset(), SleepTimerPreferences.vibrate()); } @@ -669,21 +708,20 @@ public class PlaybackService extends MediaBrowserServiceCompat { case ERROR: writePlaybackPreferencesNoMediaPlaying(); + stopService(); break; } - Intent statusUpdate = new Intent(ACTION_PLAYER_STATUS_CHANGED); - // statusUpdate.putExtra(EXTRA_NEW_PLAYER_STATUS, newInfo.playerStatus.ordinal()); - sendBroadcast(statusUpdate); - updateWidget(); + IntentUtils.sendLocalBroadcast(getApplicationContext(), ACTION_PLAYER_STATUS_CHANGED); + PlayerWidgetJobService.updateWidget(getBaseContext()); bluetoothNotifyChange(newInfo, AVRCP_ACTION_PLAYER_STATUS_CHANGED); bluetoothNotifyChange(newInfo, AVRCP_ACTION_META_CHANGED); } @Override public void shouldStop() { - stopSelf(); + stopService(); } @Override @@ -732,7 +770,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { } sendNotificationBroadcast(NOTIFICATION_TYPE_ERROR, what); writePlaybackPreferencesNoMediaPlaying(); - stopSelf(); + stopService(); return true; } @@ -804,7 +842,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { Log.e(TAG, "Error handling the queue in order to retrieve the next item", e); return null; } - return (nextItem != null)? nextItem.getMedia() : null; + return (nextItem != null) ? nextItem.getMedia() : null; } @@ -819,7 +857,6 @@ public class PlaybackService extends MediaBrowserServiceCompat { if (!isCasting) { stopForeground(true); } - stopWidgetUpdater(); } if (mediaType == null) { sendNotificationBroadcast(NOTIFICATION_TYPE_PLAYBACK_END, 0); @@ -833,7 +870,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { /** * This method processes the media object after its playback ended, either because it completed * or because a different media object was selected for playback. - * + * <p> * Even though these tasks aren't supposed to be resource intensive, a good practice is to * usually call this method on a background thread. * @@ -913,7 +950,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { taskManager.setSleepTimer(waitingTime, shakeToReset, vibrate); sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0); EventBus.getDefault().post(new MessageEvent(getString(R.string.sleep_timer_enabled_label), - () -> disableSleepTimer())); + this::disableSleepTimer)); } public void disableSleepTimer() { @@ -1017,22 +1054,17 @@ public class PlaybackService extends MediaBrowserServiceCompat { editor.commit(); } - /** - * Send ACTION_PLAYER_STATUS_CHANGED without changing the status attribute. - */ - private void postStatusUpdateIntent() { - sendBroadcast(new Intent(ACTION_PLAYER_STATUS_CHANGED)); - } - private void sendNotificationBroadcast(int type, int code) { Intent intent = new Intent(ACTION_PLAYER_NOTIFICATION); intent.putExtra(EXTRA_NOTIFICATION_TYPE, type); intent.putExtra(EXTRA_NOTIFICATION_CODE, code); + intent.setPackage(getPackageName()); sendBroadcast(intent); } /** * Updates the Media Session for the corresponding status. + * * @param playerStatus the current {@link PlayerStatus} */ private void updateMediaSession(final PlayerStatus playerStatus) { @@ -1108,8 +1140,8 @@ public class PlaybackService extends MediaBrowserServiceCompat { // showRewindOnCompactNotification() corresponds to the "Set Lockscreen Buttons" // Settings in UI. // Hence, from user perspective, he/she is setting the buttons for Lockscreen - return ( UserPreferences.showRewindOnCompactNotification() && - (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) ); + return (UserPreferences.showRewindOnCompactNotification() && + (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)); } /** @@ -1161,7 +1193,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { PendingIntent.FLAG_UPDATE_CURRENT)); try { mediaSession.setMetadata(builder.build()); - } catch (OutOfMemoryError e) { + } catch (OutOfMemoryError e) { Log.e(TAG, "Setting media session metadata", e); builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, null); mediaSession.setMetadata(builder.build()); @@ -1182,63 +1214,67 @@ public class PlaybackService extends MediaBrowserServiceCompat { * Prepares notification and starts the service in the foreground. */ private void setupNotification(final PlaybackServiceMediaPlayer.PSMPInfo info) { - final PendingIntent pIntent = PendingIntent.getActivity(this, 0, - PlaybackService.getPlayerActivityIntent(this), - PendingIntent.FLAG_UPDATE_CURRENT); + setupNotification(info.playable); + } + private synchronized void setupNotification(final Playable playable) { if (notificationSetupThread != null) { notificationSetupThread.interrupt(); } + if (playable == null) { + Log.d(TAG, "setupNotification: playable is null"); + if (!started) { + stopService(); + } + return; + } Runnable notificationSetupTask = new Runnable() { Bitmap icon = null; @Override public void run() { Log.d(TAG, "Starting background work"); - if (android.os.Build.VERSION.SDK_INT >= 11) { - if (info.playable != null) { - int iconSize = getResources().getDimensionPixelSize( - android.R.dimen.notification_large_icon_width); - try { - icon = Glide.with(PlaybackService.this) - .load(info.playable.getImageLocation()) - .asBitmap() - .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY) - .centerCrop() - .into(iconSize, iconSize) - .get(); - } catch (Throwable tr) { - Log.e(TAG, "Error loading the media icon for the notification", tr); - } + + if (mediaPlayer == null) { + Log.d(TAG, "notificationSetupTask: mediaPlayer is null"); + if (!started) { + stopService(); } + return; } + + int iconSize = getResources().getDimensionPixelSize( + android.R.dimen.notification_large_icon_width); + try { + icon = Glide.with(PlaybackService.this) + .load(playable.getImageLocation()) + .asBitmap() + .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY) + .centerCrop() + .into(iconSize, iconSize) + .get(); + } catch (Throwable tr) { + Log.e(TAG, "Error loading the media icon for the notification", tr); + } + if (icon == null) { icon = BitmapFactory.decodeResource(getApplicationContext().getResources(), ClientConfig.playbackServiceCallbacks.getNotificationIconResource(getApplicationContext())); } - if (mediaPlayer == null) { - return; - } PlayerStatus playerStatus = mediaPlayer.getPlayerStatus(); - final int smallIcon = ClientConfig.playbackServiceCallbacks.getNotificationIconResource(getApplicationContext()); - if (!Thread.currentThread().isInterrupted() && started && info.playable != null) { - String contentText = info.playable.getEpisodeTitle(); - String contentTitle = info.playable.getFeedTitle(); + if (!Thread.currentThread().isInterrupted() && started) { + String contentText = playable.getEpisodeTitle(); + String contentTitle = playable.getFeedTitle(); Notification notification; // Builder is v7, even if some not overwritten methods return its parent's v4 interface - NotificationCompat.Builder notificationBuilder = (NotificationCompat.Builder) new NotificationCompat.Builder( - PlaybackService.this) - .setContentTitle(contentTitle) + NotificationCompat.Builder notificationBuilder = createBasicNotification(); + notificationBuilder.setContentTitle(contentTitle) .setContentText(contentText) - .setOngoing(false) - .setContentIntent(pIntent) - .setLargeIcon(icon) - .setSmallIcon(smallIcon) - .setWhen(0) // we don't need the time - .setPriority(UserPreferences.getNotifyPriority()); // set notification priority + .setPriority(UserPreferences.getNotifyPriority()) + .setLargeIcon(icon); // set notification priority IntList compactActionList = new IntList(); int numActions = 0; // we start and 0 and then increment by 1 for each call to addAction @@ -1260,7 +1296,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { notificationBuilder.addAction(android.R.drawable.ic_media_rew, getString(R.string.rewind_label), rewindButtonPendingIntent); - if(UserPreferences.showRewindOnCompactNotification()) { + if (UserPreferences.showRewindOnCompactNotification()) { compactActionList.add(numActions); } numActions++; @@ -1287,7 +1323,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { notificationBuilder.addAction(android.R.drawable.ic_media_ff, getString(R.string.fast_forward_label), ffButtonPendingIntent); - if(UserPreferences.showFastForwardOnCompactNotification()) { + if (UserPreferences.showFastForwardOnCompactNotification()) { compactActionList.add(numActions); } numActions++; @@ -1298,7 +1334,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { notificationBuilder.addAction(android.R.drawable.ic_media_next, getString(R.string.skip_episode_label), skipButtonPendingIntent); - if(UserPreferences.showSkipOnCompactNotification()) { + if (UserPreferences.showSkipOnCompactNotification()) { compactActionList.add(numActions); } numActions++; @@ -1306,7 +1342,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { PendingIntent stopButtonPendingIntent = getPendingIntentForMediaAction( KeyEvent.KEYCODE_MEDIA_STOP, numActions); - notificationBuilder.setStyle(new android.support.v7.app.NotificationCompat.MediaStyle() + notificationBuilder.setStyle(new android.support.v4.media.app.NotificationCompat.MediaStyle() .setMediaSession(mediaSession.getSessionToken()) .setShowActionsInCompactView(compactActionList.toArray()) .setShowCancelButton(true) @@ -1351,9 +1387,9 @@ public class PlaybackService extends MediaBrowserServiceCompat { * * @param fromMediaPlayer if true, the information is gathered from the current Media Player * and {@param playable} and {@param position} become irrelevant. - * @param playable the playable for which the current position should be saved, unless - * {@param fromMediaPlayer} is true. - * @param position the position that should be saved, unless {@param fromMediaPlayer} is true. + * @param playable the playable for which the current position should be saved, unless + * {@param fromMediaPlayer} is true. + * @param position the position that should be saved, unless {@param fromMediaPlayer} is true. */ private synchronized void saveCurrentPosition(boolean fromMediaPlayer, Playable playable, int position) { int duration; @@ -1373,16 +1409,6 @@ public class PlaybackService extends MediaBrowserServiceCompat { } } - private void stopWidgetUpdater() { - taskManager.cancelWidgetUpdater(); - sendBroadcast(new Intent(STOP_WIDGET_UPDATE)); - } - - private void updateWidget() { - PlaybackService.this.sendBroadcast(new Intent( - FORCE_WIDGET_UPDATE)); - } - public boolean sleepTimerActive() { return taskManager.isSleepTimerActive(); } @@ -1421,7 +1447,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { String status = intent.getStringExtra("media_connection_status"); boolean isConnectedToCar = "media_connected".equals(status); Log.d(TAG, "Received Auto Connection update: " + status); - if(!isConnectedToCar) { + if (!isConnectedToCar) { Log.d(TAG, "Car was unplugged during playback."); pauseIfPauseOnDisconnect(); } else { @@ -1449,6 +1475,12 @@ public class PlaybackService extends MediaBrowserServiceCompat { @Override public void onReceive(Context context, Intent intent) { + if (isInitialStickyBroadcast ()) { + // Don't pause playback after we just started, just because the receiver + // delivers the current headset state (instead of a change) + return; + } + if (TextUtils.equals(intent.getAction(), Intent.ACTION_HEADSET_PLUG)) { int state = intent.getIntExtra("state", -1); if (state != -1) { @@ -1470,13 +1502,11 @@ public class PlaybackService extends MediaBrowserServiceCompat { private final BroadcastReceiver bluetoothStateUpdated = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { - if (TextUtils.equals(intent.getAction(), BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED)) { - int state = intent.getIntExtra(BluetoothA2dp.EXTRA_STATE, -1); - if (state == BluetoothA2dp.STATE_CONNECTED) { - Log.d(TAG, "Received bluetooth connection intent"); - unpauseIfPauseOnDisconnect(true); - } + if (TextUtils.equals(intent.getAction(), BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED)) { + int state = intent.getIntExtra(BluetoothA2dp.EXTRA_STATE, -1); + if (state == BluetoothA2dp.STATE_CONNECTED) { + Log.d(TAG, "Received bluetooth connection intent"); + unpauseIfPauseOnDisconnect(true); } } } @@ -1513,10 +1543,10 @@ public class PlaybackService extends MediaBrowserServiceCompat { transientPause = false; if (!bluetooth && UserPreferences.isUnpauseOnHeadsetReconnect()) { mediaPlayer.resume(); - } else if (bluetooth && UserPreferences.isUnpauseOnBluetoothReconnect()){ + } else if (bluetooth && UserPreferences.isUnpauseOnBluetoothReconnect()) { // let the user know we've started playback again... Vibrator v = (Vibrator) getApplicationContext().getSystemService(Context.VIBRATOR_SERVICE); - if(v != null) { + if (v != null) { v.vibrate(500); } mediaPlayer.resume(); @@ -1529,7 +1559,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { @Override public void onReceive(Context context, Intent intent) { if (TextUtils.equals(intent.getAction(), ACTION_SHUTDOWN_PLAYBACK_SERVICE)) { - stopSelf(); + stopService(); } } @@ -1597,7 +1627,9 @@ public class PlaybackService extends MediaBrowserServiceCompat { return mediaPlayer.getPlayerStatus(); } - public Playable getPlayable() { return mediaPlayer.getPlayable(); } + public Playable getPlayable() { + return mediaPlayer.getPlayable(); + } public boolean canSetSpeed() { return mediaPlayer.canSetSpeed(); @@ -1637,7 +1669,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { } - public void seekDelta(final int d) { + private void seekDelta(final int d) { mediaPlayer.seekDelta(d); } @@ -1653,6 +1685,9 @@ public class PlaybackService extends MediaBrowserServiceCompat { * an invalid state. */ public int getDuration() { + if (mediaPlayer == null) { + return INVALID_TIME; + } return mediaPlayer.getDuration(); } @@ -1661,6 +1696,9 @@ public class PlaybackService extends MediaBrowserServiceCompat { * is in an invalid state. */ public int getCurrentPosition() { + if (mediaPlayer == null) { + return INVALID_TIME; + } return mediaPlayer.getPosition(); } @@ -1692,19 +1730,19 @@ public class PlaybackService extends MediaBrowserServiceCompat { public void onPlayFromMediaId(String mediaId, Bundle extras) { Log.d(TAG, "onPlayFromMediaId: mediaId: " + mediaId + " extras: " + extras.toString()); FeedMedia p = DBReader.getFeedMedia(Long.parseLong(mediaId)); - if(p != null) { + if (p != null) { mediaPlayer.playMediaObject(p, !p.localFileAvailable(), true, true); } } @Override - public void onPlayFromSearch (String query, Bundle extras) { + public void onPlayFromSearch(String query, Bundle extras) { Log.d(TAG, "onPlayFromSearch query=" + query + " extras=" + extras.toString()); - List<SearchResult> results = FeedSearcher.performSearch(getBaseContext(),query,0); - for( SearchResult result : results) { + List<SearchResult> results = FeedSearcher.performSearch(getBaseContext(), query, 0); + for (SearchResult result : results) { try { - FeedMedia p = ((FeedItem)(result.getComponent())).getMedia(); + FeedMedia p = ((FeedItem) (result.getComponent())).getMedia(); mediaPlayer.playMediaObject(p, !p.localFileAvailable(), true, true); return; } catch (Exception e) { @@ -1714,7 +1752,6 @@ public class PlaybackService extends MediaBrowserServiceCompat { } } onPlay(); - return; } @Override @@ -1752,7 +1789,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { @Override public void onSkipToNext() { Log.d(TAG, "onSkipToNext()"); - if(UserPreferences.shouldHardwareButtonSkip()) { + if (UserPreferences.shouldHardwareButtonSkip()) { mediaPlayer.skip(); } else { seekDelta(UserPreferences.getFastForwardSecs() * 1000); @@ -1770,11 +1807,11 @@ public class PlaybackService extends MediaBrowserServiceCompat { public boolean onMediaButtonEvent(final Intent mediaButton) { Log.d(TAG, "onMediaButtonEvent(" + mediaButton + ")"); if (mediaButton != null) { - KeyEvent keyEvent = (KeyEvent) mediaButton.getExtras().get(Intent.EXTRA_KEY_EVENT); + KeyEvent keyEvent = (KeyEvent) mediaButton.getParcelableExtra(Intent.EXTRA_KEY_EVENT); if (keyEvent != null && keyEvent.getAction() == KeyEvent.ACTION_DOWN && - keyEvent.getRepeatCount() == 0){ - handleKeycode(keyEvent.getKeyCode(), keyEvent.getSource()); + keyEvent.getRepeatCount() == 0) { + return handleKeycode(keyEvent.getKeyCode(), false); } } return false; @@ -1791,29 +1828,38 @@ public class PlaybackService extends MediaBrowserServiceCompat { } }; - private SharedPreferences.OnSharedPreferenceChangeListener prefListener = + private final SharedPreferences.OnSharedPreferenceChangeListener prefListener = (sharedPreferences, key) -> { if (UserPreferences.PREF_LOCKSCREEN_BACKGROUND.equals(key)) { updateMediaSessionMetadata(getPlayable()); } else { flavorHelper.onSharedPreference(key); } - }; + }; interface FlavorHelperCallback { PlaybackServiceMediaPlayer.PSMPCallback getMediaPlayerCallback(); + void setMediaPlayer(PlaybackServiceMediaPlayer mediaPlayer); + PlaybackServiceMediaPlayer getMediaPlayer(); + void setIsCasting(boolean isCasting); + void sendNotificationBroadcast(int type, int code); + void saveCurrentPosition(boolean fromMediaPlayer, Playable playable, int position); + void setupNotification(boolean connected, PlaybackServiceMediaPlayer.PSMPInfo info); + MediaSessionCompat getMediaSession(); + Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter); + void unregisterReceiver(BroadcastReceiver receiver); } - private FlavorHelperCallback flavorHelperCallback = new FlavorHelperCallback() { + private final FlavorHelperCallback flavorHelperCallback = new FlavorHelperCallback() { @Override public PlaybackServiceMediaPlayer.PSMPCallback getMediaPlayerCallback() { return PlaybackService.this.mediaPlayerCallback; @@ -1856,7 +1902,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { UserPreferences.isPersistNotify()) && android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { PlaybackService.this.setupNotification(info); - } else if (!UserPreferences.isPersistNotify()){ + } else if (!UserPreferences.isPersistNotify()) { PlaybackService.this.stopForeground(true); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java index 393019fd1..a2481b801 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,14 +24,14 @@ import de.danoeh.antennapod.core.util.playback.Playable; * and remote (cast devices) playback. */ public abstract class PlaybackServiceMediaPlayer { - public static final String TAG = "PlaybackSvcMediaPlayer"; + private static final String TAG = "PlaybackSvcMediaPlayer"; /** * Return value of some PSMP methods if the method call failed. */ static final int INVALID_TIME = -1; - volatile PlayerStatus oldPlayerStatus; + private volatile PlayerStatus oldPlayerStatus; volatile PlayerStatus playerStatus; /** @@ -39,8 +39,8 @@ public abstract class PlaybackServiceMediaPlayer { */ private WifiManager.WifiLock wifiLock; - protected final PSMPCallback callback; - protected final Context context; + final PSMPCallback callback; + final Context context; PlaybackServiceMediaPlayer(@NonNull Context context, @NonNull PSMPCallback callback){ @@ -279,7 +279,7 @@ public abstract class PlaybackServiceMediaPlayer { final synchronized void acquireWifiLockIfNecessary() { if (shouldLockWifi()) { if (wifiLock == null) { - wifiLock = ((WifiManager) context.getSystemService(Context.WIFI_SERVICE)) + wifiLock = ((WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE)) .createWifiLock(WifiManager.WIFI_MODE_FULL, TAG); wifiLock.setReferenceCounted(false); } @@ -365,7 +365,7 @@ public abstract class PlaybackServiceMediaPlayer { * Holds information about a PSMP object. */ public static class PSMPInfo { - public PlayerStatus oldPlayerStatus; + public final PlayerStatus oldPlayerStatus; public PlayerStatus playerStatus; public Playable playable; 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 0c7d5e718..3d97e862a 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 @@ -295,7 +295,7 @@ public class PlaybackServiceTaskManager { /** * Sleeps for a given time and then pauses playback. */ - protected class SleepTimer implements Runnable { + class SleepTimer implements Runnable { private static final String TAG = "SleepTimer"; private static final long UPDATE_INTERVAL = 1000L; private static final long NOTIFICATION_THRESHOLD = 10000; diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/ShakeListener.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/ShakeListener.java index fcd96826b..c0b1b3bc0 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/ShakeListener.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/ShakeListener.java @@ -7,14 +7,14 @@ import android.hardware.SensorEventListener; import android.hardware.SensorManager; import android.util.Log; -public class ShakeListener implements SensorEventListener +class ShakeListener implements SensorEventListener { private static final String TAG = ShakeListener.class.getSimpleName(); private Sensor mAccelerometer; private SensorManager mSensorMgr; - private PlaybackServiceTaskManager.SleepTimer mSleepTimer; - private Context mContext; + private final PlaybackServiceTaskManager.SleepTimer mSleepTimer; + private final Context mContext; public ShakeListener(Context context, PlaybackServiceTaskManager.SleepTimer sleepTimer) { mContext = context; @@ -22,7 +22,7 @@ public class ShakeListener implements SensorEventListener resume(); } - public void resume() { + private void resume() { // only a precaution, the user should actually not be able to activate shake to reset // when the accelerometer is not available mSensorMgr = (SensorManager) mContext.getSystemService(Context.SENSOR_SERVICE); 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 cb268daca..456d05ded 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,6 +2,7 @@ package de.danoeh.antennapod.core.storage; import android.database.Cursor; import android.support.v4.util.ArrayMap; +import android.text.TextUtils; import android.util.Log; import java.util.ArrayList; @@ -13,13 +14,9 @@ import java.util.Map; import de.danoeh.antennapod.core.feed.Chapter; import de.danoeh.antennapod.core.feed.Feed; -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.ID3Chapter; -import de.danoeh.antennapod.core.feed.SimpleChapter; -import de.danoeh.antennapod.core.feed.VorbisCommentChapter; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.download.DownloadStatus; import de.danoeh.antennapod.core.util.LongIntMap; @@ -47,7 +44,7 @@ public final class DBReader { /** * Maximum size of the list returned by {@link #getDownloadLog()}. */ - public static final int DOWNLOAD_LOG_SIZE = 200; + private static final int DOWNLOAD_LOG_SIZE = 200; private DBReader() { @@ -123,7 +120,7 @@ public final class DBReader { loadFeedDataOfFeedItemList(items); } - public static void loadTagsOfFeedItemList(List<FeedItem> items) { + private static void loadTagsOfFeedItemList(List<FeedItem> items) { LongList favoriteIds = getFavoriteIDList(); LongList queueIds = getQueueIDList(); @@ -144,7 +141,7 @@ public final class DBReader { * * @param items The FeedItems whose Feed-objects should be loaded. */ - public static void loadFeedDataOfFeedItemList(List<FeedItem> items) { + private static void loadFeedDataOfFeedItemList(List<FeedItem> items) { List<Feed> feeds = getFeedList(); Map<Long, Feed> feedIndex = new ArrayMap<>(feeds.size()); @@ -204,25 +201,15 @@ public final class DBReader { private static List<FeedItem> extractItemlistFromCursor(PodDBAdapter adapter, Cursor cursor) { List<FeedItem> result = new ArrayList<>(cursor.getCount()); - LongList imageIds = new LongList(cursor.getCount()); LongList itemIds = new LongList(cursor.getCount()); if (cursor.moveToFirst()) { do { - int indexImage = cursor.getColumnIndex(PodDBAdapter.KEY_IMAGE); - long imageId = cursor.getLong(indexImage); - imageIds.add(imageId); - FeedItem item = FeedItem.fromCursor(cursor); result.add(item); itemIds.add(item.getId()); } while (cursor.moveToNext()); - Map<Long, FeedImage> images = getFeedImages(adapter, imageIds.toArray()); Map<Long, FeedMedia> medias = getFeedMedia(adapter, itemIds); - for (int i = 0; i < result.size(); i++) { - FeedItem item = result.get(i); - long imageId = imageIds.get(i); - FeedImage image = images.get(imageId); - item.setImage(image); + for (FeedItem item : result) { FeedMedia media = medias.get(item.getId()); item.setMedia(media); if (media != null) { @@ -257,24 +244,9 @@ public final class DBReader { } private static Feed extractFeedFromCursorRow(PodDBAdapter adapter, Cursor cursor) { - final FeedImage image; - int indexImage = cursor.getColumnIndex(PodDBAdapter.KEY_IMAGE); - long imageId = cursor.getLong(indexImage); - if (imageId != 0) { - image = getFeedImage(adapter, imageId); - } else { - image = null; - } - Feed feed = Feed.fromCursor(cursor); - if (image != null) { - feed.setImage(image); - image.setOwner(feed); - } - FeedPreferences preferences = FeedPreferences.fromCursor(cursor); feed.setPreferences(preferences); - return feed; } @@ -415,7 +387,7 @@ public final class DBReader { } } - public static LongList getFavoriteIDList() { + private static LongList getFavoriteIDList() { Log.d(TAG, "getFavoriteIDList() called"); PodDBAdapter adapter = PodDBAdapter.getInstance(); @@ -666,7 +638,7 @@ public final class DBReader { } } - static FeedItem getFeedItem(final String podcastUrl, final String episodeUrl, PodDBAdapter adapter) { + private static FeedItem getFeedItem(final String podcastUrl, final String episodeUrl, PodDBAdapter adapter) { Log.d(TAG, "Loading feeditem with podcast url " + podcastUrl + " and episode url " + episodeUrl); Cursor cursor = null; try { @@ -717,7 +689,7 @@ public final class DBReader { if (cursor.moveToFirst()) { String username = cursor.getString(0); String password = cursor.getString(1); - if (username != null && password != null) { + if (!TextUtils.isEmpty(username) && password != null) { credentials = username + ":" + password; } else { credentials = ""; @@ -800,7 +772,7 @@ public final class DBReader { } } - static void loadChaptersOfFeedItem(PodDBAdapter adapter, FeedItem item) { + private static void loadChaptersOfFeedItem(PodDBAdapter adapter, FeedItem item) { Cursor cursor = null; try { cursor = adapter.getSimpleChaptersOfFeedItemCursor(item); @@ -842,62 +814,6 @@ public final class DBReader { } /** - * Searches the DB for a FeedImage of the given id. - * - * @param imageId The id of the object - * @return The found object - */ - public static FeedImage getFeedImage(final long imageId) { - Log.d(TAG, "getFeedImage() called with: " + "imageId = [" + imageId + "]"); - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - try { - return getFeedImage(adapter, imageId); - } finally { - adapter.close(); - } - } - - /** - * Searches the DB for a FeedImage of the given id. - * - * @param imageId The id of the object - * @return The found object - */ - private static FeedImage getFeedImage(PodDBAdapter adapter, final long imageId) { - return getFeedImages(adapter, imageId).get(imageId); - } - - /** - * Searches the DB for a FeedImage of the given id. - * - * @param imageIds The ids of the images - * @return Map that associates the id of an image with the image itself - */ - private static Map<Long, FeedImage> getFeedImages(PodDBAdapter adapter, final long... imageIds) { - String[] ids = new String[imageIds.length]; - for (int i = 0, len = imageIds.length; i < len; i++) { - ids[i] = String.valueOf(imageIds[i]); - } - Cursor cursor = adapter.getImageCursor(ids); - int imageCount = cursor.getCount(); - if (imageCount == 0) { - cursor.close(); - return Collections.emptyMap(); - } - Map<Long, FeedImage> result = new ArrayMap<>(imageCount); - try { - while (cursor.moveToNext()) { - FeedImage image = FeedImage.fromCursor(cursor); - result.put(image.getId(), image); - } - } finally { - cursor.close(); - } - return result; - } - - /** * Searches the DB for a FeedMedia of the given id. * * @param mediaId The id of the object @@ -1026,14 +942,14 @@ public final class DBReader { /** * Simply sums up time of podcasts that are marked as played */ - public long totalTimeCountAll; + public final long totalTimeCountAll; /** * Respects speed, listening twice, ... */ - public long totalTime; + public final long totalTime; - public List<StatisticsItem> feedTime; + public final List<StatisticsItem> feedTime; public StatisticsData(long totalTime, long totalTimeCountAll, List<StatisticsItem> feedTime) { this.totalTime = totalTime; @@ -1043,26 +959,26 @@ public final class DBReader { } public static class StatisticsItem { - public Feed feed; - public long time; + public final Feed feed; + public final long time; /** * Respects speed, listening twice, ... */ - public long timePlayed; + public final long timePlayed; /** * Simply sums up time of podcasts that are marked as played */ - public long timePlayedCountAll; - public long episodes; + public final long timePlayedCountAll; + public final long episodes; /** * Episodes that are actually played */ - public long episodesStarted; + public final long episodesStarted; /** * All episodes that are marked as played (or have position != 0) */ - public long episodesStartedIncludingMarked; + public final long episodesStartedIncludingMarked; public StatisticsItem(Feed feed, long time, long timePlayed, long timePlayedCountAll, long episodes, long episodesStarted, long episodesStartedIncludingMarked) { @@ -1198,12 +1114,12 @@ public final class DBReader { } public static class NavDrawerData { - public List<Feed> feeds; - public int queueSize; - public int numNewItems; - public int numDownloadedItems; - public LongIntMap feedCounters; - public int reclaimableSpace; + public final List<Feed> feeds; + public final int queueSize; + public final int numNewItems; + public final int numDownloadedItems; + public final LongIntMap feedCounters; + public final int reclaimableSpace; public NavDrawerData(List<Feed> feeds, int queueSize, 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 148b530a7..8eed10cd7 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 @@ -4,6 +4,8 @@ import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.database.Cursor; +import android.support.annotation.Nullable; +import android.support.v4.content.ContextCompat; import android.util.Log; import java.util.ArrayList; @@ -34,13 +36,14 @@ import de.danoeh.antennapod.core.service.download.DownloadStatus; import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.core.util.Converter; import de.danoeh.antennapod.core.util.DownloadError; +import de.danoeh.antennapod.core.util.IntentUtils; import de.danoeh.antennapod.core.util.LongList; import de.danoeh.antennapod.core.util.comparator.FeedItemPubdateComparator; import de.danoeh.antennapod.core.util.exception.MediaFileNotFoundException; import de.danoeh.antennapod.core.util.flattr.FlattrUtils; +import de.danoeh.antennapod.core.util.playback.PlaybackServiceStarter; import static android.content.Context.MODE_PRIVATE; -import static android.provider.Contacts.SettingsColumns.KEY; /** * Provides methods for doing common tasks that use DBReader and DBWriter. @@ -48,13 +51,13 @@ import static android.provider.Contacts.SettingsColumns.KEY; public final class DBTasks { private static final String TAG = "DBTasks"; - public static final String PREF_NAME = "dbtasks"; + private static final String PREF_NAME = "dbtasks"; private static final String PREF_LAST_REFRESH = "last_refresh"; /** * Executor service used by the autodownloadUndownloadedEpisodes method. */ - private static ExecutorService autodownloadExec; + private static final ExecutorService autodownloadExec; static { autodownloadExec = Executors.newSingleThreadExecutor(r -> { @@ -124,16 +127,13 @@ public final class DBTasks { media); } } - // Start playback Service - Intent launchIntent = new Intent(context, PlaybackService.class); - launchIntent.putExtra(PlaybackService.EXTRA_PLAYABLE, media); - launchIntent.putExtra(PlaybackService.EXTRA_START_WHEN_PREPARED, - startWhenPrepared); - launchIntent.putExtra(PlaybackService.EXTRA_SHOULD_STREAM, - shouldStream); - launchIntent.putExtra(PlaybackService.EXTRA_PREPARE_IMMEDIATELY, - true); - context.startService(launchIntent); + + new PlaybackServiceStarter(context, media) + .callEvenIfRunning(true) + .startWhenPrepared(startWhenPrepared) + .shouldStream(shouldStream) + .start(); + if (showPlayer) { // Launch media player context.startActivity(PlaybackService.getPlayerActivityIntent( @@ -143,55 +143,70 @@ public final class DBTasks { } catch (MediaFileNotFoundException e) { e.printStackTrace(); if (media.isPlaying()) { - context.sendBroadcast(new Intent( - PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE)); + IntentUtils.sendLocalBroadcast(context, PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE); } notifyMissingFeedMediaFile(context, media); } } - private static AtomicBoolean isRefreshing = new AtomicBoolean(false); + private static final AtomicBoolean isRefreshing = new AtomicBoolean(false); /** * Refreshes a given list of Feeds in a separate Thread. This method might ignore subsequent calls if it is still * enqueuing Feeds for download from a previous call * - * @param context Might be used for accessing the database - * @param feeds List of Feeds that should be refreshed. + * @param context Might be used for accessing the database + * @param feeds List of Feeds that should be refreshed. */ - public static void refreshAllFeeds(final Context context, - final List<Feed> feeds) { - if (isRefreshing.compareAndSet(false, true)) { - new Thread() { - public void run() { - if (feeds != null) { - refreshFeeds(context, feeds); - } else { - refreshFeeds(context, DBReader.getFeedList()); - } - isRefreshing.set(false); + public static void refreshAllFeeds(final Context context, final List<Feed> feeds) { + refreshAllFeeds(context, feeds, null); + } + + /** + * Refreshes a given list of Feeds in a separate Thread. This method might ignore subsequent calls if it is still + * enqueuing Feeds for download from a previous call + * + * @param context Might be used for accessing the database + * @param feeds List of Feeds that should be refreshed. + * @param callback Called after everything was added enqueued for download. Might be null. + */ + public static void refreshAllFeeds(final Context context, final List<Feed> feeds, @Nullable Runnable callback) { + if (!isRefreshing.compareAndSet(false, true)) { + Log.d(TAG, "Ignoring request to refresh all feeds: Refresh lock is locked"); + return; + } - SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, MODE_PRIVATE); - prefs.edit().putLong(PREF_LAST_REFRESH, System.currentTimeMillis()).apply(); + new Thread(() -> { + if (feeds != null) { + refreshFeeds(context, feeds); + } else { + refreshFeeds(context, DBReader.getFeedList()); + } + isRefreshing.set(false); - if (FlattrUtils.hasToken()) { - Log.d(TAG, "Flattring all pending things."); - new FlattrClickWorker(context).executeAsync(); // flattr pending things + SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, MODE_PRIVATE); + prefs.edit().putLong(PREF_LAST_REFRESH, System.currentTimeMillis()).apply(); - Log.d(TAG, "Fetching flattr status."); - new FlattrStatusFetcher(context).start(); + if (FlattrUtils.hasToken()) { + Log.d(TAG, "Flattring all pending things."); + new FlattrClickWorker(context).executeAsync(); // flattr pending things - } - if (ClientConfig.gpodnetCallbacks.gpodnetEnabled()) { - GpodnetSyncService.sendSyncIntent(context); - } - Log.d(TAG, "refreshAllFeeds autodownload"); - autodownloadUndownloadedItems(context); - } - }.start(); - } else { - Log.d(TAG, "Ignoring request to refresh all feeds: Refresh lock is locked"); - } + Log.d(TAG, "Fetching flattr status."); + new FlattrStatusFetcher(context).start(); + + } + if (ClientConfig.gpodnetCallbacks.gpodnetEnabled()) { + GpodnetSyncService.sendSyncIntent(context); + } + // Note: automatic download of episodes will be done but not here. + // Instead it is done after all feeds have been refreshed (asynchronously), + // in DownloadService.onDestroy() + // See Issue #2577 for the details of the rationale + + if (callback != null) { + callback.run(); + } + }).start(); } /** @@ -224,27 +239,6 @@ public final class DBTasks { } /** - * Downloads all pages of the given feed. - * - * @param context Used for requesting the download. - * @param feed The Feed object. - */ - public static void refreshCompleteFeed(final Context context, final Feed feed) { - try { - refreshFeed(context, feed, true, false); - } catch (DownloadRequestException e) { - e.printStackTrace(); - DBWriter.addDownloadStatus( - new DownloadStatus(feed, feed - .getHumanReadableIdentifier(), - DownloadError.ERROR_REQUEST_ERROR, false, e - .getMessage() - ) - ); - } - } - - /** * Downloads all pages of the given feed even if feed has not been modified since last refresh * * @param context Used for requesting the download. @@ -293,7 +287,7 @@ public final class DBTasks { * @param context Used for requesting the download. * @param feed The Feed object. */ - public static void refreshFeed(Context context, Feed feed) + private static void refreshFeed(Context context, Feed feed) throws DownloadRequestException { Log.d(TAG, "refreshFeed(feed.id: " + feed.getId() +")"); refreshFeed(context, feed, false, false); @@ -365,27 +359,6 @@ public final class DBTasks { } /** - * Request the download of all objects in the queue. from a separate Thread. - * - * @param context Used for requesting the download an accessing the database. - */ - public static void downloadAllItemsInQueue(final Context context) { - new Thread() { - public void run() { - List<FeedItem> queue = DBReader.getQueue(); - if (!queue.isEmpty()) { - try { - downloadFeedItems(context, - queue.toArray(new FeedItem[queue.size()])); - } catch (DownloadRequestException e) { - e.printStackTrace(); - } - } - } - }.start(); - } - - /** * Requests the download of a list of FeedItem objects. * * @param context Used for requesting the download and accessing the DB. @@ -804,7 +777,7 @@ public final class DBTasks { */ abstract static class QueryTask<T> implements Callable<T> { private T result; - private Context context; + private final Context context; public QueryTask(Context context) { this.context = context; @@ -821,7 +794,7 @@ public final class DBTasks { public abstract void execute(PodDBAdapter adapter); - protected void setResult(T result) { + void setResult(T result) { this.result = result; } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBUpgrader.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBUpgrader.java new file mode 100644 index 000000000..29ed5f7f9 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBUpgrader.java @@ -0,0 +1,292 @@ +package de.danoeh.antennapod.core.storage; + +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.media.MediaMetadataRetriever; +import android.util.Log; +import de.danoeh.antennapod.core.feed.FeedItem; + +class DBUpgrader { + /** + * Upgrades the given database to a new schema version + */ + static void upgrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) { + if (oldVersion <= 1) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + " ADD COLUMN " + + PodDBAdapter.KEY_TYPE + " TEXT"); + } + if (oldVersion <= 2) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_SIMPLECHAPTERS + + " ADD COLUMN " + PodDBAdapter.KEY_LINK + " TEXT"); + } + if (oldVersion <= 3) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS + + " ADD COLUMN " + PodDBAdapter.KEY_ITEM_IDENTIFIER + " TEXT"); + } + if (oldVersion <= 4) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + " ADD COLUMN " + + PodDBAdapter.KEY_FEED_IDENTIFIER + " TEXT"); + } + if (oldVersion <= 5) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_DOWNLOAD_LOG + + " ADD COLUMN " + PodDBAdapter.KEY_REASON_DETAILED + " TEXT"); + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_DOWNLOAD_LOG + + " ADD COLUMN " + PodDBAdapter.KEY_DOWNLOADSTATUS_TITLE + " TEXT"); + } + if (oldVersion <= 6) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_SIMPLECHAPTERS + + " ADD COLUMN " + PodDBAdapter.KEY_CHAPTER_TYPE + " INTEGER"); + } + if (oldVersion <= 7) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_MEDIA + + " ADD COLUMN " + PodDBAdapter.KEY_PLAYBACK_COMPLETION_DATE + + " INTEGER"); + } + if (oldVersion <= 8) { + final int KEY_ID_POSITION = 0; + final int KEY_MEDIA_POSITION = 1; + + // Add feeditem column to feedmedia table + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_MEDIA + + " ADD COLUMN " + PodDBAdapter.KEY_FEEDITEM + + " INTEGER"); + Cursor feeditemCursor = db.query(PodDBAdapter.TABLE_NAME_FEED_ITEMS, + new String[]{PodDBAdapter.KEY_ID, PodDBAdapter.KEY_MEDIA}, "? > 0", + new String[]{PodDBAdapter.KEY_MEDIA}, null, null, null); + if (feeditemCursor.moveToFirst()) { + db.beginTransaction(); + ContentValues contentValues = new ContentValues(); + do { + long mediaId = feeditemCursor.getLong(KEY_MEDIA_POSITION); + contentValues.put(PodDBAdapter.KEY_FEEDITEM, feeditemCursor.getLong(KEY_ID_POSITION)); + db.update(PodDBAdapter.TABLE_NAME_FEED_MEDIA, contentValues, PodDBAdapter.KEY_ID + "=?", new String[]{String.valueOf(mediaId)}); + contentValues.clear(); + } while (feeditemCursor.moveToNext()); + db.setTransactionSuccessful(); + db.endTransaction(); + } + feeditemCursor.close(); + } + if (oldVersion <= 9) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + + " ADD COLUMN " + PodDBAdapter.KEY_AUTO_DOWNLOAD + + " INTEGER DEFAULT 1"); + } + if (oldVersion <= 10) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + + " ADD COLUMN " + PodDBAdapter.KEY_FLATTR_STATUS + + " INTEGER"); + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS + + " ADD COLUMN " + PodDBAdapter.KEY_FLATTR_STATUS + + " INTEGER"); + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_MEDIA + + " ADD COLUMN " + PodDBAdapter.KEY_PLAYED_DURATION + + " INTEGER"); + } + if (oldVersion <= 11) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + + " ADD COLUMN " + PodDBAdapter.KEY_USERNAME + + " TEXT"); + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + + " ADD COLUMN " + PodDBAdapter.KEY_PASSWORD + + " TEXT"); + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS + + " ADD COLUMN " + PodDBAdapter.KEY_IMAGE + + " INTEGER"); + } + if (oldVersion <= 12) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + + " ADD COLUMN " + PodDBAdapter.KEY_IS_PAGED + " INTEGER DEFAULT 0"); + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + + " ADD COLUMN " + PodDBAdapter.KEY_NEXT_PAGE_LINK + " TEXT"); + } + if (oldVersion <= 13) { + // remove duplicate rows in "Chapters" table that were created because of a bug. + db.execSQL(String.format("DELETE FROM %s WHERE %s NOT IN " + + "(SELECT MIN(%s) as %s FROM %s GROUP BY %s,%s,%s,%s,%s)", + PodDBAdapter.TABLE_NAME_SIMPLECHAPTERS, + PodDBAdapter.KEY_ID, + PodDBAdapter.KEY_ID, + PodDBAdapter.KEY_ID, + PodDBAdapter.TABLE_NAME_SIMPLECHAPTERS, + PodDBAdapter.KEY_TITLE, + PodDBAdapter.KEY_START, + PodDBAdapter.KEY_FEEDITEM, + PodDBAdapter.KEY_LINK, + PodDBAdapter.KEY_CHAPTER_TYPE)); + } + if (oldVersion <= 14) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS + + " ADD COLUMN " + PodDBAdapter.KEY_AUTO_DOWNLOAD + " INTEGER"); + db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS + + " SET " + PodDBAdapter.KEY_AUTO_DOWNLOAD + " = " + + "(SELECT " + PodDBAdapter.KEY_AUTO_DOWNLOAD + + " FROM " + PodDBAdapter.TABLE_NAME_FEEDS + + " WHERE " + PodDBAdapter.TABLE_NAME_FEEDS + "." + PodDBAdapter.KEY_ID + + " = " + PodDBAdapter.TABLE_NAME_FEED_ITEMS + "." + PodDBAdapter.KEY_FEED + ")"); + + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + + " ADD COLUMN " + PodDBAdapter.KEY_HIDE + " TEXT"); + + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + + " ADD COLUMN " + PodDBAdapter.KEY_LAST_UPDATE_FAILED + " INTEGER DEFAULT 0"); + + // create indexes + db.execSQL(PodDBAdapter.CREATE_INDEX_FEEDITEMS_FEED); + db.execSQL(PodDBAdapter.CREATE_INDEX_FEEDMEDIA_FEEDITEM); + db.execSQL(PodDBAdapter.CREATE_INDEX_QUEUE_FEEDITEM); + db.execSQL(PodDBAdapter.CREATE_INDEX_SIMPLECHAPTERS_FEEDITEM); + } + if (oldVersion <= 15) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_MEDIA + + " ADD COLUMN " + PodDBAdapter.KEY_HAS_EMBEDDED_PICTURE + " INTEGER DEFAULT -1"); + db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEED_MEDIA + + " SET " + PodDBAdapter.KEY_HAS_EMBEDDED_PICTURE + "=0" + + " WHERE " + PodDBAdapter.KEY_DOWNLOADED + "=0"); + Cursor c = db.rawQuery("SELECT " + PodDBAdapter.KEY_FILE_URL + + " FROM " + PodDBAdapter.TABLE_NAME_FEED_MEDIA + + " WHERE " + PodDBAdapter.KEY_DOWNLOADED + "=1 " + + " AND " + PodDBAdapter.KEY_HAS_EMBEDDED_PICTURE + "=-1", null); + if (c.moveToFirst()) { + MediaMetadataRetriever mmr = new MediaMetadataRetriever(); + do { + String fileUrl = c.getString(0); + try { + mmr.setDataSource(fileUrl); + byte[] image = mmr.getEmbeddedPicture(); + if (image != null) { + db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEED_MEDIA + + " SET " + PodDBAdapter.KEY_HAS_EMBEDDED_PICTURE + "=1" + + " WHERE " + PodDBAdapter.KEY_FILE_URL + "='" + fileUrl + "'"); + } else { + db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEED_MEDIA + + " SET " + PodDBAdapter.KEY_HAS_EMBEDDED_PICTURE + "=0" + + " WHERE " + PodDBAdapter.KEY_FILE_URL + "='" + fileUrl + "'"); + } + } catch (Exception e) { + e.printStackTrace(); + } + } while (c.moveToNext()); + } + c.close(); + } + if (oldVersion <= 16) { + String selectNew = "SELECT " + PodDBAdapter.TABLE_NAME_FEED_ITEMS + "." + PodDBAdapter.KEY_ID + + " FROM " + PodDBAdapter.TABLE_NAME_FEED_ITEMS + + " INNER JOIN " + PodDBAdapter.TABLE_NAME_FEED_MEDIA + " ON " + + PodDBAdapter.TABLE_NAME_FEED_ITEMS + "." + PodDBAdapter.KEY_ID + "=" + + PodDBAdapter.TABLE_NAME_FEED_MEDIA + "." + PodDBAdapter.KEY_FEEDITEM + + " LEFT OUTER JOIN " + PodDBAdapter.TABLE_NAME_QUEUE + " ON " + + PodDBAdapter.TABLE_NAME_FEED_ITEMS + "." + PodDBAdapter.KEY_ID + "=" + + PodDBAdapter.TABLE_NAME_QUEUE + "." + PodDBAdapter.KEY_FEEDITEM + + " WHERE " + + PodDBAdapter.TABLE_NAME_FEED_ITEMS + "." + PodDBAdapter.KEY_READ + " = 0 AND " // unplayed + + PodDBAdapter.TABLE_NAME_FEED_MEDIA + "." + PodDBAdapter.KEY_DOWNLOADED + " = 0 AND " // undownloaded + + PodDBAdapter.TABLE_NAME_FEED_MEDIA + "." + PodDBAdapter.KEY_POSITION + " = 0 AND " // not partially played + + PodDBAdapter.TABLE_NAME_QUEUE + "." + PodDBAdapter.KEY_ID + " IS NULL"; // not in queue + String sql = "UPDATE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS + + " SET " + PodDBAdapter.KEY_READ + "=" + FeedItem.NEW + + " WHERE " + PodDBAdapter.KEY_ID + " IN (" + selectNew + ")"; + Log.d("Migration", "SQL: " + sql); + db.execSQL(sql); + } + if (oldVersion <= 17) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + + " ADD COLUMN " + PodDBAdapter.KEY_AUTO_DELETE_ACTION + " INTEGER DEFAULT 0"); + } + if (oldVersion < 1030005) { + db.execSQL("UPDATE FeedItems SET auto_download=0 WHERE " + + "(read=1 OR id IN (SELECT feeditem FROM FeedMedia WHERE position>0 OR downloaded=1)) " + + "AND id NOT IN (SELECT feeditem FROM Queue)"); + } + if (oldVersion < 1040001) { + db.execSQL(PodDBAdapter.CREATE_TABLE_FAVORITES); + } + if (oldVersion < 1040002) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_MEDIA + + " ADD COLUMN " + PodDBAdapter.KEY_LAST_PLAYED_TIME + " INTEGER DEFAULT 0"); + } + if (oldVersion < 1040013) { + db.execSQL(PodDBAdapter.CREATE_INDEX_FEEDITEMS_PUBDATE); + db.execSQL(PodDBAdapter.CREATE_INDEX_FEEDITEMS_READ); + } + if (oldVersion < 1050003) { + // Migrates feed list filter data + + db.beginTransaction(); + + // Change to intermediate values to avoid overwriting in the following find/replace + db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEEDS + "\n" + + "SET " + PodDBAdapter.KEY_HIDE + " = replace(" + PodDBAdapter.KEY_HIDE + ", 'unplayed', 'noplay')"); + db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEEDS + "\n" + + "SET " + PodDBAdapter.KEY_HIDE + " = replace(" + PodDBAdapter.KEY_HIDE + ", 'not_queued', 'noqueue')"); + db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEEDS + "\n" + + "SET " + PodDBAdapter.KEY_HIDE + " = replace(" + PodDBAdapter.KEY_HIDE + ", 'not_downloaded', 'nodl')"); + + // Replace played, queued, and downloaded with their opposites + db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEEDS + "\n" + + "SET " + PodDBAdapter.KEY_HIDE + " = replace(" + PodDBAdapter.KEY_HIDE + ", 'played', 'unplayed')"); + db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEEDS + "\n" + + "SET " + PodDBAdapter.KEY_HIDE + " = replace(" + PodDBAdapter.KEY_HIDE + ", 'queued', 'not_queued')"); + db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEEDS + "\n" + + "SET " + PodDBAdapter.KEY_HIDE + " = replace(" + PodDBAdapter.KEY_HIDE + ", 'downloaded', 'not_downloaded')"); + + // Now replace intermediates for unplayed, not queued, etc. with their opposites + db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEEDS + "\n" + + "SET " + PodDBAdapter.KEY_HIDE + " = replace(" + PodDBAdapter.KEY_HIDE + ", 'noplay', 'played')"); + db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEEDS + "\n" + + "SET " + PodDBAdapter.KEY_HIDE + " = replace(" + PodDBAdapter.KEY_HIDE + ", 'noqueue', 'queued')"); + db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEEDS + "\n" + + "SET " + PodDBAdapter.KEY_HIDE + " = replace(" + PodDBAdapter.KEY_HIDE + ", 'nodl', 'downloaded')"); + + // Paused doesn't have an opposite, so unplayed is the next best option + db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEEDS + "\n" + + "SET " + PodDBAdapter.KEY_HIDE + " = replace(" + PodDBAdapter.KEY_HIDE + ", 'paused', 'unplayed')"); + + db.setTransactionSuccessful(); + db.endTransaction(); + + // and now get ready for autodownload filters + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + + " ADD COLUMN " + PodDBAdapter.KEY_INCLUDE_FILTER + " TEXT DEFAULT ''"); + + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + + " ADD COLUMN " + PodDBAdapter.KEY_EXCLUDE_FILTER + " TEXT DEFAULT ''"); + + // and now auto refresh + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + + " ADD COLUMN " + PodDBAdapter.KEY_KEEP_UPDATED + " INTEGER DEFAULT 1"); + } + if (oldVersion < 1050004) { + // prevent old timestamps to be misinterpreted as ETags + db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEEDS + + " SET " + PodDBAdapter.KEY_LASTUPDATE + "=NULL"); + } + if (oldVersion < 1060200) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + + " ADD COLUMN " + PodDBAdapter.KEY_CUSTOM_TITLE + " TEXT"); + } + if (oldVersion < 1060596) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + + " ADD COLUMN " + PodDBAdapter.KEY_IMAGE_URL + " TEXT"); + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS + + " ADD COLUMN " + PodDBAdapter.KEY_IMAGE_URL + " TEXT"); + + db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS + " SET " + PodDBAdapter.KEY_IMAGE_URL + " = (" + + " SELECT " + PodDBAdapter.KEY_DOWNLOAD_URL + + " FROM " + PodDBAdapter.TABLE_NAME_FEED_IMAGES + + " WHERE " + PodDBAdapter.TABLE_NAME_FEED_IMAGES + "." + PodDBAdapter.KEY_ID + + " = " + PodDBAdapter.TABLE_NAME_FEED_ITEMS + "." + PodDBAdapter.KEY_IMAGE + ")"); + + db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEEDS + " SET " + PodDBAdapter.KEY_IMAGE_URL + " = (" + + " SELECT " + PodDBAdapter.KEY_DOWNLOAD_URL + + " FROM " + PodDBAdapter.TABLE_NAME_FEED_IMAGES + + " WHERE " + PodDBAdapter.TABLE_NAME_FEED_IMAGES + "." + PodDBAdapter.KEY_ID + + " = " + PodDBAdapter.TABLE_NAME_FEEDS + "." + PodDBAdapter.KEY_IMAGE + ")"); + + db.execSQL("DROP TABLE " + PodDBAdapter.TABLE_NAME_FEED_IMAGES); + } + } + +} 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 49ec07004..8bb5bc31a 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,8 +7,7 @@ import android.content.SharedPreferences; import android.preference.PreferenceManager; import android.util.Log; -import de.danoeh.antennapod.core.R; -import de.danoeh.antennapod.core.event.MessageEvent; +import de.danoeh.antennapod.core.util.IntentUtils; import org.shredzone.flattr4j.model.Flattr; import java.io.File; @@ -25,14 +24,15 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; import de.danoeh.antennapod.core.ClientConfig; +import de.danoeh.antennapod.core.R; import de.danoeh.antennapod.core.asynctask.FlattrClickWorker; import de.danoeh.antennapod.core.event.FavoritesEvent; import de.danoeh.antennapod.core.event.FeedItemEvent; +import de.danoeh.antennapod.core.event.MessageEvent; import de.danoeh.antennapod.core.event.QueueEvent; 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; @@ -43,6 +43,7 @@ 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.LongList; +import de.danoeh.antennapod.core.util.Permutor; import de.danoeh.antennapod.core.util.flattr.FlattrStatus; import de.danoeh.antennapod.core.util.flattr.FlattrThing; import de.danoeh.antennapod.core.util.flattr.SimpleFlattrThing; @@ -115,11 +116,8 @@ public class DBWriter { true); editor.commit(); } - if (PlaybackPreferences - .getCurrentlyPlayingFeedMediaId() == media - .getId()) { - context.sendBroadcast(new Intent( - PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE)); + if (PlaybackPreferences.getCurrentlyPlayingFeedMediaId() == media.getId()) { + IntentUtils.sendLocalBroadcast(context, PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE); } } // Gpodder: queue delete action for synchronization @@ -156,8 +154,7 @@ public class DBWriter { if (PlaybackPreferences.getCurrentlyPlayingMedia() == FeedMedia.PLAYABLE_TYPE_FEEDMEDIA && PlaybackPreferences.getLastPlayedFeedId() == feed .getId()) { - context.sendBroadcast(new Intent( - PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE)); + IntentUtils.sendLocalBroadcast(context, PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE); SharedPreferences.Editor editor = prefs.edit(); editor.putLong( PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID, @@ -165,17 +162,6 @@ public class DBWriter { editor.commit(); } - // delete image file - if (feed.getImage() != null) { - if (feed.getImage().isDownloaded() - && feed.getImage().getFile_url() != null) { - File imageFile = new File(feed.getImage() - .getFile_url()); - imageFile.delete(); - } else if (requester.isDownloadingFile(feed.getImage())) { - requester.cancelDownload(context, feed.getImage()); - } - } // delete stored media files and mark them as read List<FeedItem> queue = DBReader.getQueue(); List<FeedItem> removed = new ArrayList<>(); @@ -187,6 +173,9 @@ public class DBWriter { if(queue.remove(item)) { removed.add(item); } + if (item.getState() == FeedItem.State.PLAYING && PlaybackService.isRunning) { + context.stopService(new Intent(context, PlaybackService.class)); + } if (item.getMedia() != null && item.getMedia().isDownloaded()) { File mediaFile = new File(item.getMedia() @@ -196,16 +185,6 @@ public class DBWriter { && requester.isDownloadingFile(item.getMedia())) { requester.cancelDownload(context, item.getMedia()); } - - if (item.hasItemImage()) { - FeedImage image = item.getImage(); - if (image.isDownloaded() && image.getFile_url() != null) { - File imgFile = new File(image.getFile_url()); - imgFile.delete(); - } else if (requester.isDownloadingFile(image)) { - requester.cancelDownload(context, item.getImage()); - } - } } PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); @@ -382,8 +361,8 @@ public class DBWriter { // add item to either front ot back of queue boolean addToFront = UserPreferences.enqueueAtFront(); if (addToFront) { - queue.add(0 + i, item); - events.add(QueueEvent.added(item, 0 + i)); + queue.add(i, item); + events.add(QueueEvent.added(item, i)); } else { queue.add(item); events.add(QueueEvent.added(item, queue.size() - 1)); @@ -478,22 +457,6 @@ public class DBWriter { }); } - public static Future<?> addFavoriteItemById(final long itemId) { - return dbExec.submit(() -> { - final FeedItem item = DBReader.getFeedItem(itemId); - if (item == null) { - Log.d(TAG, "Can't find item for itemId " + itemId); - return; - } - final PodDBAdapter adapter = PodDBAdapter.getInstance().open(); - adapter.addFavoriteItem(item); - adapter.close(); - item.addTag(FeedItem.TAG_FAVORITE); - EventBus.getDefault().post(FavoritesEvent.added(item)); - EventBus.getDefault().post(FeedItemEvent.updated(item)); - }); - } - public static Future<?> removeFavoriteItem(final FeedItem item) { return dbExec.submit(() -> { final PodDBAdapter adapter = PodDBAdapter.getInstance().open(); @@ -782,21 +745,6 @@ public class DBWriter { } /** - * Saves a FeedImage object in the database. This method will save all attributes of the FeedImage object. The - * contents of FeedComponent-attributes (e.g. the FeedImages's 'feed'-attribute) will not be saved. - * - * @param image The FeedImage object. - */ - public static Future<?> setFeedImage(final FeedImage image) { - return dbExec.submit(() -> { - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.setImage(image); - adapter.close(); - }); - } - - /** * Updates download URL of a feed */ public static Future<?> updateFeedDownloadURL(final String original, final String updated) { @@ -838,9 +786,9 @@ public class DBWriter { * * @param startFlattrClickWorker true if FlattrClickWorker should be started after the FlattrStatus has been saved */ - public static Future<?> setFeedItemFlattrStatus(final Context context, - final FeedItem item, - final boolean startFlattrClickWorker) { + private static Future<?> setFeedItemFlattrStatus(final Context context, + final FeedItem item, + final boolean startFlattrClickWorker) { return dbExec.submit(() -> { PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); @@ -992,6 +940,32 @@ public class DBWriter { } /** + * Similar to sortQueue, but allows more complex reordering by providing whole-queue context. + * @param permutor Encapsulates whole-Queue reordering logic. + * @param broadcastUpdate <code>true</code> if this operation should trigger a + * QueueUpdateBroadcast. This option should be set to <code>false</code> + * if the caller wants to avoid unexpected updates of the GUI. + */ + public static Future<?> reorderQueue(final Permutor<FeedItem> permutor, final boolean broadcastUpdate) { + return dbExec.submit(() -> { + final PodDBAdapter adapter = PodDBAdapter.getInstance(); + adapter.open(); + final List<FeedItem> queue = DBReader.getQueue(adapter); + + if (queue != null) { + permutor.reorder(queue); + adapter.setQueue(queue); + if (broadcastUpdate) { + EventBus.getDefault().post(QueueEvent.sorted(queue)); + } + } else { + Log.e(TAG, "reorderQueue: Could not load queue"); + } + adapter.close(); + }); + } + + /** * Sets the 'auto_download'-attribute of specific FeedItem. * * @param feedItem FeedItem. diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java index 7051d7f4d..827874f54 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java @@ -4,10 +4,13 @@ import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.support.annotation.NonNull; +import android.support.v4.content.ContextCompat; import android.text.TextUtils; import android.util.Log; import android.webkit.URLUtil; +import de.danoeh.antennapod.core.service.playback.PlaybackService; +import de.danoeh.antennapod.core.util.IntentUtils; import org.apache.commons.io.FilenameUtils; import java.io.File; @@ -33,8 +36,8 @@ public class DownloadRequester { private static final String TAG = "DownloadRequester"; public static final String IMAGE_DOWNLOADPATH = "images/"; - public static final String FEED_DOWNLOADPATH = "cache/"; - public static final String MEDIA_DOWNLOADPATH = "media/"; + private static final String FEED_DOWNLOADPATH = "cache/"; + private static final String MEDIA_DOWNLOADPATH = "media/"; /** * Denotes the page of the feed that is contained in the DownloadRequest sent by the DownloadRequester. @@ -48,7 +51,7 @@ public class DownloadRequester { private static DownloadRequester downloader; - private Map<String, DownloadRequest> downloads; + private final Map<String, DownloadRequest> downloads; private DownloadRequester() { downloads = new ConcurrentHashMap<>(); @@ -81,7 +84,7 @@ public class DownloadRequester { Intent launchIntent = new Intent(context, DownloadService.class); launchIntent.putExtra(DownloadService.EXTRA_REQUEST, request); - context.startService(launchIntent); + ContextCompat.startForegroundService(context, launchIntent); return true; } @@ -89,7 +92,9 @@ public class DownloadRequester { private void download(Context context, FeedFile item, FeedFile container, File dest, boolean overwriteIfExists, String username, String password, String lastModified, boolean deleteOnFailure, Bundle arguments) { - final boolean partiallyDownloadedFileExists = item.getFile_url() != null; + final boolean partiallyDownloadedFileExists = item.getFile_url() != null && new File(item.getFile_url()).exists(); + + Log.d(TAG, "partiallyDownloadedFileExists: " + partiallyDownloadedFileExists); if (isDownloadingFile(item)) { Log.e(TAG, "URL " + item.getDownload_url() + " is already being downloaded"); @@ -174,8 +179,8 @@ public class DownloadRequester { args.putInt(REQUEST_ARG_PAGE_NR, feed.getPageNr()); args.putBoolean(REQUEST_ARG_LOAD_ALL_PAGES, loadAllPages); - download(context, feed, null, new File(getFeedfilePath(context), - getFeedfileName(feed)), true, username, password, lastModified, true, args); + download(context, feed, null, new File(getFeedfilePath(), getFeedfileName(feed)), + true, username, password, lastModified, true, args); } } @@ -201,8 +206,7 @@ public class DownloadRequester { if (feedmedia.getFile_url() != null) { dest = new File(feedmedia.getFile_url()); } else { - dest = new File(getMediafilePath(context, feedmedia), - getMediafilename(feedmedia)); + dest = new File(getMediafilePath(feedmedia), getMediafilename(feedmedia)); } download(context, feedmedia, feed, dest, false, username, password, null, false, null); @@ -240,6 +244,7 @@ public class DownloadRequester { Log.d(TAG, "Cancelling download with url " + downloadUrl); Intent cancelIntent = new Intent(DownloadService.ACTION_CANCEL_DOWNLOAD); cancelIntent.putExtra(DownloadService.EXTRA_DOWNLOAD_URL, downloadUrl); + cancelIntent.setPackage(context.getPackageName()); context.sendBroadcast(cancelIntent); } @@ -248,8 +253,7 @@ public class DownloadRequester { */ public synchronized void cancelAllDownloads(Context context) { Log.d(TAG, "Cancelling all running downloads"); - context.sendBroadcast(new Intent( - DownloadService.ACTION_CANCEL_ALL_DOWNLOADS)); + IntentUtils.sendLocalBroadcast(context, DownloadService.ACTION_CANCEL_ALL_DOWNLOADS); } /** @@ -303,13 +307,11 @@ public class DownloadRequester { return downloads.size(); } - public synchronized String getFeedfilePath(Context context) - throws DownloadRequestException { - return getExternalFilesDirOrThrowException(context, FEED_DOWNLOADPATH) - .toString() + "/"; + private synchronized String getFeedfilePath() throws DownloadRequestException { + return getExternalFilesDirOrThrowException(FEED_DOWNLOADPATH).toString() + "/"; } - public synchronized String getFeedfileName(Feed feed) { + private synchronized String getFeedfileName(Feed feed) { String filename = feed.getDownload_url(); if (feed.getTitle() != null && !feed.getTitle().isEmpty()) { filename = feed.getTitle(); @@ -317,10 +319,8 @@ public class DownloadRequester { return "feed-" + FileNameGenerator.generateFileName(filename); } - public synchronized String getMediafilePath(Context context, FeedMedia media) - throws DownloadRequestException { + private synchronized String getMediafilePath(FeedMedia media) throws DownloadRequestException { File externalStorage = getExternalFilesDirOrThrowException( - context, MEDIA_DOWNLOADPATH + FileNameGenerator.generateFileName(media.getItem() .getFeed().getTitle()) + "/" @@ -328,8 +328,7 @@ public class DownloadRequester { return externalStorage.toString(); } - private File getExternalFilesDirOrThrowException(Context context, - String type) throws DownloadRequestException { + private File getExternalFilesDirOrThrowException(String type) throws DownloadRequestException { File result = UserPreferences.getDataFolder(type); if (result == null) { throw new DownloadRequestException( diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/EpisodeCleanupAlgorithm.java b/core/src/main/java/de/danoeh/antennapod/core/storage/EpisodeCleanupAlgorithm.java index 97cbdca33..aae5b352e 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/EpisodeCleanupAlgorithm.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/EpisodeCleanupAlgorithm.java @@ -15,7 +15,7 @@ public abstract class EpisodeCleanupAlgorithm { * or getPerformCleanupParameter. * @return The number of episodes that were deleted. */ - public abstract int performCleanup(Context context, int numToRemove); + protected abstract int performCleanup(Context context, int numToRemove); public int performCleanup(Context context) { return performCleanup(context, getDefaultCleanupParameter()); @@ -26,7 +26,7 @@ public abstract class EpisodeCleanupAlgorithm { * space to free to satisfy the episode cache conditions. If the conditions are already satisfied, this * method should not have any effects. */ - public abstract int getDefaultCleanupParameter(); + protected abstract int getDefaultCleanupParameter(); /** * Cleans up just enough episodes to make room for the requested number @@ -48,7 +48,7 @@ public abstract class EpisodeCleanupAlgorithm { * @param amountOfRoomNeeded the number of episodes we want to download * @return the number of episodes to delete in order to make room */ - protected int getNumEpisodesToCleanup(final int amountOfRoomNeeded) { + int getNumEpisodesToCleanup(final int amountOfRoomNeeded) { if (amountOfRoomNeeded >= 0 && UserPreferences.getEpisodeCacheSize() != UserPreferences .getEpisodeCacheSizeUnlimited()) { diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/FeedItemStatistics.java b/core/src/main/java/de/danoeh/antennapod/core/storage/FeedItemStatistics.java index 09949b87e..d84279f6e 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/FeedItemStatistics.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/FeedItemStatistics.java @@ -8,11 +8,11 @@ import java.util.Date; * Contains information about a feed's items. */ public class FeedItemStatistics { - private long feedID; - private int numberOfItems; - private int numberOfNewItems; - private int numberOfInProgressItems; - private Date lastUpdate; + private final long feedID; + private final int numberOfItems; + private final int numberOfNewItems; + private final int numberOfInProgressItems; + private final Date lastUpdate; private static final Date UNKNOWN_DATE = new Date(0); @@ -26,7 +26,7 @@ public class FeedItemStatistics { * @param lastUpdate pubDate of the latest episode. A lastUpdate value of 0 will be interpreted as DATE_UNKOWN if * numberOfItems is 0. */ - public FeedItemStatistics(long feedID, int numberOfItems, int numberOfNewItems, int numberOfInProgressItems, Date lastUpdate) { + private FeedItemStatistics(long feedID, int numberOfItems, int numberOfNewItems, int numberOfInProgressItems, Date lastUpdate) { this.feedID = feedID; this.numberOfItems = numberOfItems; this.numberOfNewItems = numberOfNewItems; 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 dc8692866..51b41d3b3 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 @@ -3,31 +3,21 @@ package de.danoeh.antennapod.core.storage; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; +import android.database.DatabaseErrorHandler; import android.database.DatabaseUtils; +import android.database.DefaultDatabaseErrorHandler; import android.database.MergeCursor; import android.database.SQLException; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase.CursorFactory; import android.database.sqlite.SQLiteOpenHelper; import android.media.MediaMetadataRetriever; -import android.os.Build; import android.text.TextUtils; import android.util.Log; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Set; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; - import de.danoeh.antennapod.core.R; import de.danoeh.antennapod.core.event.ProgressEvent; import de.danoeh.antennapod.core.feed.Chapter; import de.danoeh.antennapod.core.feed.Feed; -import de.danoeh.antennapod.core.feed.FeedComponent; -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; @@ -36,6 +26,14 @@ import de.danoeh.antennapod.core.service.download.DownloadStatus; import de.danoeh.antennapod.core.util.LongIntMap; import de.danoeh.antennapod.core.util.flattr.FlattrStatus; import de.greenrobot.event.EventBus; +import org.apache.commons.io.FileUtils; + +import java.io.File; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Set; // TODO Remove media column from feeditem table @@ -45,7 +43,7 @@ import de.greenrobot.event.EventBus; public class PodDBAdapter { private static final String TAG = "PodDBAdapter"; - private static final String DATABASE_NAME = "Antennapod.db"; + public static final String DATABASE_NAME = "Antennapod.db"; /** * Maximum number of arguments for IN-operator. @@ -73,6 +71,7 @@ public class PodDBAdapter { public static final String KEY_SIZE = "filesize"; public static final String KEY_MIME_TYPE = "mime_type"; public static final String KEY_IMAGE = "image"; + public static final String KEY_IMAGE_URL = "image_url"; public static final String KEY_FEED = "feed"; public static final String KEY_MEDIA = "media"; public static final String KEY_DOWNLOADED = "downloaded"; @@ -113,26 +112,26 @@ public class PodDBAdapter { public static final String KEY_EXCLUDE_FILTER = "exclude_filter"; // Table names - private static final String TABLE_NAME_FEEDS = "Feeds"; - private static final String TABLE_NAME_FEED_ITEMS = "FeedItems"; - private static final String TABLE_NAME_FEED_IMAGES = "FeedImages"; - private static final String TABLE_NAME_FEED_MEDIA = "FeedMedia"; - private static final String TABLE_NAME_DOWNLOAD_LOG = "DownloadLog"; - private static final String TABLE_NAME_QUEUE = "Queue"; - private static final String TABLE_NAME_SIMPLECHAPTERS = "SimpleChapters"; - private static final String TABLE_NAME_FAVORITES = "Favorites"; + static final String TABLE_NAME_FEEDS = "Feeds"; + static final String TABLE_NAME_FEED_ITEMS = "FeedItems"; + static final String TABLE_NAME_FEED_IMAGES = "FeedImages"; + static final String TABLE_NAME_FEED_MEDIA = "FeedMedia"; + static final String TABLE_NAME_DOWNLOAD_LOG = "DownloadLog"; + static final String TABLE_NAME_QUEUE = "Queue"; + static final String TABLE_NAME_SIMPLECHAPTERS = "SimpleChapters"; + static final String TABLE_NAME_FAVORITES = "Favorites"; // SQL Statements for creating new tables private static final String TABLE_PRIMARY_KEY = KEY_ID + " INTEGER PRIMARY KEY AUTOINCREMENT ,"; - public static final String CREATE_TABLE_FEEDS = "CREATE TABLE " + private static final String CREATE_TABLE_FEEDS = "CREATE TABLE " + TABLE_NAME_FEEDS + " (" + TABLE_PRIMARY_KEY + KEY_TITLE + " TEXT," + KEY_CUSTOM_TITLE + " TEXT," + KEY_FILE_URL + " TEXT," + KEY_DOWNLOAD_URL + " TEXT," + KEY_DOWNLOADED + " INTEGER," + KEY_LINK + " TEXT," + KEY_DESCRIPTION + " TEXT," + KEY_PAYMENT_LINK + " TEXT," + KEY_LASTUPDATE + " TEXT," + KEY_LANGUAGE + " TEXT," + KEY_AUTHOR - + " TEXT," + KEY_IMAGE + " INTEGER," + KEY_TYPE + " TEXT," + + " TEXT," + KEY_IMAGE_URL + " TEXT," + KEY_TYPE + " TEXT," + KEY_FEED_IDENTIFIER + " TEXT," + KEY_AUTO_DOWNLOAD + " INTEGER DEFAULT 1," + KEY_FLATTR_STATUS + " INTEGER," + KEY_USERNAME + " TEXT," @@ -146,7 +145,7 @@ public class PodDBAdapter { + KEY_LAST_UPDATE_FAILED + " INTEGER DEFAULT 0," + KEY_AUTO_DELETE_ACTION + " INTEGER DEFAULT 0)"; - public static final String CREATE_TABLE_FEED_ITEMS = "CREATE TABLE " + private static final String CREATE_TABLE_FEED_ITEMS = "CREATE TABLE " + TABLE_NAME_FEED_ITEMS + " (" + TABLE_PRIMARY_KEY + KEY_TITLE + " TEXT," + KEY_CONTENT_ENCODED + " TEXT," + KEY_PUBDATE + " INTEGER," + KEY_READ + " INTEGER," + KEY_LINK + " TEXT," @@ -154,15 +153,10 @@ 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_URL + " TEXT," + KEY_AUTO_DOWNLOAD + " INTEGER)"; - public static final String CREATE_TABLE_FEED_IMAGES = "CREATE TABLE " - + TABLE_NAME_FEED_IMAGES + " (" + TABLE_PRIMARY_KEY + KEY_TITLE - + " TEXT," + KEY_FILE_URL + " TEXT," + KEY_DOWNLOAD_URL + " TEXT," - + KEY_DOWNLOADED + " INTEGER)"; - - public static final String CREATE_TABLE_FEED_MEDIA = "CREATE TABLE " + private static final String CREATE_TABLE_FEED_MEDIA = "CREATE TABLE " + TABLE_NAME_FEED_MEDIA + " (" + TABLE_PRIMARY_KEY + KEY_DURATION + " INTEGER," + KEY_FILE_URL + " TEXT," + KEY_DOWNLOAD_URL + " TEXT," + KEY_DOWNLOADED + " INTEGER," + KEY_POSITION @@ -173,53 +167,48 @@ public class PodDBAdapter { + KEY_HAS_EMBEDDED_PICTURE + " INTEGER," + KEY_LAST_PLAYED_TIME + " INTEGER)"; - public static final String CREATE_TABLE_DOWNLOAD_LOG = "CREATE TABLE " + private static final String CREATE_TABLE_DOWNLOAD_LOG = "CREATE TABLE " + TABLE_NAME_DOWNLOAD_LOG + " (" + TABLE_PRIMARY_KEY + KEY_FEEDFILE + " INTEGER," + KEY_FEEDFILETYPE + " INTEGER," + KEY_REASON + " INTEGER," + KEY_SUCCESSFUL + " INTEGER," + KEY_COMPLETION_DATE + " INTEGER," + KEY_REASON_DETAILED + " TEXT," + KEY_DOWNLOADSTATUS_TITLE + " TEXT)"; - public static final String CREATE_TABLE_QUEUE = "CREATE TABLE " + private static final String CREATE_TABLE_QUEUE = "CREATE TABLE " + TABLE_NAME_QUEUE + "(" + KEY_ID + " INTEGER PRIMARY KEY," + KEY_FEEDITEM + " INTEGER," + KEY_FEED + " INTEGER)"; - public static final String CREATE_TABLE_SIMPLECHAPTERS = "CREATE TABLE " + private static final String CREATE_TABLE_SIMPLECHAPTERS = "CREATE TABLE " + TABLE_NAME_SIMPLECHAPTERS + " (" + TABLE_PRIMARY_KEY + KEY_TITLE + " 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 " + 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_FEEDITEMS_PUBDATE = "CREATE INDEX IF NOT EXISTS " + static final String CREATE_INDEX_FEEDITEMS_PUBDATE = "CREATE INDEX IF NOT EXISTS " + TABLE_NAME_FEED_ITEMS + "_" + KEY_PUBDATE + " ON " + TABLE_NAME_FEED_ITEMS + " (" + KEY_PUBDATE + ")"; - public static final String CREATE_INDEX_FEEDITEMS_READ = "CREATE INDEX IF NOT EXISTS " + static final String CREATE_INDEX_FEEDITEMS_READ = "CREATE INDEX IF NOT EXISTS " + TABLE_NAME_FEED_ITEMS + "_" + KEY_READ + " ON " + TABLE_NAME_FEED_ITEMS + " (" + KEY_READ + ")"; - - public static final String CREATE_INDEX_QUEUE_FEEDITEM = "CREATE INDEX " + 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 " + 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 " + static final String CREATE_INDEX_SIMPLECHAPTERS_FEEDITEM = "CREATE INDEX " + TABLE_NAME_SIMPLECHAPTERS + "_" + KEY_FEEDITEM + " ON " + TABLE_NAME_SIMPLECHAPTERS + " (" + KEY_FEEDITEM + ")"; - public static final String CREATE_TABLE_FAVORITES = "CREATE TABLE " + static final String CREATE_TABLE_FAVORITES = "CREATE TABLE " + TABLE_NAME_FAVORITES + "(" + KEY_ID + " INTEGER PRIMARY KEY," + KEY_FEEDITEM + " INTEGER," + KEY_FEED + " INTEGER)"; @@ -239,7 +228,7 @@ public class PodDBAdapter { TABLE_NAME_FEEDS + "." + KEY_LASTUPDATE, TABLE_NAME_FEEDS + "." + KEY_LANGUAGE, TABLE_NAME_FEEDS + "." + KEY_AUTHOR, - TABLE_NAME_FEEDS + "." + KEY_IMAGE, + TABLE_NAME_FEEDS + "." + KEY_IMAGE_URL, TABLE_NAME_FEEDS + "." + KEY_TYPE, TABLE_NAME_FEEDS + "." + KEY_FEED_IDENTIFIER, TABLE_NAME_FEEDS + "." + KEY_AUTO_DOWNLOAD, @@ -272,7 +261,7 @@ 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_URL, TABLE_NAME_FEED_ITEMS + "." + KEY_AUTO_DOWNLOAD }; @@ -282,7 +271,6 @@ public class PodDBAdapter { private static final String[] ALL_TABLES = { TABLE_NAME_FEEDS, TABLE_NAME_FEED_ITEMS, - TABLE_NAME_FEED_IMAGES, TABLE_NAME_FEED_MEDIA, TABLE_NAME_DOWNLOAD_LOG, TABLE_NAME_QUEUE, @@ -307,72 +295,57 @@ public class PodDBAdapter { KEY_CONTENT_ENCODED, KEY_FEED}; private static Context context; - private static PodDBHelper dbHelper; private static volatile SQLiteDatabase db; - private static Lock dbLock = new ReentrantLock(); - private static AtomicInteger counter = new AtomicInteger(0); + private static int counter = 0; public static void init(Context context) { PodDBAdapter.context = context.getApplicationContext(); } - private static class PodDBHelperholder { - public static final PodDBHelper dbHelper = new PodDBHelper(PodDBAdapter.context, DATABASE_NAME, null); + // Bill Pugh Singleton Implementation + private static class SingletonHolder { + private static final PodDBHelper dbHelper = new PodDBHelper(PodDBAdapter.context, DATABASE_NAME, null); + private static final PodDBAdapter dbAdapter = new PodDBAdapter(); } public static PodDBAdapter getInstance() { - dbHelper = PodDBHelperholder.dbHelper; - return new PodDBAdapter(); + return SingletonHolder.dbAdapter; } private PodDBAdapter() { } - public PodDBAdapter open() { - int adapters = counter.incrementAndGet(); - Log.v(TAG, "Opening DB #" + adapters); + public synchronized PodDBAdapter open() { + counter++; + Log.v(TAG, "Opening DB #" + counter); - if ((db == null) || (!db.isOpen()) || (db.isReadOnly())) { - try { - dbLock.lock(); - if ((db == null) || (!db.isOpen()) || (db.isReadOnly())) { - db = openDb(); - } - } finally { - dbLock.unlock(); - } + if (db == null || !db.isOpen() || db.isReadOnly()) { + db = openDb(); } return this; } private SQLiteDatabase openDb() { - SQLiteDatabase newDb = null; + SQLiteDatabase newDb; try { - newDb = dbHelper.getWritableDatabase(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { - newDb.enableWriteAheadLogging(); - } + newDb = SingletonHolder.dbHelper.getWritableDatabase(); + newDb.enableWriteAheadLogging(); } catch (SQLException ex) { Log.e(TAG, Log.getStackTraceString(ex)); - newDb = dbHelper.getReadableDatabase(); + newDb = SingletonHolder.dbHelper.getReadableDatabase(); } return newDb; } - public void close() { - int adapters = counter.decrementAndGet(); - Log.v(TAG, "Closing DB #" + adapters); + public synchronized void close() { + counter--; + Log.v(TAG, "Closing DB #" + counter); - if (adapters == 0) { + if (counter == 0) { Log.v(TAG, "Closing DB, really"); - try { - dbLock.lock(); - db.close(); - db = null; - } finally { - dbLock.unlock(); - } + db.close(); + db = null; } } @@ -394,7 +367,7 @@ public class PodDBAdapter { * * @return the id of the entry */ - public long setFeed(Feed feed) { + private long setFeed(Feed feed) { ContentValues values = new ContentValues(); values.put(KEY_TITLE, feed.getFeedTitle()); values.put(KEY_LINK, feed.getLink()); @@ -402,12 +375,7 @@ public class PodDBAdapter { values.put(KEY_PAYMENT_LINK, feed.getPaymentLink()); values.put(KEY_AUTHOR, feed.getAuthor()); values.put(KEY_LANGUAGE, feed.getLanguage()); - if (feed.getImage() != null) { - if (feed.getImage().getId() == 0) { - setImage(feed.getImage()); - } - values.put(KEY_IMAGE, feed.getImage().getId()); - } + values.put(KEY_IMAGE_URL, feed.getImageUrl()); values.put(KEY_FILE_URL, feed.getFile_url()); values.put(KEY_DOWNLOAD_URL, feed.getDownload_url()); @@ -464,58 +432,7 @@ public class PodDBAdapter { } /** - * Inserts or updates an image entry - * - * @return the id of the entry - */ - public long setImage(FeedImage image) { - boolean startedTransaction = false; - - try { - if (!db.inTransaction()) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { - db.beginTransactionNonExclusive(); - } else { - db.beginTransaction(); - } - startedTransaction = true; - } - - ContentValues values = new ContentValues(); - values.put(KEY_TITLE, image.getTitle()); - values.put(KEY_DOWNLOAD_URL, image.getDownload_url()); - values.put(KEY_DOWNLOADED, image.isDownloaded()); - values.put(KEY_FILE_URL, image.getFile_url()); - if (image.getId() == 0) { - image.setId(db.insert(TABLE_NAME_FEED_IMAGES, null, values)); - } else { - db.update(TABLE_NAME_FEED_IMAGES, values, KEY_ID + "=?", - new String[]{String.valueOf(image.getId())}); - } - - final FeedComponent owner = image.getOwner(); - if (owner != null && owner.getId() != 0) { - values.clear(); - values.put(KEY_IMAGE, image.getId()); - if (owner instanceof Feed) { - db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?", new String[]{String.valueOf(image.getOwner().getId())}); - } - } - if (startedTransaction) { - db.setTransactionSuccessful(); - } - } catch (SQLException e) { - Log.e(TAG, Log.getStackTraceString(e)); - } finally { - if (startedTransaction) { - db.endTransaction(); - } - } - return image.getId(); - } - - /** - * Inserts or updates an image entry + * Inserts or updates a media entry * * @return the id of the entry */ @@ -580,11 +497,7 @@ public class PodDBAdapter { */ public void setCompleteFeed(Feed... feeds) { try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { - db.beginTransactionNonExclusive(); - } else { - db.beginTransaction(); - } + db.beginTransactionNonExclusive(); for (Feed feed : feeds) { setFeed(feed); if (feed.getItems() != null) { @@ -630,31 +543,6 @@ public class PodDBAdapter { } /** - * Counts feeds and feed items in the flattr queue - */ - public int getFlattrQueueSize() { - int res = 0; - Cursor c = db.rawQuery(String.format("SELECT count(*) FROM %s WHERE %s=%s", - TABLE_NAME_FEEDS, KEY_FLATTR_STATUS, String.valueOf(FlattrStatus.STATUS_QUEUE)), null); - if (c.moveToFirst()) { - res = c.getInt(0); - c.close(); - } else { - Log.e(TAG, "Unable to determine size of flattr queue: Could not count number of feeds"); - } - c = db.rawQuery(String.format("SELECT count(*) FROM %s WHERE %s=%s", - TABLE_NAME_FEED_ITEMS, KEY_FLATTR_STATUS, String.valueOf(FlattrStatus.STATUS_QUEUE)), null); - if (c.moveToFirst()) { - res += c.getInt(0); - c.close(); - } else { - Log.e(TAG, "Unable to determine size of flattr queue: Could not count number of feed items"); - } - - return res; - } - - /** * Updates the download URL of a Feed. */ public void setFeedDownloadUrl(String original, String updated) { @@ -665,11 +553,7 @@ public class PodDBAdapter { public void setFeedItemlist(List<FeedItem> items) { try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { - db.beginTransactionNonExclusive(); - } else { - db.beginTransaction(); - } + db.beginTransactionNonExclusive(); for (FeedItem item : items) { setFeedItem(item, true); } @@ -684,11 +568,7 @@ public class PodDBAdapter { public long setSingleFeedItem(FeedItem item) { long result = 0; try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { - db.beginTransactionNonExclusive(); - } else { - db.beginTransaction(); - } + db.beginTransactionNonExclusive(); result = setFeedItem(item, true); db.setTransactionSuccessful(); } catch (SQLException e) { @@ -789,12 +669,7 @@ public class PodDBAdapter { 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()); - } - values.put(KEY_IMAGE, item.getImage().getId()); - } + values.put(KEY_IMAGE_URL, item.getImageUrl()); if (item.getId() == 0) { item.setId(db.insert(TABLE_NAME_FEED_ITEMS, null, values)); @@ -814,11 +689,7 @@ public class PodDBAdapter { public void setFeedItemRead(int played, long itemId, long mediaId, boolean resetMediaPosition) { try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { - db.beginTransactionNonExclusive(); - } else { - db.beginTransaction(); - } + db.beginTransactionNonExclusive(); ContentValues values = new ContentValues(); values.put(KEY_READ, played); @@ -846,11 +717,7 @@ public class PodDBAdapter { */ public void setFeedItemRead(int read, long... itemIds) { try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { - db.beginTransactionNonExclusive(); - } else { - db.beginTransaction(); - } + db.beginTransactionNonExclusive(); ContentValues values = new ContentValues(); for (long id : itemIds) { values.clear(); @@ -865,7 +732,7 @@ public class PodDBAdapter { } } - public void setChapters(FeedItem item) { + private void setChapters(FeedItem item) { ContentValues values = new ContentValues(); for (Chapter chapter : item.getChapters()) { values.put(KEY_TITLE, chapter.getTitle()); @@ -933,11 +800,7 @@ public class PodDBAdapter { public void setFavorites(List<FeedItem> favorites) { ContentValues values = new ContentValues(); try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { - db.beginTransactionNonExclusive(); - } else { - db.beginTransaction(); - } + db.beginTransactionNonExclusive(); db.delete(TABLE_NAME_FAVORITES, null, null); for (int i = 0; i < favorites.size(); i++) { FeedItem item = favorites.get(i); @@ -977,7 +840,7 @@ public class PodDBAdapter { db.execSQL(deleteClause); } - public boolean isItemInFavorites(FeedItem item) { + private boolean isItemInFavorites(FeedItem item) { String query = String.format("SELECT %s from %s WHERE %s=%d", KEY_ID, TABLE_NAME_FAVORITES, KEY_FEEDITEM, item.getId()); Cursor c = db.rawQuery(query, null); @@ -986,25 +849,10 @@ public class PodDBAdapter { return count > 0; } - 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); - long count = 0; - if (result.moveToFirst()) { - count = result.getLong(0); - } - result.close(); - return count; - } - public void setQueue(List<FeedItem> queue) { ContentValues values = new ContentValues(); try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { - db.beginTransactionNonExclusive(); - } else { - db.beginTransaction(); - } + db.beginTransactionNonExclusive(); db.delete(TABLE_NAME_QUEUE, null, null); for (int i = 0; i < queue.size(); i++) { FeedItem item = queue.get(i); @@ -1025,7 +873,7 @@ public class PodDBAdapter { db.delete(TABLE_NAME_QUEUE, null, null); } - public void removeFeedMedia(FeedMedia media) { + private void removeFeedMedia(FeedMedia media) { // delete download log entries for feed media db.delete(TABLE_NAME_DOWNLOAD_LOG, KEY_FEEDFILE + "=? AND " + KEY_FEEDFILETYPE + "=?", new String[]{String.valueOf(media.getId()), String.valueOf(FeedMedia.FEEDFILETYPE_FEEDMEDIA)}); @@ -1034,29 +882,21 @@ public class PodDBAdapter { new String[]{String.valueOf(media.getId())}); } - public void removeChaptersOfItem(FeedItem item) { + private void removeChaptersOfItem(FeedItem item) { db.delete(TABLE_NAME_SIMPLECHAPTERS, KEY_FEEDITEM + "=?", new String[]{String.valueOf(item.getId())}); } - public void removeFeedImage(FeedImage image) { - db.delete(TABLE_NAME_FEED_IMAGES, KEY_ID + "=?", - new String[]{String.valueOf(image.getId())}); - } - /** * Remove a FeedItem and its FeedMedia entry. */ - public void removeFeedItem(FeedItem item) { + private void removeFeedItem(FeedItem item) { if (item.getMedia() != null) { removeFeedMedia(item.getMedia()); } if (item.hasChapters() || item.getChapters() != null) { removeChaptersOfItem(item); } - if (item.hasItemImage()) { - removeFeedImage(item.getImage()); - } db.delete(TABLE_NAME_FEED_ITEMS, KEY_ID + "=?", new String[]{String.valueOf(item.getId())}); } @@ -1066,14 +906,7 @@ public class PodDBAdapter { */ public void removeFeed(Feed feed) { try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { - db.beginTransactionNonExclusive(); - } else { - db.beginTransaction(); - } - if (feed.getImage() != null) { - removeFeedImage(feed.getImage()); - } + db.beginTransactionNonExclusive(); if (feed.getItems() != null) { for (FeedItem item : feed.getItems()) { removeFeedItem(item); @@ -1127,7 +960,7 @@ public class PodDBAdapter { return getAllItemsOfFeedCursor(feed.getId()); } - public final Cursor getAllItemsOfFeedCursor(final long feedId) { + private Cursor getAllItemsOfFeedCursor(final long feedId) { return db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, KEY_FEED + "=?", new String[]{String.valueOf(feedId)}, null, null, null); @@ -1144,18 +977,6 @@ public class PodDBAdapter { } /** - * Returns a cursor for a DB query in the FeedMedia table for a given ID. - * - * @param item The item you want to get the FeedMedia from - * @return The cursor of the query - */ - public final Cursor getFeedMediaOfItemCursor(final FeedItem item) { - return db.query(TABLE_NAME_FEED_MEDIA, null, KEY_ID + "=?", - new String[]{String.valueOf(item.getMedia().getId())}, null, - null, null); - } - - /** * Returns a cursor for a DB query in the FeedImages table for given IDs. * * @param imageIds IDs of the images @@ -1370,11 +1191,7 @@ public class PodDBAdapter { if (size == 1) { return "(?)"; } - StringBuilder builder = - new StringBuilder("(") - .append(TextUtils.join(",", Collections.nCopies(size, "?"))) - .append(")"); - return builder.toString(); + return "(" + TextUtils.join(",", Collections.nCopies(size, "?")) + ")"; } public final Cursor getFeedCursor(final long id) { @@ -1417,17 +1234,12 @@ public class PodDBAdapter { public Cursor getImageAuthenticationCursor(final String imageUrl) { String downloadUrl = DatabaseUtils.sqlEscapeString(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 + "=" + downloadUrl - + " 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 + + "SELECT " + KEY_USERNAME + "," + KEY_PASSWORD + " 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_IMAGES + "." + KEY_DOWNLOAD_URL + "=" + downloadUrl; + + " ON " + TABLE_NAME_FEED_ITEMS + "." + KEY_FEED + " = " + TABLE_NAME_FEEDS + "." + KEY_ID + + " WHERE " + TABLE_NAME_FEED_ITEMS + "." + KEY_IMAGE_URL + "=" + downloadUrl + + " UNION SELECT " + KEY_USERNAME + "," + KEY_PASSWORD + " FROM " + TABLE_NAME_FEEDS + + " WHERE " + TABLE_NAME_FEEDS + "." + KEY_IMAGE_URL + "=" + downloadUrl; return db.rawQuery(query, null); } @@ -1700,13 +1512,35 @@ public class PodDBAdapter { } /** + * Called when a database corruption happens + */ + public static class PodDbErrorHandler implements DatabaseErrorHandler { + @Override + public void onCorruption(SQLiteDatabase db) { + Log.e(TAG, "Database corrupted: " + db.getPath()); + + File dbPath = new File(db.getPath()); + File backupFolder = PodDBAdapter.context.getExternalFilesDir(null); + File backupFile = new File(backupFolder, "CorruptedDatabaseBackup.db"); + try { + FileUtils.copyFile(dbPath, backupFile); + Log.d(TAG, "Dumped database to " + backupFile.getPath()); + } catch (IOException e) { + Log.d(TAG, Log.getStackTraceString(e)); + } + + new DefaultDatabaseErrorHandler().onCorruption(db); // This deletes the database + } + } + + /** * Helper class for opening the Antennapod database. */ private static class PodDBHelper extends SQLiteOpenHelper { - private static final int VERSION = 1060200; + private static final int VERSION = 1060596; - private Context context; + private final Context context; /** * Constructor. @@ -1717,7 +1551,7 @@ public class PodDBAdapter { */ public PodDBHelper(final Context context, final String name, final CursorFactory factory) { - super(context, name, factory, VERSION); + super(context, name, factory, VERSION, new PodDbErrorHandler()); this.context = context; } @@ -1725,7 +1559,6 @@ public class PodDBAdapter { public void onCreate(final SQLiteDatabase db) { db.execSQL(CREATE_TABLE_FEEDS); db.execSQL(CREATE_TABLE_FEED_ITEMS); - db.execSQL(CREATE_TABLE_FEED_IMAGES); db.execSQL(CREATE_TABLE_FEED_MEDIA); db.execSQL(CREATE_TABLE_DOWNLOAD_LOG); db.execSQL(CREATE_TABLE_QUEUE); @@ -1733,7 +1566,6 @@ public class PodDBAdapter { db.execSQL(CREATE_TABLE_FAVORITES); db.execSQL(CREATE_INDEX_FEEDITEMS_FEED); - db.execSQL(CREATE_INDEX_FEEDITEMS_IMAGE); db.execSQL(CREATE_INDEX_FEEDITEMS_PUBDATE); db.execSQL(CREATE_INDEX_FEEDITEMS_READ); db.execSQL(CREATE_INDEX_FEEDMEDIA_FEEDITEM); @@ -1748,263 +1580,7 @@ public class PodDBAdapter { EventBus.getDefault().post(ProgressEvent.start(context.getString(R.string.progress_upgrading_database))); Log.w("DBAdapter", "Upgrading from version " + oldVersion + " to " + newVersion + "."); - if (oldVersion <= 1) { - db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + " ADD COLUMN " - + KEY_TYPE + " TEXT"); - } - if (oldVersion <= 2) { - db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_SIMPLECHAPTERS - + " ADD COLUMN " + KEY_LINK + " TEXT"); - } - if (oldVersion <= 3) { - db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS - + " ADD COLUMN " + KEY_ITEM_IDENTIFIER + " TEXT"); - } - if (oldVersion <= 4) { - db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + " ADD COLUMN " - + KEY_FEED_IDENTIFIER + " TEXT"); - } - if (oldVersion <= 5) { - db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_DOWNLOAD_LOG - + " ADD COLUMN " + KEY_REASON_DETAILED + " TEXT"); - db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_DOWNLOAD_LOG - + " ADD COLUMN " + KEY_DOWNLOADSTATUS_TITLE + " TEXT"); - } - if (oldVersion <= 6) { - db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_SIMPLECHAPTERS - + " ADD COLUMN " + KEY_CHAPTER_TYPE + " INTEGER"); - } - if (oldVersion <= 7) { - db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_MEDIA - + " ADD COLUMN " + KEY_PLAYBACK_COMPLETION_DATE - + " INTEGER"); - } - if (oldVersion <= 8) { - final int KEY_ID_POSITION = 0; - final int KEY_MEDIA_POSITION = 1; - - // Add feeditem column to feedmedia table - db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_MEDIA - + " ADD COLUMN " + KEY_FEEDITEM - + " INTEGER"); - Cursor feeditemCursor = db.query(PodDBAdapter.TABLE_NAME_FEED_ITEMS, - new String[]{KEY_ID, KEY_MEDIA}, "? > 0", - new String[]{KEY_MEDIA}, null, null, null); - if (feeditemCursor.moveToFirst()) { - db.beginTransaction(); - ContentValues contentValues = new ContentValues(); - do { - long mediaId = feeditemCursor.getLong(KEY_MEDIA_POSITION); - contentValues.put(KEY_FEEDITEM, feeditemCursor.getLong(KEY_ID_POSITION)); - db.update(PodDBAdapter.TABLE_NAME_FEED_MEDIA, contentValues, KEY_ID + "=?", new String[]{String.valueOf(mediaId)}); - contentValues.clear(); - } while (feeditemCursor.moveToNext()); - db.setTransactionSuccessful(); - db.endTransaction(); - } - feeditemCursor.close(); - } - if (oldVersion <= 9) { - db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS - + " ADD COLUMN " + KEY_AUTO_DOWNLOAD - + " INTEGER DEFAULT 1"); - } - if (oldVersion <= 10) { - db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS - + " ADD COLUMN " + KEY_FLATTR_STATUS - + " INTEGER"); - db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS - + " ADD COLUMN " + KEY_FLATTR_STATUS - + " INTEGER"); - db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_MEDIA - + " ADD COLUMN " + KEY_PLAYED_DURATION - + " INTEGER"); - } - if (oldVersion <= 11) { - db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS - + " ADD COLUMN " + KEY_USERNAME - + " TEXT"); - db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS - + " ADD COLUMN " + KEY_PASSWORD - + " TEXT"); - db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS - + " ADD COLUMN " + KEY_IMAGE - + " INTEGER"); - } - if (oldVersion <= 12) { - db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS - + " ADD COLUMN " + KEY_IS_PAGED + " INTEGER DEFAULT 0"); - db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS - + " ADD COLUMN " + KEY_NEXT_PAGE_LINK + " TEXT"); - } - if (oldVersion <= 13) { - // remove duplicate rows in "Chapters" table that were created because of a bug. - db.execSQL(String.format("DELETE FROM %s WHERE %s NOT IN " + - "(SELECT MIN(%s) as %s FROM %s GROUP BY %s,%s,%s,%s,%s)", - PodDBAdapter.TABLE_NAME_SIMPLECHAPTERS, - KEY_ID, - KEY_ID, - KEY_ID, - PodDBAdapter.TABLE_NAME_SIMPLECHAPTERS, - KEY_TITLE, - KEY_START, - KEY_FEEDITEM, - KEY_LINK, - KEY_CHAPTER_TYPE)); - } - if (oldVersion <= 14) { - db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS - + " ADD COLUMN " + KEY_AUTO_DOWNLOAD + " INTEGER"); - db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS - + " SET " + KEY_AUTO_DOWNLOAD + " = " - + "(SELECT " + KEY_AUTO_DOWNLOAD - + " FROM " + PodDBAdapter.TABLE_NAME_FEEDS - + " WHERE " + PodDBAdapter.TABLE_NAME_FEEDS + "." + KEY_ID - + " = " + PodDBAdapter.TABLE_NAME_FEED_ITEMS + "." + KEY_FEED + ")"); - - db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS - + " ADD COLUMN " + KEY_HIDE + " TEXT"); - - db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS - + " ADD COLUMN " + KEY_LAST_UPDATE_FAILED + " INTEGER DEFAULT 0"); - - // create indexes - db.execSQL(PodDBAdapter.CREATE_INDEX_FEEDITEMS_FEED); - db.execSQL(PodDBAdapter.CREATE_INDEX_FEEDITEMS_IMAGE); - db.execSQL(PodDBAdapter.CREATE_INDEX_FEEDMEDIA_FEEDITEM); - db.execSQL(PodDBAdapter.CREATE_INDEX_QUEUE_FEEDITEM); - db.execSQL(PodDBAdapter.CREATE_INDEX_SIMPLECHAPTERS_FEEDITEM); - } - if (oldVersion <= 15) { - db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_MEDIA - + " ADD COLUMN " + KEY_HAS_EMBEDDED_PICTURE + " INTEGER DEFAULT -1"); - db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEED_MEDIA - + " SET " + KEY_HAS_EMBEDDED_PICTURE + "=0" - + " WHERE " + KEY_DOWNLOADED + "=0"); - Cursor c = db.rawQuery("SELECT " + KEY_FILE_URL - + " FROM " + PodDBAdapter.TABLE_NAME_FEED_MEDIA - + " WHERE " + KEY_DOWNLOADED + "=1 " - + " AND " + KEY_HAS_EMBEDDED_PICTURE + "=-1", null); - if (c.moveToFirst()) { - MediaMetadataRetriever mmr = new MediaMetadataRetriever(); - do { - String fileUrl = c.getString(0); - try { - mmr.setDataSource(fileUrl); - byte[] image = mmr.getEmbeddedPicture(); - if (image != null) { - db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEED_MEDIA - + " SET " + KEY_HAS_EMBEDDED_PICTURE + "=1" - + " WHERE " + KEY_FILE_URL + "='" + fileUrl + "'"); - } else { - db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEED_MEDIA - + " SET " + KEY_HAS_EMBEDDED_PICTURE + "=0" - + " WHERE " + KEY_FILE_URL + "='" + fileUrl + "'"); - } - } catch (Exception e) { - e.printStackTrace(); - } - } while (c.moveToNext()); - } - c.close(); - } - if (oldVersion <= 16) { - String selectNew = "SELECT " + PodDBAdapter.TABLE_NAME_FEED_ITEMS + "." + KEY_ID - + " FROM " + PodDBAdapter.TABLE_NAME_FEED_ITEMS - + " INNER JOIN " + PodDBAdapter.TABLE_NAME_FEED_MEDIA + " ON " - + PodDBAdapter.TABLE_NAME_FEED_ITEMS + "." + KEY_ID + "=" - + PodDBAdapter.TABLE_NAME_FEED_MEDIA + "." + KEY_FEEDITEM - + " LEFT OUTER JOIN " + PodDBAdapter.TABLE_NAME_QUEUE + " ON " - + PodDBAdapter.TABLE_NAME_FEED_ITEMS + "." + KEY_ID + "=" - + PodDBAdapter.TABLE_NAME_QUEUE + "." + KEY_FEEDITEM - + " WHERE " - + PodDBAdapter.TABLE_NAME_FEED_ITEMS + "." + KEY_READ + " = 0 AND " // unplayed - + PodDBAdapter.TABLE_NAME_FEED_MEDIA + "." + KEY_DOWNLOADED + " = 0 AND " // undownloaded - + PodDBAdapter.TABLE_NAME_FEED_MEDIA + "." + KEY_POSITION + " = 0 AND " // not partially played - + PodDBAdapter.TABLE_NAME_QUEUE + "." + KEY_ID + " IS NULL"; // not in queue - String sql = "UPDATE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS - + " SET " + KEY_READ + "=" + FeedItem.NEW - + " WHERE " + KEY_ID + " IN (" + selectNew + ")"; - Log.d("Migration", "SQL: " + sql); - db.execSQL(sql); - } - if (oldVersion <= 17) { - db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS - + " ADD COLUMN " + PodDBAdapter.KEY_AUTO_DELETE_ACTION + " INTEGER DEFAULT 0"); - } - if (oldVersion < 1030005) { - db.execSQL("UPDATE FeedItems SET auto_download=0 WHERE " + - "(read=1 OR id IN (SELECT feeditem FROM FeedMedia WHERE position>0 OR downloaded=1)) " + - "AND id NOT IN (SELECT feeditem FROM Queue)"); - } - if (oldVersion < 1040001) { - db.execSQL(CREATE_TABLE_FAVORITES); - } - if (oldVersion < 1040002) { - db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_MEDIA - + " ADD COLUMN " + PodDBAdapter.KEY_LAST_PLAYED_TIME + " INTEGER DEFAULT 0"); - } - if (oldVersion < 1040013) { - db.execSQL(PodDBAdapter.CREATE_INDEX_FEEDITEMS_PUBDATE); - db.execSQL(PodDBAdapter.CREATE_INDEX_FEEDITEMS_READ); - } - if (oldVersion < 1050003) { - // Migrates feed list filter data - - db.beginTransaction(); - - // Change to intermediate values to avoid overwriting in the following find/replace - db.execSQL("UPDATE " + TABLE_NAME_FEEDS + "\n" + - "SET " + KEY_HIDE + " = replace(" + KEY_HIDE + ", 'unplayed', 'noplay')"); - db.execSQL("UPDATE " + TABLE_NAME_FEEDS + "\n" + - "SET " + KEY_HIDE + " = replace(" + KEY_HIDE + ", 'not_queued', 'noqueue')"); - db.execSQL("UPDATE " + TABLE_NAME_FEEDS + "\n" + - "SET " + KEY_HIDE + " = replace(" + KEY_HIDE + ", 'not_downloaded', 'nodl')"); - - // Replace played, queued, and downloaded with their opposites - db.execSQL("UPDATE " + TABLE_NAME_FEEDS + "\n" + - "SET " + KEY_HIDE + " = replace(" + KEY_HIDE + ", 'played', 'unplayed')"); - db.execSQL("UPDATE " + TABLE_NAME_FEEDS + "\n" + - "SET " + KEY_HIDE + " = replace(" + KEY_HIDE + ", 'queued', 'not_queued')"); - db.execSQL("UPDATE " + TABLE_NAME_FEEDS + "\n" + - "SET " + KEY_HIDE + " = replace(" + KEY_HIDE + ", 'downloaded', 'not_downloaded')"); - - // Now replace intermediates for unplayed, not queued, etc. with their opposites - db.execSQL("UPDATE " + TABLE_NAME_FEEDS + "\n" + - "SET " + KEY_HIDE + " = replace(" + KEY_HIDE + ", 'noplay', 'played')"); - db.execSQL("UPDATE " + TABLE_NAME_FEEDS + "\n" + - "SET " + KEY_HIDE + " = replace(" + KEY_HIDE + ", 'noqueue', 'queued')"); - db.execSQL("UPDATE " + TABLE_NAME_FEEDS + "\n" + - "SET " + KEY_HIDE + " = replace(" + KEY_HIDE + ", 'nodl', 'downloaded')"); - - // Paused doesn't have an opposite, so unplayed is the next best option - db.execSQL("UPDATE " + TABLE_NAME_FEEDS + "\n" + - "SET " + KEY_HIDE + " = replace(" + KEY_HIDE + ", 'paused', 'unplayed')"); - - db.setTransactionSuccessful(); - db.endTransaction(); - - // and now get ready for autodownload filters - db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS - + " ADD COLUMN " + PodDBAdapter.KEY_INCLUDE_FILTER + " TEXT DEFAULT ''"); - - db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS - + " ADD COLUMN " + PodDBAdapter.KEY_EXCLUDE_FILTER + " TEXT DEFAULT ''"); - - // and now auto refresh - db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS - + " ADD COLUMN " + PodDBAdapter.KEY_KEEP_UPDATED + " INTEGER DEFAULT 1"); - } - if (oldVersion < 1050004) { - // prevent old timestamps to be misinterpreted as ETags - db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEEDS - + " SET " + PodDBAdapter.KEY_LASTUPDATE + "=NULL"); - } - if (oldVersion < 1060200) { - db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS - + " ADD COLUMN " + PodDBAdapter.KEY_CUSTOM_TITLE + " TEXT"); - } - + DBUpgrader.upgrade(db, oldVersion, newVersion); EventBus.getDefault().post(ProgressEvent.end()); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/FeedHandler.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/FeedHandler.java index 9efc5888f..8f2ce5465 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/FeedHandler.java +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/FeedHandler.java @@ -1,17 +1,19 @@ package de.danoeh.antennapod.core.syndication.handler; -import de.danoeh.antennapod.core.feed.Feed; import org.apache.commons.io.input.XmlStreamReader; import org.xml.sax.InputSource; import org.xml.sax.SAXException; -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.parsers.SAXParser; -import javax.xml.parsers.SAXParserFactory; import java.io.File; import java.io.IOException; import java.io.Reader; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +import de.danoeh.antennapod.core.feed.Feed; + public class FeedHandler { public FeedHandlerResult parseFeed(Feed feed) throws SAXException, IOException, diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/FeedHandlerResult.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/FeedHandlerResult.java index f67721a6e..77300d864 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/FeedHandlerResult.java +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/FeedHandlerResult.java @@ -9,8 +9,8 @@ import de.danoeh.antennapod.core.feed.Feed; */ public class FeedHandlerResult { - public Feed feed; - public Map<String, String> alternateFeedUrls; + public final Feed feed; + public final Map<String, String> alternateFeedUrls; public FeedHandlerResult(Feed feed, Map<String, String> alternateFeedUrls) { this.feed = feed; diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/HandlerState.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/HandlerState.java index 66513a12e..1cd05aa26 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/HandlerState.java +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/HandlerState.java @@ -20,29 +20,29 @@ public class HandlerState { /** * Feed that the Handler is currently processing. */ - protected Feed feed; + Feed feed; /** * Contains links to related feeds, e.g. feeds with enclosures in other formats. The key of the map is the * URL of the feed, the value is the title */ - protected Map<String, String> alternateUrls; - protected ArrayList<FeedItem> items; - protected FeedItem currentItem; - protected Stack<SyndElement> tagstack; + final Map<String, String> alternateUrls; + private final ArrayList<FeedItem> items; + private FeedItem currentItem; + final Stack<SyndElement> tagstack; /** * Namespaces that have been defined so far. */ - protected Map<String, Namespace> namespaces; - protected Stack<Namespace> defaultNamespaces; + final Map<String, Namespace> namespaces; + final Stack<Namespace> defaultNamespaces; /** * Buffer for saving characters. */ - protected StringBuffer contentBuf; + protected StringBuilder contentBuf; /** * Temporarily saved objects. */ - protected Map<String, Object> tempObjects; + private final Map<String, Object> tempObjects; public HandlerState(Feed feed) { this.feed = feed; @@ -97,7 +97,7 @@ public class HandlerState { return third; } - public StringBuffer getContentBuf() { + public StringBuilder getContentBuf() { return contentBuf; } diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/SyndHandler.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/SyndHandler.java index ae91c0743..ab66b912b 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/SyndHandler.java +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/SyndHandler.java @@ -18,10 +18,10 @@ import de.danoeh.antennapod.core.syndication.namespace.SyndElement; import de.danoeh.antennapod.core.syndication.namespace.atom.NSAtom; /** Superclass for all SAX Handlers which process Syndication formats */ -public class SyndHandler extends DefaultHandler { +class SyndHandler extends DefaultHandler { private static final String TAG = "SyndHandler"; private static final String DEFAULT_PREFIX = ""; - protected HandlerState state; + final HandlerState state; public SyndHandler(Feed feed, TypeGetter.Type type) { state = new HandlerState(feed); @@ -33,7 +33,7 @@ public class SyndHandler extends DefaultHandler { @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { - state.contentBuf = new StringBuffer(); + state.contentBuf = new StringBuilder(); Namespace handler = getHandlingNamespace(uri, qName); if (handler != null) { SyndElement element = handler.handleElementStart(localName, state, diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/TypeGetter.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/TypeGetter.java index ee0a71f30..b4c77e58d 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/TypeGetter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/TypeGetter.java @@ -54,18 +54,19 @@ public class TypeGetter { return Type.ATOM; case RSS_ROOT: String strVersion = xpp.getAttributeValue(null, "version"); - if (strVersion != null) { - if (strVersion.equals("2.0")) { - feed.setType(Feed.TYPE_RSS2); - Log.d(TAG, "Recognized type RSS 2.0"); - return Type.RSS20; - } else if (strVersion.equals("0.91") - || strVersion.equals("0.92")) { - Log.d(TAG, "Recognized type RSS 0.91/0.92"); - return Type.RSS091; - } + if (strVersion == null) { + feed.setType(Feed.TYPE_RSS2); + Log.d(TAG, "Assuming type RSS 2.0"); + return Type.RSS20; + } else if (strVersion.equals("2.0")) { + feed.setType(Feed.TYPE_RSS2); + Log.d(TAG, "Recognized type RSS 2.0"); + return Type.RSS20; + } else if (strVersion.equals("0.91") || strVersion.equals("0.92")) { + Log.d(TAG, "Recognized type RSS 0.91/0.92"); + return Type.RSS091; } - throw new UnsupportedFeedtypeException(Type.INVALID); + throw new UnsupportedFeedtypeException("Unsupported rss version"); default: Log.d(TAG, "Type is invalid"); throw new UnsupportedFeedtypeException(Type.INVALID, tag); diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/UnsupportedFeedtypeException.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/UnsupportedFeedtypeException.java index 3da9251d9..fd7d0a4e1 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/UnsupportedFeedtypeException.java +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/UnsupportedFeedtypeException.java @@ -4,8 +4,9 @@ import de.danoeh.antennapod.core.syndication.handler.TypeGetter.Type; public class UnsupportedFeedtypeException extends Exception { private static final long serialVersionUID = 9105878964928170669L; - private TypeGetter.Type type; - private String rootElement; + private final TypeGetter.Type type; + private String rootElement; + private String message = null; public UnsupportedFeedtypeException(Type type) { super(); @@ -17,6 +18,11 @@ public class UnsupportedFeedtypeException extends Exception { this.rootElement = rootElement; } + public UnsupportedFeedtypeException(String message) { + this.message = message; + type = Type.INVALID; + } + public TypeGetter.Type getType() { return type; } @@ -27,7 +33,9 @@ public class UnsupportedFeedtypeException extends Exception { @Override public String getMessage() { - if (type == TypeGetter.Type.INVALID) { + if (message != null) { + return message; + } else if (type == TypeGetter.Type.INVALID) { return "Invalid type"; } else { return "Type " + type + " not supported"; diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSITunes.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSITunes.java index 7d60566b2..b3b8a40ce 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSITunes.java +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSITunes.java @@ -7,7 +7,6 @@ import org.xml.sax.Attributes; import java.util.concurrent.TimeUnit; -import de.danoeh.antennapod.core.feed.FeedImage; import de.danoeh.antennapod.core.syndication.handler.HandlerState; public class NSITunes extends Namespace { @@ -16,34 +15,27 @@ public class NSITunes extends Namespace { public static final String NSURI = "http://www.itunes.com/dtds/podcast-1.0.dtd"; private static final String IMAGE = "image"; - private static final String IMAGE_TITLE = "image"; private static final String IMAGE_HREF = "href"; private static final String AUTHOR = "author"; public static final String DURATION = "duration"; - public static final String SUBTITLE = "subtitle"; - public static final String SUMMARY = "summary"; + private static final String SUBTITLE = "subtitle"; + private static final String SUMMARY = "summary"; @Override public SyndElement handleElementStart(String localName, HandlerState state, Attributes attributes) { if (IMAGE.equals(localName)) { - FeedImage image = new FeedImage(); - image.setTitle(IMAGE_TITLE); - image.setDownload_url(attributes.getValue(IMAGE_HREF)); + String url = attributes.getValue(IMAGE_HREF); if (state.getCurrentItem() != null) { - // this is an items image - image.setTitle(state.getCurrentItem().getTitle() + IMAGE_TITLE); - image.setOwner(state.getCurrentItem()); - state.getCurrentItem().setImage(image); + state.getCurrentItem().setImageUrl(url); } else { // this is the feed image // prefer to all other images - if (!TextUtils.isEmpty(image.getDownload_url())) { - image.setOwner(state.getFeed()); - state.getFeed().setImage(image); + if (!TextUtils.isEmpty(url)) { + state.getFeed().setImageUrl(url); } } } @@ -55,6 +47,9 @@ public class NSITunes extends Namespace { if(state.getContentBuf() == null) { return; } + SyndElement secondElement = state.getSecondTag(); + String second = secondElement.getName(); + if (AUTHOR.equals(localName)) { if (state.getFeed() != null) { String author = state.getContentBuf().toString(); @@ -103,10 +98,9 @@ public class NSITunes extends Namespace { } if (state.getCurrentItem() != null && (TextUtils.isEmpty(state.getCurrentItem().getDescription()) || - state.getCurrentItem().getDescription().length() * 1.25 < summary.length()) - ) { + state.getCurrentItem().getDescription().length() * 1.25 < summary.length())) { state.getCurrentItem().setDescription(summary); - } else if (state.getFeed() != null) { + } else if (NSRSS20.CHANNEL.equals(second) && state.getFeed() != null) { state.getFeed().setDescription(summary); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSMedia.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSMedia.java index f2cfc2e57..638383223 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSMedia.java +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSMedia.java @@ -7,7 +7,6 @@ import org.xml.sax.Attributes; import java.util.concurrent.TimeUnit; -import de.danoeh.antennapod.core.feed.FeedImage; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.syndication.handler.HandlerState; import de.danoeh.antennapod.core.syndication.namespace.atom.AtomText; @@ -94,25 +93,16 @@ public class NSMedia extends Namespace { } state.getCurrentItem().setMedia(media); } else if (state.getCurrentItem() != null && url != null && validTypeImage) { - FeedImage image = new FeedImage(); - image.setDownload_url(url); - image.setOwner(state.getCurrentItem()); - - state.getCurrentItem().setImage(image); + state.getCurrentItem().setImageUrl(url); } } else if (IMAGE.equals(localName)) { String url = attributes.getValue(IMAGE_URL); if (url != null) { - FeedImage image = new FeedImage(); - image.setDownload_url(url); - if (state.getCurrentItem() != null) { - image.setOwner(state.getCurrentItem()); - state.getCurrentItem().setImage(image); + state.getCurrentItem().setImageUrl(url); } else { - if (state.getFeed().getImage() == null) { - image.setOwner(state.getFeed()); - state.getFeed().setImage(image); + if (state.getFeed().getImageUrl() == null) { + state.getFeed().setImageUrl(url); } } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSRSS20.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSRSS20.java index 3d752df76..a1100a976 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 @@ -6,7 +6,6 @@ import android.util.Log; import org.xml.sax.Attributes; import de.danoeh.antennapod.core.feed.Feed; -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; @@ -77,17 +76,6 @@ public class NSRSS20 extends Namespace { state.getCurrentItem().setMedia(media); } - } else if (IMAGE.equals(localName)) { - if (state.getTagstack().size() >= 1) { - String parent = state.getTagstack().peek().getName(); - if (CHANNEL.equals(parent)) { - Feed feed = state.getFeed(); - if(feed != null && feed.getImage() == null) { - feed.setImage(new FeedImage()); - feed.getImage().setOwner(state.getFeed()); - } - } - } } return new SyndElement(localName, this); } @@ -134,11 +122,6 @@ public class NSRSS20 extends Namespace { state.getCurrentItem().setTitle(title); } else if (CHANNEL.equals(second) && state.getFeed() != null) { state.getFeed().setTitle(title); - } else if (IMAGE.equals(second) && CHANNEL.equals(third)) { - if(state.getFeed() != null && state.getFeed().getImage() != null && - state.getFeed().getImage().getTitle() == null) { - state.getFeed().getImage().setTitle(title); - } } } else if (LINK.equals(top)) { if (CHANNEL.equals(second) && state.getFeed() != null) { @@ -150,9 +133,8 @@ public class NSRSS20 extends Namespace { state.getCurrentItem().setPubDate(DateUtils.parse(content)); } else if (URL.equals(top) && IMAGE.equals(second) && CHANNEL.equals(third)) { // prefer itunes:image - if(state.getFeed() != null && state.getFeed().getImage() != null && - state.getFeed().getImage().getDownload_url() == null) { - state.getFeed().getImage().setDownload_url(content); + if (state.getFeed() != null) { + state.getFeed().setImageUrl(content); } } else if (DESCR.equals(localName)) { if (CHANNEL.equals(second) && state.getFeed() != null) { 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 703817a35..45266569a 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 @@ -17,11 +17,11 @@ public class NSSimpleChapters extends Namespace { public static final String NSTAG = "psc|sc"; public static final String NSURI = "http://podlove.org/simple-chapters"; - public static final String CHAPTERS = "chapters"; - public static final String CHAPTER = "chapter"; - public static final String START = "start"; - public static final String TITLE = "title"; - public static final String HREF = "href"; + private static final String CHAPTERS = "chapters"; + private static final String CHAPTER = "chapter"; + private static final String START = "start"; + private static final String TITLE = "title"; + private static final String HREF = "href"; @Override public SyndElement handleElementStart(String localName, HandlerState state, diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/Namespace.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/Namespace.java index cf118d202..1836bbec1 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/Namespace.java +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/Namespace.java @@ -1,8 +1,9 @@ package de.danoeh.antennapod.core.syndication.namespace; -import de.danoeh.antennapod.core.syndication.handler.HandlerState; import org.xml.sax.Attributes; +import de.danoeh.antennapod.core.syndication.handler.HandlerState; + public abstract class Namespace { public static final String NSTAG = null; diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/SyndElement.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/SyndElement.java index 8adcd2086..ba1b8ba5c 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/SyndElement.java +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/SyndElement.java @@ -2,8 +2,8 @@ package de.danoeh.antennapod.core.syndication.namespace; /** Defines a XML Element that is pushed on the tagstack */ public class SyndElement { - protected String name; - protected Namespace namespace; + private final String name; + private final Namespace namespace; public SyndElement(String name, Namespace namespace) { this.name = name; diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/AtomText.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/AtomText.java index 43fe0edb7..b512dce3f 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/AtomText.java +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/AtomText.java @@ -1,16 +1,17 @@ package de.danoeh.antennapod.core.syndication.namespace.atom; +import org.apache.commons.text.StringEscapeUtils; + import de.danoeh.antennapod.core.syndication.namespace.Namespace; import de.danoeh.antennapod.core.syndication.namespace.SyndElement; -import org.apache.commons.lang3.StringEscapeUtils; /** Represents Atom Element which contains text (content, title, summary). */ public class AtomText extends SyndElement { public static final String TYPE_TEXT = "text"; - public static final String TYPE_HTML = "html"; - public static final String TYPE_XHTML = "xhtml"; + private static final String TYPE_HTML = "html"; + private static final String TYPE_XHTML = "xhtml"; - private String type; + private final String type; private String content; public AtomText(String name, Namespace namespace, String type) { 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 cfb20d578..aab1b1a5b 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 @@ -5,7 +5,6 @@ import android.util.Log; import org.xml.sax.Attributes; -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; @@ -64,8 +63,8 @@ public class NSAtom extends Namespace { private static final String isText = TITLE + "|" + CONTENT + "|" + SUBTITLE + "|" + SUMMARY; - public static final String isFeed = FEED + "|" + NSRSS20.CHANNEL; - public static final String isFeedItem = ENTRY + "|" + NSRSS20.ITEM; + private static final String isFeed = FEED + "|" + NSRSS20.CHANNEL; + private static final String isFeedItem = ENTRY + "|" + NSRSS20.ITEM; @Override public SyndElement handleElementStart(String localName, HandlerState state, @@ -210,10 +209,10 @@ public class NSAtom extends Namespace { state.getCurrentItem().setPubDate(DateUtils.parse(content)); } else if (PUBLISHED.equals(top) && ENTRY.equals(second) && state.getCurrentItem() != null) { state.getCurrentItem().setPubDate(DateUtils.parse(content)); - } else if (IMAGE_LOGO.equals(top) && state.getFeed() != null && state.getFeed().getImage() == null) { - state.getFeed().setImage(new FeedImage(state.getFeed(), content, null)); + } else if (IMAGE_LOGO.equals(top) && state.getFeed() != null && state.getFeed().getImageUrl() == null) { + state.getFeed().setImageUrl(content); } else if (IMAGE_ICON.equals(top) && state.getFeed() != null) { - state.getFeed().setImage(new FeedImage(state.getFeed(), content, null)); + state.getFeed().setImageUrl(content); } else if (AUTHOR_NAME.equals(top) && AUTHOR.equals(second) && state.getFeed() != null && state.getCurrentItem() == null) { String currentName = state.getFeed().getAuthor(); diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/Converter.java b/core/src/main/java/de/danoeh/antennapod/core/util/Converter.java index 70a180913..b513fbe99 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/Converter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/Converter.java @@ -71,8 +71,8 @@ public final class Converter { int m = rest / MINUTES_MIL; rest -= m * MINUTES_MIL; int s = rest / SECONDS_MIL; - - return String.format("%02d:%02d:%02d", h, m, s); + + return String.format(Locale.getDefault(), "%02d:%02d:%02d", h, m, s); } /** Converts milliseconds to a string containing hours and minutes */ @@ -81,7 +81,7 @@ public final class Converter { int rest = duration - h * HOURS_MIL; int m = rest / MINUTES_MIL; - return String.format("%02d:%02d", h, m); + return String.format(Locale.getDefault(), "%02d:%02d", h, m); } /** Converts long duration string (HH:MM:SS) to milliseconds. */ diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/DateUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/DateUtils.java index 783293a3e..101992e8c 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/DateUtils.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/DateUtils.java @@ -87,7 +87,8 @@ public class DateUtils { "yyyy-MM-dd'T'HH:mm:ss'Z'", "yyyy-MM-dd'T'HH:mm:ss.SSSZ", "yyyy-MM-ddZ", - "yyyy-MM-dd" + "yyyy-MM-dd", + "EEE d MMM yyyy HH:mm:ss 'GMT'Z (z)" }; SimpleDateFormat parser = new SimpleDateFormat("", Locale.US); diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/DownloadError.java b/core/src/main/java/de/danoeh/antennapod/core/util/DownloadError.java index 7779158e5..0c9989b43 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/DownloadError.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/DownloadError.java @@ -1,6 +1,7 @@ package de.danoeh.antennapod.core.util; import android.content.Context; + import de.danoeh.antennapod.core.R; /** Utility class for Download Errors. */ diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/DuckType.java b/core/src/main/java/de/danoeh/antennapod/core/util/DuckType.java index f432424f8..69dc38895 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/DuckType.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/DuckType.java @@ -92,7 +92,7 @@ public class DuckType { * false otherwise. */ @SuppressWarnings("rawtypes") - public boolean quacksLikeA(Class iface) { + private boolean quacksLikeA(Class iface) { for (Method method : iface.getMethods()) { if (findMethodBySignature(method) == null) { return false; 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 deleted file mode 100644 index 89edd7dbe..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/util/EpisodeFilter.java +++ /dev/null @@ -1,50 +0,0 @@ -package de.danoeh.antennapod.core.util; - -import java.util.ArrayList; -import java.util.List; - -import de.danoeh.antennapod.core.feed.FeedItem; - -public class EpisodeFilter { - - private EpisodeFilter() { - - } - - /** Return a copy of the itemlist without items which have no media. */ - public static ArrayList<FeedItem> getEpisodeList(List<FeedItem> items) { - ArrayList<FeedItem> episodes = new ArrayList<>(items); - for (FeedItem item : items) { - if (item.getMedia() == null) { - episodes.remove(item); - } - } - return episodes; - } - - public static int countItemsWithEpisodes(List<FeedItem> items) { - int count = 0; - for (FeedItem item : items) { - if (item.getMedia() != null) { - count++; - } - } - return count; - } - - public static FeedItem accessEpisodeByIndex(List<FeedItem> items, - int position) { - int count = 0; - for (FeedItem item : items) { - - if (item.getMedia() != null) { - if (count == position) { - return item; - } else { - count++; - } - } - } - return null; - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/FeedItemUtil.java b/core/src/main/java/de/danoeh/antennapod/core/util/FeedItemUtil.java index d0f782fca..826c06822 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/FeedItemUtil.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/FeedItemUtil.java @@ -76,4 +76,18 @@ public class FeedItemUtil { return false; } + /** + * Get the link for the feed item for the purpose of Share. It fallbacks to + * use the feed's link if the named feed item has no link. + */ + public static String getLinkWithFallback(FeedItem item) { + if (item == null) { + return null; + } else if (item.getLink() != null) { + return item.getLink(); + } else if (item.getFeed() != null) { + return item.getFeed().getLink(); + } + return null; + } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/FeedUpdateUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/FeedUpdateUtils.java new file mode 100644 index 000000000..afaf13390 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/FeedUpdateUtils.java @@ -0,0 +1,31 @@ +package de.danoeh.antennapod.core.util; + +import android.content.Context; +import android.util.Log; + +import org.awaitility.core.ConditionTimeoutException; + +import java.util.concurrent.TimeUnit; + +import de.danoeh.antennapod.core.storage.DBTasks; + +import static org.awaitility.Awaitility.with; + +public class FeedUpdateUtils { + private static final String TAG = "FeedUpdateUtils"; + + private FeedUpdateUtils() {} + + public static void startAutoUpdate(Context context, Runnable callback) { + try { + with().pollInterval(1, TimeUnit.SECONDS) + .await() + .atMost(10, TimeUnit.SECONDS) + .until(() -> NetworkUtils.networkAvailable() && NetworkUtils.isDownloadAllowed()); + DBTasks.refreshAllFeeds(context, null, callback); + } catch (ConditionTimeoutException ignore) { + Log.d(TAG, "Blocking automatic update: no wifi available / no mobile updates allowed"); + } + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/FeedtitleComparator.java b/core/src/main/java/de/danoeh/antennapod/core/util/FeedtitleComparator.java index bf14cd23e..29d095cd2 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/FeedtitleComparator.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/FeedtitleComparator.java @@ -1,11 +1,11 @@ package de.danoeh.antennapod.core.util; -import de.danoeh.antennapod.core.feed.Feed; - import java.util.Comparator; +import de.danoeh.antennapod.core.feed.Feed; + /** Compares the title of two feeds for sorting. */ -public class FeedtitleComparator implements Comparator<Feed> { +class FeedtitleComparator implements Comparator<Feed> { @Override public int compare(Feed lhs, Feed rhs) { diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/FileNameGenerator.java b/core/src/main/java/de/danoeh/antennapod/core/util/FileNameGenerator.java index a93dd8ee3..e99461806 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/FileNameGenerator.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/FileNameGenerator.java @@ -3,7 +3,8 @@ package de.danoeh.antennapod.core.util; import android.text.TextUtils; import org.apache.commons.lang3.ArrayUtils; -import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.text.RandomStringGenerator; + /** Generates valid filenames for a given string. */ public class FileNameGenerator { @@ -34,7 +35,11 @@ public class FileNameGenerator { } String filename = buf.toString().trim(); if(TextUtils.isEmpty(filename)) { - return RandomStringUtils.randomAlphanumeric(8); + return new RandomStringGenerator.Builder() + .withinRange('0', 'z') + .filteredBy(Character::isLetterOrDigit) + .build() + .generate(8); } return filename; } 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 index f48b9169b..1da5417c1 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/IntList.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/IntList.java @@ -8,7 +8,7 @@ import java.util.Arrays; public final class IntList { private int[] values; - protected int size; + private int size; /** * Constructs an empty instance with a default initial capacity. @@ -22,7 +22,7 @@ public final class IntList { * * @param initialCapacity {@code >= 0;} initial capacity of the list */ - public IntList(int initialCapacity) { + private IntList(int initialCapacity) { if(initialCapacity < 0) { throw new IllegalArgumentException("initial capacity must be 0 or higher"); } @@ -200,7 +200,7 @@ public final class IntList { * @param value value to find * @return index of value or -1 */ - public int indexOf(int value) { + private int indexOf(int value) { for (int i = 0; i < size; i++) { if (values[i] == value) { return i; diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/IntentUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/IntentUtils.java index 9e35833da..e81ab47ed 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/IntentUtils.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/IntentUtils.java @@ -24,4 +24,8 @@ public class IntentUtils { return false; } + public static void sendLocalBroadcast(Context context, String action) { + context.sendBroadcast(new Intent(action).setPackage(context.getPackageName())); + } + } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/LangUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/LangUtils.java index 970210ec3..90e0b0981 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/LangUtils.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/LangUtils.java @@ -10,7 +10,7 @@ public class LangUtils { public static final Charset UTF_8 = Charset.forName("UTF-8"); - private static ArrayMap<String, String> languages; + private static final ArrayMap<String, String> languages; static { languages = new ArrayMap<>(); languages.put("af", "Afrikaans"); 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 index c5ac44bf5..78ed002ac 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/LongIntMap.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/LongIntMap.java @@ -88,7 +88,7 @@ public class LongIntMap { /** * Removes the mapping at the given index. */ - public void removeAt(int index) { + private 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--; @@ -130,7 +130,7 @@ public class LongIntMap { * smallest key and <code>keyAt(size()-1)</code> will return the largest * key.</p> */ - public long keyAt(int index) { + private long keyAt(int index) { if (index >= size) { throw new IndexOutOfBoundsException("n >= size()"); } else if(index < 0) { @@ -150,7 +150,7 @@ public class LongIntMap { * smallest key and <code>valueAt(size()-1)</code> will return the value * associated with the largest key.</p> */ - public int valueAt(int index) { + private int valueAt(int index) { if (index >= size) { throw new IndexOutOfBoundsException("n >= size()"); } else if(index < 0) { 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 34ad8b8a2..49709bb53 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java @@ -8,6 +8,7 @@ import android.net.wifi.WifiManager; import android.support.v4.net.ConnectivityManagerCompat; import android.text.TextUtils; import android.util.Log; + import java.io.File; import java.io.IOException; import java.util.Arrays; @@ -54,7 +55,7 @@ public class NetworkUtils { Log.d(TAG, "Auto-dl filter is disabled"); return true; } else { - WifiManager wm = (WifiManager) context + WifiManager wm = (WifiManager) context.getApplicationContext() .getSystemService(Context.WIFI_SERVICE); WifiInfo wifiInfo = wm.getConnectionInfo(); List<String> selectedNetworks = Arrays @@ -93,7 +94,7 @@ public class NetworkUtils { return UserPreferences.isAllowMobileUpdate() || !NetworkUtils.isNetworkMetered(); } - public static boolean isNetworkMetered() { + private static boolean isNetworkMetered() { ConnectivityManager connManager = (ConnectivityManager) context .getSystemService(Context.CONNECTIVITY_SERVICE); return ConnectivityManagerCompat.isActiveNetworkMetered(connManager); @@ -103,7 +104,7 @@ public class NetworkUtils { * Returns the SSID of the wifi connection, or <code>null</code> if there is no wifi. */ public static String getWifiSsid() { - WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); + WifiManager wifiManager = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE); WifiInfo wifiInfo = wifiManager.getConnectionInfo(); if (wifiInfo != null) { return wifiInfo.getSSID(); @@ -112,63 +113,60 @@ public class NetworkUtils { } public static Observable<Long> getFeedMediaSizeObservable(FeedMedia media) { - return Observable.create(new Observable.OnSubscribe<Long>() { - @Override - public void call(Subscriber<? super Long> subscriber) { - if (!NetworkUtils.isDownloadAllowed()) { + return Observable.create((Observable.OnSubscribe<Long>) subscriber -> { + if (!NetworkUtils.isDownloadAllowed()) { + subscriber.onNext(0L); + subscriber.onCompleted(); + return; + } + long size = Integer.MIN_VALUE; + if (media.isDownloaded()) { + File mediaFile = new File(media.getLocalMediaUrl()); + if (mediaFile.exists()) { + size = mediaFile.length(); + } + } else if (!media.checkedOnSizeButUnknown()) { + // only query the network if we haven't already checked + + String url = media.getDownload_url(); + if(TextUtils.isEmpty(url)) { subscriber.onNext(0L); subscriber.onCompleted(); return; } - long size = Integer.MIN_VALUE; - if (media.isDownloaded()) { - File mediaFile = new File(media.getLocalMediaUrl()); - if (mediaFile.exists()) { - size = mediaFile.length(); - } - } else if (!media.checkedOnSizeButUnknown()) { - // only query the network if we haven't already checked - - String url = media.getDownload_url(); - if(TextUtils.isEmpty(url)) { - subscriber.onNext(0L); - subscriber.onCompleted(); - return; - } - OkHttpClient client = AntennapodHttpClient.getHttpClient(); - Request.Builder httpReq = new Request.Builder() - .url(url) - .header("Accept-Encoding", "identity") - .head(); - try { - Response response = client.newCall(httpReq.build()).execute(); - if (response.isSuccessful()) { - String contentLength = response.header("Content-Length"); - try { - size = Integer.parseInt(contentLength); - } catch (NumberFormatException e) { - Log.e(TAG, Log.getStackTraceString(e)); - } + OkHttpClient client = AntennapodHttpClient.getHttpClient(); + Request.Builder httpReq = new Request.Builder() + .url(url) + .header("Accept-Encoding", "identity") + .head(); + try { + Response response = client.newCall(httpReq.build()).execute(); + if (response.isSuccessful()) { + String contentLength = response.header("Content-Length"); + try { + size = Integer.parseInt(contentLength); + } catch (NumberFormatException e) { + Log.e(TAG, Log.getStackTraceString(e)); } - } catch (IOException e) { - subscriber.onNext(0L); - subscriber.onCompleted(); - Log.e(TAG, Log.getStackTraceString(e)); - return; // better luck next time } + } catch (IOException e) { + subscriber.onNext(0L); + subscriber.onCompleted(); + Log.e(TAG, Log.getStackTraceString(e)); + return; // better luck next time } - Log.d(TAG, "new size: " + size); - if (size <= 0) { - // they didn't tell us the size, but we don't want to keep querying on it - media.setCheckedOnSizeButUnknown(); - } else { - media.setSize(size); - } - subscriber.onNext(size); - subscriber.onCompleted(); - DBWriter.setFeedMedia(media); } + Log.d(TAG, "new size: " + size); + if (size <= 0) { + // they didn't tell us the size, but we don't want to keep querying on it + media.setCheckedOnSizeButUnknown(); + } else { + media.setSize(size); + } + subscriber.onNext(size); + subscriber.onCompleted(); + DBWriter.setFeedMedia(media); }) .subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()); diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/Permutor.java b/core/src/main/java/de/danoeh/antennapod/core/util/Permutor.java new file mode 100644 index 000000000..7d6e20ab1 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/Permutor.java @@ -0,0 +1,17 @@ +package de.danoeh.antennapod.core.util; + +import java.util.List; + +/** + * Interface for passing around list permutor method. This is used for cases where a simple comparator + * won't work (e.g. Random, Smart Shuffle, etc). + * + * @param <E> the type of elements in the list + */ +public interface Permutor<E> { + /** + * Reorders the specified list. + * @param queue A (modifiable) list of elements to be reordered + */ + void reorder(List<E> queue); +} 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 7377b202d..9408be348 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 @@ -58,19 +58,4 @@ public abstract class QueueAccess { }; } - public static QueueAccess NotInQueueAccess() { - return new QueueAccess() { - @Override - public boolean contains(long id) { - return false; - } - - @Override - public boolean remove(long id) { - return false; - } - }; - - } - } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/QueueSorter.java b/core/src/main/java/de/danoeh/antennapod/core/util/QueueSorter.java index c3b4c0e15..5c827dfe9 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/QueueSorter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/QueueSorter.java @@ -2,7 +2,12 @@ package de.danoeh.antennapod.core.util; import android.content.Context; +import java.util.ArrayList; +import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; @@ -20,11 +25,15 @@ public class QueueSorter { DURATION_ASC, DURATION_DESC, FEED_TITLE_ASC, - FEED_TITLE_DESC + FEED_TITLE_DESC, + RANDOM, + SMART_SHUFFLE_ASC, + SMART_SHUFFLE_DESC } public static void sort(final Context context, final Rule rule, final boolean broadcastUpdate) { Comparator<FeedItem> comparator = null; + Permutor<FeedItem> permutor = null; switch (rule) { case EPISODE_TITLE_ASC: @@ -68,11 +77,109 @@ public class QueueSorter { case FEED_TITLE_DESC: comparator = (f1, f2) -> f2.getFeed().getTitle().compareTo(f1.getFeed().getTitle()); break; + case RANDOM: + permutor = Collections::shuffle; + break; + case SMART_SHUFFLE_ASC: + permutor = (queue) -> smartShuffle(queue, true); + break; + case SMART_SHUFFLE_DESC: + permutor = (queue) -> smartShuffle(queue, false); + break; default: } if (comparator != null) { DBWriter.sortQueue(comparator, broadcastUpdate); + } else if (permutor != null) { + DBWriter.reorderQueue(permutor, broadcastUpdate); + } + } + + /** + * Implements a reordering by pubdate that avoids consecutive episodes from the same feed in + * the queue. + * + * A listener might want to hear episodes from any given feed in pubdate order, but would + * prefer a more balanced ordering that avoids having to listen to clusters of consecutive + * episodes from the same feed. This is what "Smart Shuffle" tries to accomplish. + * + * The Smart Shuffle algorithm involves choosing episodes (in round-robin fashion) from a + * collection of individual, pubdate-sorted lists that each contain only items from a specific + * feed. + * + * Of course, clusters of consecutive episodes <i>at the end of the queue</i> may be + * unavoidable. This seems unlikely to be an issue for most users who presumably maintain + * large queues with new episodes continuously being added. + * + * For example, given a queue containing three episodes each from three different feeds + * (A, B, and C), a simple pubdate sort might result in a queue that looks like the following: + * + * B1, B2, B3, A1, A2, C1, C2, C3, A3 + * + * (note that feed B episodes were all published before the first feed A episode, so a simple + * pubdate sort will often result in significant clustering of episodes from a single feed) + * + * Using Smart Shuffle, the resulting queue would look like the following: + * + * A1, B1, C1, A2, B2, C2, A3, B3, C3 + * + * (note that episodes above <i>aren't strictly ordered in terms of pubdate</i>, but episodes + * of each feed <b>do</b> appear in pubdate order) + * + * @param queue A (modifiable) list of FeedItem elements to be reordered. + * @param ascending {@code true} to use ascending pubdate in the reordering; + * {@code false} for descending. + */ + private static void smartShuffle(List<FeedItem> queue, boolean ascending) { + + // Divide FeedItems into lists by feed + + Map<Long, List<FeedItem>> map = new HashMap<>(); + + while (!queue.isEmpty()) { + FeedItem item = queue.remove(0); + Long id = item.getFeedId(); + if (!map.containsKey(id)) { + map.put(id, new ArrayList<>()); + } + map.get(id).add(item); + } + + // Sort each individual list by PubDate (ascending/descending) + + Comparator<FeedItem> itemComparator = ascending + ? (f1, f2) -> f1.getPubDate().compareTo(f2.getPubDate()) + : (f1, f2) -> f2.getPubDate().compareTo(f1.getPubDate()); + + for (Long id : map.keySet()) { + Collections.sort(map.get(id), itemComparator); + } + + // Create a list of the individual FeedItems lists, and sort it by feed title (ascending). + // Doing this ensures that the feed order we use is predictable/deterministic. + + List<List<FeedItem>> feeds = new ArrayList<>(map.values()); + Collections.sort(feeds, + // (we use a desc sort here, since we're iterating back-to-front below) + (f1, f2) -> f2.get(0).getFeed().getTitle().compareTo(f1.get(0).getFeed().getTitle())); + + // Cycle through the (sorted) feed lists in a round-robin fashion, removing the first item + // and adding it back into to the original queue + + while (!feeds.isEmpty()) { + // Iterate across the (sorted) list of feeds, removing the first item in each, and + // appending it to the queue. Note that we're iterating back-to-front here, since we + // will be deleting feed lists as they become empty. + for (int i = feeds.size() - 1; i >= 0; --i) { + List<FeedItem> items = feeds.get(i); + queue.add(items.remove(0)); + // Removed the last item in this particular feed? Then remove this feed from the + // list of feeds. + if (items.isEmpty()) { + feeds.remove(i); + } + } } } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/ShareUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/ShareUtils.java index 001bd6a2c..5ae00460e 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/ShareUtils.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/ShareUtils.java @@ -2,7 +2,6 @@ package de.danoeh.antennapod.core.util; import android.content.Context; import android.content.Intent; - import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.net.Uri; @@ -10,14 +9,14 @@ import android.os.Build; import android.support.v4.content.FileProvider; import android.util.Log; +import java.io.File; +import java.util.List; + 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 java.io.File; -import java.util.List; - /** Utility methods for sharing data */ public class ShareUtils { private static final String TAG = "ShareUtils"; @@ -51,11 +50,15 @@ public class ShareUtils { return item.getFeed().getTitle() + ": " + item.getTitle(); } + public static boolean hasLinkToShare(FeedItem item) { + return FeedItemUtil.getLinkWithFallback(item) != null; + } + public static void shareFeedItemLink(Context context, FeedItem item, boolean withPosition) { - String text = getItemShareText(item) + " " + item.getLink(); + String text = getItemShareText(item) + " " + FeedItemUtil.getLinkWithFallback(item); if(withPosition) { int pos = item.getMedia().getPosition(); - text = item.getLink() + " [" + Converter.getDurationStringLong(pos) + "]"; + text += " [" + Converter.getDurationStringLong(pos) + "]"; } shareLink(context, text); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/ThemeUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/ThemeUtils.java index 1d5fb2645..031eaed49 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/ThemeUtils.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/ThemeUtils.java @@ -1,7 +1,12 @@ package de.danoeh.antennapod.core.util; +import android.content.Context; +import android.support.annotation.AttrRes; +import android.support.annotation.ColorInt; +import android.support.annotation.ColorRes; import android.util.Log; +import android.util.TypedValue; import de.danoeh.antennapod.core.R; import de.danoeh.antennapod.core.preferences.UserPreferences; @@ -14,6 +19,8 @@ public class ThemeUtils { int theme = UserPreferences.getTheme(); if (theme == R.style.Theme_AntennaPod_Dark) { return R.color.selection_background_color_dark; + } else if (theme == R.style.Theme_AntennaPod_TrueBlack){ + return R.color.selection_background_color_trueblack; } else if (theme == R.style.Theme_AntennaPod_Light) { return R.color.selection_background_color_light; } else { @@ -22,4 +29,10 @@ public class ThemeUtils { return R.color.selection_background_color_light; } } + + public static @ColorInt int getColorFromAttr(Context context, @AttrRes int attr) { + TypedValue typedValue = new TypedValue(); + context.getTheme().resolveAttribute(attr, typedValue, true); + return typedValue.data; + } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/ChapterStartTimeComparator.java b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/ChapterStartTimeComparator.java index 5274ffc9e..8bd23c2ed 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/ChapterStartTimeComparator.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/ChapterStartTimeComparator.java @@ -1,9 +1,9 @@ package de.danoeh.antennapod.core.util.comparator; -import de.danoeh.antennapod.core.feed.Chapter; - import java.util.Comparator; +import de.danoeh.antennapod.core.feed.Chapter; + public class ChapterStartTimeComparator implements Comparator<Chapter> { @Override diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/DownloadStatusComparator.java b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/DownloadStatusComparator.java index ebdbfe2a5..868f3b835 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/DownloadStatusComparator.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/DownloadStatusComparator.java @@ -1,9 +1,9 @@ package de.danoeh.antennapod.core.util.comparator; -import de.danoeh.antennapod.core.service.download.DownloadStatus; - import java.util.Comparator; +import de.danoeh.antennapod.core.service.download.DownloadStatus; + /** Compares the completion date of two Downloadstatus objects. */ public class DownloadStatusComparator implements Comparator<DownloadStatus> { diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/FeedItemPubdateComparator.java b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/FeedItemPubdateComparator.java index a1f3ec699..a96eda3c5 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/FeedItemPubdateComparator.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/FeedItemPubdateComparator.java @@ -1,9 +1,9 @@ package de.danoeh.antennapod.core.util.comparator; -import de.danoeh.antennapod.core.feed.FeedItem; - import java.util.Comparator; +import de.danoeh.antennapod.core.feed.FeedItem; + /** Compares the pubDate of two FeedItems for sorting*/ public class FeedItemPubdateComparator implements Comparator<FeedItem> { diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/PlaybackCompletionDateComparator.java b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/PlaybackCompletionDateComparator.java index 84d244660..d65eb3e0b 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/PlaybackCompletionDateComparator.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/PlaybackCompletionDateComparator.java @@ -1,9 +1,9 @@ package de.danoeh.antennapod.core.util.comparator; -import de.danoeh.antennapod.core.feed.FeedItem; - import java.util.Comparator; +import de.danoeh.antennapod.core.feed.FeedItem; + public class PlaybackCompletionDateComparator implements Comparator<FeedItem> { public int compare(FeedItem lhs, FeedItem rhs) { diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/SearchResultValueComparator.java b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/SearchResultValueComparator.java index d23901a45..56a684475 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/SearchResultValueComparator.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/SearchResultValueComparator.java @@ -1,10 +1,10 @@ package de.danoeh.antennapod.core.util.comparator; +import java.util.Comparator; + import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.SearchResult; -import java.util.Comparator; - public class SearchResultValueComparator implements Comparator<SearchResult> { /** diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/download/AutoUpdateManager.java b/core/src/main/java/de/danoeh/antennapod/core/util/download/AutoUpdateManager.java new file mode 100644 index 000000000..ad723c685 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/download/AutoUpdateManager.java @@ -0,0 +1,155 @@ +package de.danoeh.antennapod.core.util.download; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.app.job.JobInfo; +import android.app.job.JobScheduler; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.os.SystemClock; +import android.support.annotation.RequiresApi; +import android.util.Log; +import de.danoeh.antennapod.core.receiver.FeedUpdateReceiver; +import de.danoeh.antennapod.core.service.FeedUpdateJobService; + +import java.util.Calendar; +import java.util.concurrent.TimeUnit; + +public class AutoUpdateManager { + private static final int JOB_ID_FEED_UPDATE = 42; + private static final String TAG = "AutoUpdateManager"; + + private AutoUpdateManager() { + + } + + /** + * Sets the interval in which the feeds are refreshed automatically + */ + public static void restartUpdateIntervalAlarm(Context context, long triggerAtMillis, long intervalMillis) { + Log.d(TAG, "Restarting update alarm."); + + if (Build.VERSION.SDK_INT >= 24) { + restartJobServiceInterval(context, intervalMillis); + } else { + restartAlarmManagerInterval(context, triggerAtMillis, intervalMillis); + } + } + + /** + * Sets time of day the feeds are refreshed automatically + */ + public static void restartUpdateTimeOfDayAlarm(Context context, int hoursOfDay, int minute) { + Log.d(TAG, "Restarting update alarm."); + + Calendar now = Calendar.getInstance(); + Calendar alarm = (Calendar)now.clone(); + alarm.set(Calendar.HOUR_OF_DAY, hoursOfDay); + alarm.set(Calendar.MINUTE, minute); + if (alarm.before(now) || alarm.equals(now)) { + alarm.add(Calendar.DATE, 1); + } + + if (Build.VERSION.SDK_INT >= 24) { + long triggerAtMillis = alarm.getTimeInMillis() - now.getTimeInMillis(); + restartJobServiceTriggerAt(context, triggerAtMillis); + } else { + restartAlarmManagerTimeOfDay(context, alarm); + } + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + private static JobInfo.Builder getFeedUpdateJobBuilder(Context context) { + ComponentName serviceComponent = new ComponentName(context, FeedUpdateJobService.class); + JobInfo.Builder builder = new JobInfo.Builder(JOB_ID_FEED_UPDATE, serviceComponent); + builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY); + builder.setPersisted(true); + return builder; + } + + @RequiresApi(api = Build.VERSION_CODES.N) + private static void restartJobServiceInterval(Context context, long intervalMillis) { + JobScheduler jobScheduler = context.getSystemService(JobScheduler.class); + if (jobScheduler == null) { + Log.d(TAG, "JobScheduler was null."); + return; + } + + JobInfo oldJob = jobScheduler.getPendingJob(JOB_ID_FEED_UPDATE); + if (oldJob != null && oldJob.getIntervalMillis() == intervalMillis) { + Log.d(TAG, "JobScheduler was already set at interval " + intervalMillis + ", ignoring."); + return; + } + + JobInfo.Builder builder = getFeedUpdateJobBuilder(context); + builder.setPeriodic(intervalMillis); + jobScheduler.cancel(JOB_ID_FEED_UPDATE); + + if (intervalMillis <= 0) { + Log.d(TAG, "Automatic update was deactivated"); + return; + } + + jobScheduler.schedule(builder.build()); + Log.d(TAG, "JobScheduler was set at interval " + intervalMillis); + } + + private static void restartAlarmManagerInterval(Context context, long triggerAtMillis, long intervalMillis) { + AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + + if (alarmManager == null) { + Log.d(TAG, "AlarmManager was null"); + return; + } + + PendingIntent updateIntent = PendingIntent.getBroadcast(context, 0, + new Intent(context, FeedUpdateReceiver.class), 0); + alarmManager.cancel(updateIntent); + + if (intervalMillis <= 0) { + Log.d(TAG, "Automatic update was deactivated"); + return; + } + + alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, + SystemClock.elapsedRealtime() + triggerAtMillis, + updateIntent); + Log.d(TAG, "Changed alarm to new interval " + TimeUnit.MILLISECONDS.toHours(intervalMillis) + " h"); + } + + @RequiresApi(api = Build.VERSION_CODES.N) + private static void restartJobServiceTriggerAt(Context context, long triggerAtMillis) { + JobScheduler jobScheduler = context.getSystemService(JobScheduler.class); + if (jobScheduler == null) { + Log.d(TAG, "JobScheduler was null."); + return; + } + + JobInfo.Builder builder = getFeedUpdateJobBuilder(context); + builder.setMinimumLatency(triggerAtMillis); + jobScheduler.cancel(JOB_ID_FEED_UPDATE); + jobScheduler.schedule(builder.build()); + Log.d(TAG, "JobScheduler was set for " + triggerAtMillis); + } + + private static void restartAlarmManagerTimeOfDay(Context context, Calendar alarm) { + AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + + if (alarmManager == null) { + Log.d(TAG, "AlarmManager was null"); + return; + } + + PendingIntent updateIntent = PendingIntent.getBroadcast(context, 0, + new Intent(context, FeedUpdateReceiver.class), 0); + alarmManager.cancel(updateIntent); + + Log.d(TAG, "Alarm set for: " + alarm.toString() + " : " + alarm.getTimeInMillis()); + alarmManager.set(AlarmManager.RTC_WAKEUP, + alarm.getTimeInMillis(), + updateIntent); + Log.d(TAG, "Changed alarm to new time of day " + alarm.get(Calendar.HOUR_OF_DAY) + ":" + alarm.get(Calendar.MINUTE)); + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/exception/MediaFileNotFoundException.java b/core/src/main/java/de/danoeh/antennapod/core/util/exception/MediaFileNotFoundException.java index 287fe1100..3000e2fa4 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/exception/MediaFileNotFoundException.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/exception/MediaFileNotFoundException.java @@ -5,18 +5,13 @@ import de.danoeh.antennapod.core.feed.FeedMedia; public class MediaFileNotFoundException extends Exception { private static final long serialVersionUID = 1L; - private FeedMedia media; + private final FeedMedia media; public MediaFileNotFoundException(String msg, FeedMedia media) { super(msg); this.media = media; } - public MediaFileNotFoundException(FeedMedia media) { - super(); - this.media = media; - } - public FeedMedia getMedia() { return media; } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrServiceCreator.java b/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrServiceCreator.java index 2103ae3b2..d4d5843d2 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrServiceCreator.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrServiceCreator.java @@ -10,7 +10,7 @@ import de.danoeh.antennapod.core.BuildConfig; /** Ensures that only one instance of the FlattrService class exists at a time */ -public class FlattrServiceCreator { +class FlattrServiceCreator { private FlattrServiceCreator(){} public static final String TAG = "FlattrServiceCreator"; diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrStatus.java b/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrStatus.java index d82171d1a..40a9fc7d5 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrStatus.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrStatus.java @@ -3,9 +3,9 @@ package de.danoeh.antennapod.core.util.flattr; import java.util.Calendar; public class FlattrStatus { - public static final int STATUS_UNFLATTERED = 0; + private static final int STATUS_UNFLATTERED = 0; public static final int STATUS_QUEUE = 1; - public static final int STATUS_FLATTRED = 2; + private static final int STATUS_FLATTRED = 2; private int status = STATUS_UNFLATTERED; private Calendar lastFlattred; @@ -38,7 +38,7 @@ public class FlattrStatus { status = STATUS_QUEUE; } - public void fromLong(long status) { + private void fromLong(long status) { if (status == STATUS_UNFLATTERED || status == STATUS_QUEUE) this.status = (int) status; else { diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrThing.java b/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrThing.java index 515028ab6..d5bb88771 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrThing.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrThing.java @@ -1,7 +1,7 @@ package de.danoeh.antennapod.core.util.flattr; public interface FlattrThing { - public String getTitle(); - public String getPaymentLink(); - public FlattrStatus getFlattrStatus(); + String getTitle(); + String getPaymentLink(); + FlattrStatus getFlattrStatus(); } 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 558485ce3..dfb5484cd 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 @@ -101,7 +101,7 @@ public class FlattrUtils { cachedToken = token; } - public static void deleteToken() { + private static void deleteToken() { Log.d(TAG, "Deleting flattr token"); storeToken(null); } @@ -174,17 +174,11 @@ public class FlattrUtils { // ------------------------------------------------ DIALOGS - public static void showRevokeDialog(final Context context) { + private static void showRevokeDialog(final Context context) { AlertDialog.Builder builder = new AlertDialog.Builder(context); builder.setTitle(R.string.access_revoked_title); builder.setMessage(R.string.access_revoked_info); - builder.setNeutralButton(android.R.string.ok, new OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - dialog.cancel(); - } - }); + builder.setNeutralButton(android.R.string.ok, (dialog, which) -> dialog.cancel()); builder.create().show(); } @@ -199,27 +193,15 @@ public class FlattrUtils { builder.setTitle(R.string.no_flattr_token_title); builder.setMessage(R.string.no_flattr_token_msg); builder.setPositiveButton(R.string.authenticate_now_label, - new OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - context.startActivity( - ClientConfig.flattrCallbacks.getFlattrAuthenticationActivityIntent(context)); - } - - } + (dialog, which) -> context.startActivity( + ClientConfig.flattrCallbacks.getFlattrAuthenticationActivityIntent(context)) ); builder.setNegativeButton(R.string.visit_website_label, - new OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - Uri uri = Uri.parse(url); - context.startActivity(new Intent(Intent.ACTION_VIEW, - uri)); - } - + (dialog, which) -> { + Uri uri = Uri.parse(url); + context.startActivity(new Intent(Intent.ACTION_VIEW, + uri)); } ); builder.create().show(); @@ -233,13 +215,7 @@ public class FlattrUtils { AlertDialog.Builder builder = new AlertDialog.Builder(context); builder.setTitle(R.string.error_label); builder.setMessage(msg); - builder.setNeutralButton(android.R.string.ok, new OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - dialog.cancel(); - } - }); + builder.setNeutralButton(android.R.string.ok, (dialog, which) -> dialog.cancel()); builder.create().show(); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/flattr/SimpleFlattrThing.java b/core/src/main/java/de/danoeh/antennapod/core/util/flattr/SimpleFlattrThing.java index 2c178496e..43cd5f170 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/flattr/SimpleFlattrThing.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/flattr/SimpleFlattrThing.java @@ -24,7 +24,7 @@ public class SimpleFlattrThing implements FlattrThing { return this.status; } - private String title; - private String url; - private FlattrStatus status; + private final String title; + private final String url; + private final FlattrStatus status; } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/gui/NotificationUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/gui/NotificationUtils.java new file mode 100644 index 000000000..2a537dc62 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/gui/NotificationUtils.java @@ -0,0 +1,64 @@ +package de.danoeh.antennapod.core.util.gui; + + +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.content.Context; +import android.os.Build; +import android.support.annotation.RequiresApi; +import de.danoeh.antennapod.core.R; + +public class NotificationUtils { + public static final String CHANNEL_ID_USER_ACTION = "user_action"; + public static final String CHANNEL_ID_DOWNLOADING = "downloading"; + public static final String CHANNEL_ID_PLAYING = "playing"; + public static final String CHANNEL_ID_ERROR = "error"; + + public static void createChannels(Context context) { + if (android.os.Build.VERSION.SDK_INT < 26) { + return; + } + NotificationManager mNotificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + + if (mNotificationManager != null) { + mNotificationManager.createNotificationChannel(createChannelUserAction(context)); + mNotificationManager.createNotificationChannel(createChannelDownloading(context)); + mNotificationManager.createNotificationChannel(createChannelPlaying(context)); + mNotificationManager.createNotificationChannel(createChannelError(context)); + } + } + + @RequiresApi(api = Build.VERSION_CODES.O) + private static NotificationChannel createChannelUserAction(Context c) { + NotificationChannel mChannel = new NotificationChannel(CHANNEL_ID_USER_ACTION, + c.getString(R.string.notification_channel_user_action), NotificationManager.IMPORTANCE_HIGH); + mChannel.setDescription(c.getString(R.string.notification_channel_user_action_description)); + return mChannel; + } + + @RequiresApi(api = Build.VERSION_CODES.O) + private static NotificationChannel createChannelDownloading(Context c) { + NotificationChannel mChannel = new NotificationChannel(CHANNEL_ID_DOWNLOADING, + c.getString(R.string.notification_channel_downloading), NotificationManager.IMPORTANCE_LOW); + mChannel.setDescription(c.getString(R.string.notification_channel_downloading_description)); + mChannel.setShowBadge(false); + return mChannel; + } + + @RequiresApi(api = Build.VERSION_CODES.O) + private static NotificationChannel createChannelPlaying(Context c) { + NotificationChannel mChannel = new NotificationChannel(CHANNEL_ID_PLAYING, + c.getString(R.string.notification_channel_playing), NotificationManager.IMPORTANCE_LOW); + mChannel.setDescription(c.getString(R.string.notification_channel_playing_description)); + mChannel.setShowBadge(false); + return mChannel; + } + + @RequiresApi(api = Build.VERSION_CODES.O) + private static NotificationChannel createChannelError(Context c) { + NotificationChannel mChannel = new NotificationChannel(CHANNEL_ID_ERROR, + c.getString(R.string.notification_channel_error), NotificationManager.IMPORTANCE_HIGH); + mChannel.setDescription(c.getString(R.string.notification_channel_error_description)); + return mChannel; + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/gui/PictureInPictureUtil.java b/core/src/main/java/de/danoeh/antennapod/core/util/gui/PictureInPictureUtil.java new file mode 100644 index 000000000..f763653a1 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/gui/PictureInPictureUtil.java @@ -0,0 +1,27 @@ +package de.danoeh.antennapod.core.util.gui; + +import android.app.Activity; +import android.content.pm.PackageManager; +import android.os.Build; + +public class PictureInPictureUtil { + private PictureInPictureUtil() { + } + + public static boolean supportsPictureInPicture(Activity activity) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + PackageManager packageManager = activity.getPackageManager(); + return packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE); + } else { + return false; + } + } + + public static boolean isInPictureInPictureMode(Activity activity) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && supportsPictureInPicture(activity)) { + return activity.isInPictureInPictureMode(); + } else { + return false; + } + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ChapterReader.java b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ChapterReader.java index 51fa5b4d6..f681b8062 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ChapterReader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ChapterReader.java @@ -8,7 +8,6 @@ import java.net.URLDecoder; import java.util.ArrayList; import java.util.List; -import de.danoeh.antennapod.core.BuildConfig; import de.danoeh.antennapod.core.feed.Chapter; import de.danoeh.antennapod.core.feed.ID3Chapter; import de.danoeh.antennapod.core.util.id3reader.model.FrameHeader; @@ -44,7 +43,7 @@ public class ChapterReader extends ID3Reader { currentChapter = null; } } - StringBuffer elementId = new StringBuffer(); + StringBuilder elementId = new StringBuilder(); readISOString(elementId, input, Integer.MAX_VALUE); char[] startTimeSource = readBytes(input, 4); long startTime = ((int) startTimeSource[0] << 24) @@ -55,7 +54,7 @@ public class ChapterReader extends ID3Reader { return ID3Reader.ACTION_DONT_SKIP; case FRAME_ID_TITLE: if (currentChapter != null && currentChapter.getTitle() == null) { - StringBuffer title = new StringBuffer(); + StringBuilder title = new StringBuilder(); readString(title, input, header.getSize()); currentChapter .setTitle(title.toString()); @@ -68,7 +67,7 @@ public class ChapterReader extends ID3Reader { if (currentChapter != null) { // skip description int descriptionLength = readString(null, input, header.getSize()); - StringBuffer link = new StringBuffer(); + StringBuilder link = new StringBuilder(); readISOString(link, input, header.getSize() - descriptionLength); String decodedLink = URLDecoder.decode(link.toString(), "UTF-8"); diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ID3Reader.java b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ID3Reader.java index a238c11e9..7290b9d98 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ID3Reader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ID3Reader.java @@ -1,7 +1,5 @@ package de.danoeh.antennapod.core.util.id3reader; -import de.danoeh.antennapod.core.util.id3reader.model.FrameHeader; -import de.danoeh.antennapod.core.util.id3reader.model.TagHeader; import org.apache.commons.io.IOUtils; import java.io.IOException; @@ -9,6 +7,9 @@ import java.io.InputStream; import java.nio.ByteBuffer; import java.nio.charset.Charset; +import de.danoeh.antennapod.core.util.id3reader.model.FrameHeader; +import de.danoeh.antennapod.core.util.id3reader.model.TagHeader; + /** * Reads the ID3 Tag of a given file. In order to use this class, you should * create a subclass of it and overwrite the onStart* - or onEnd* - methods. @@ -18,10 +19,10 @@ public class ID3Reader { private static final int ID3_LENGTH = 3; private static final int FRAME_ID_LENGTH = 4; - protected static final int ACTION_SKIP = 1; - protected static final int ACTION_DONT_SKIP = 2; + private static final int ACTION_SKIP = 1; + static final int ACTION_DONT_SKIP = 2; - protected int readerPosition; + private int readerPosition; private static final byte ENCODING_UTF16_WITH_BOM = 1; private static final byte ENCODING_UTF16_WITHOUT_BOM = 2; @@ -29,7 +30,7 @@ public class ID3Reader { private TagHeader tagHeader; - public ID3Reader() { + ID3Reader() { } public final void readInputStream(InputStream input) throws IOException, @@ -91,7 +92,7 @@ public class ID3Reader { * Read a certain number of bytes from the given input stream. This method * changes the readerPosition-attribute. */ - protected char[] readBytes(InputStream input, int number) + char[] readBytes(InputStream input, int number) throws IOException, ID3ReaderException { char[] header = new char[number]; for (int i = 0; i < number; i++) { @@ -110,7 +111,7 @@ public class ID3Reader { * Skip a certain number of bytes on the given input stream. This method * changes the readerPosition-attribute. */ - protected void skipBytes(InputStream input, int number) throws IOException { + void skipBytes(InputStream input, int number) throws IOException { if (number <= 0) { number = 1; } @@ -169,7 +170,7 @@ public class ID3Reader { return out; } - protected int readString(StringBuffer buffer, InputStream input, int max) throws IOException, + protected int readString(StringBuilder buffer, InputStream input, int max) throws IOException, ID3ReaderException { if (max > 0) { char[] encoding = readBytes(input, 1); @@ -190,9 +191,8 @@ public class ID3Reader { } } - protected int readISOString(StringBuffer buffer, InputStream input, int max) + protected int readISOString(StringBuilder buffer, InputStream input, int max) throws IOException, ID3ReaderException { - int bytesRead = 0; char c; while (++bytesRead <= max && (c = (char) input.read()) > 0) { @@ -203,7 +203,7 @@ public class ID3Reader { return bytesRead; } - private int readUnicodeString(StringBuffer strBuffer, InputStream input, int max, Charset charset) + private int readUnicodeString(StringBuilder strBuffer, InputStream input, int max, Charset charset) throws IOException, ID3ReaderException { byte[] buffer = new byte[max]; int c, cZero = -1; @@ -230,20 +230,20 @@ public class ID3Reader { return i; } - public int onStartTagHeader(TagHeader header) { + int onStartTagHeader(TagHeader header) { return ACTION_SKIP; } - public int onStartFrameHeader(FrameHeader header, InputStream input) + int onStartFrameHeader(FrameHeader header, InputStream input) throws IOException, ID3ReaderException { return ACTION_SKIP; } - public void onEndTag() { + void onEndTag() { } - public void onNoTagHeaderFound() { + void onNoTagHeaderFound() { } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/FrameHeader.java b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/FrameHeader.java index 89eab1398..2f3f378ab 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/FrameHeader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/FrameHeader.java @@ -2,7 +2,7 @@ package de.danoeh.antennapod.core.util.id3reader.model; public class FrameHeader extends Header { - protected char flags; + private final char flags; public FrameHeader(String id, int size, char flags) { super(id, size); diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/Header.java b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/Header.java index 346e2893f..29185748f 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/Header.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/Header.java @@ -2,10 +2,10 @@ package de.danoeh.antennapod.core.util.id3reader.model; public abstract class Header { - protected String id; - protected int size; + final String id; + final int size; - public Header(String id, int size) { + Header(String id, int size) { super(); this.id = id; this.size = size; diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/TagHeader.java b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/TagHeader.java index 0a6b8357f..b652a139c 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/TagHeader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/TagHeader.java @@ -2,8 +2,8 @@ package de.danoeh.antennapod.core.util.id3reader.model; public class TagHeader extends Header { - protected char version; - protected byte flags; + private final char version; + private final byte flags; public TagHeader(String id, int size, char version, byte flags) { super(id, size); diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/AudioPlayer.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/AudioPlayer.java index 846733882..16d05dbb9 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/AudioPlayer.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/AudioPlayer.java @@ -21,18 +21,12 @@ public class AudioPlayer extends MediaPlayer implements IPlayer { private final SharedPreferences.OnSharedPreferenceChangeListener sonicListener = (sharedPreferences, key) -> { - if (key.equals(UserPreferences.PREF_SONIC)) { + if (key.equals(UserPreferences.PREF_MEDIA_PLAYER)) { checkMpi(); } }; @Override - public void setScreenOnWhilePlaying(boolean screenOn) { - Log.e(TAG, "Setting screen on while playing not supported in Audio Player"); - throw new UnsupportedOperationException("Setting screen on while playing not supported in Audio Player"); - } - - @Override public void setDisplay(SurfaceHolder sh) { if (sh != null) { Log.e(TAG, "Setting display not supported in Audio Player"); @@ -40,11 +34,6 @@ public class AudioPlayer extends MediaPlayer implements IPlayer { } } - @Override - public void setVideoScalingMode(int mode) { - throw new UnsupportedOperationException("Setting scaling mode is not supported in Audio Player"); - } - @Override protected boolean useSonic() { return UserPreferences.useSonic(); diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java index c4acdb65e..645bae5f3 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java @@ -23,7 +23,7 @@ public class ExternalMedia implements Playable { public static final String PREF_MEDIA_TYPE = "ExternalMedia.PrefMediaType"; public static final String PREF_LAST_PLAYED_TIME = "ExternalMedia.PrefLastPlayedTime"; - private String source; + private final String source; private String episodeTitle; private String feedTitle; diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/IPlayer.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/IPlayer.java index d67153a4e..a372f4241 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/IPlayer.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/IPlayer.java @@ -6,13 +6,11 @@ import android.view.SurfaceHolder; import java.io.IOException; public interface IPlayer { - boolean canSetPitch(); boolean canSetSpeed(); boolean canDownmix(); - float getCurrentPitchStepsAdjustment(); int getCurrentPosition(); @@ -20,20 +18,12 @@ public interface IPlayer { int getDuration(); - float getMaxSpeedMultiplier(); - - float getMinSpeedMultiplier(); - - boolean isLooping(); - boolean isPlaying(); void pause(); void prepare() throws IllegalStateException, IOException; - void prepareAsync(); - void release(); void reset(); @@ -42,21 +32,11 @@ public interface IPlayer { void setAudioStreamType(int streamtype); - void setScreenOnWhilePlaying(boolean screenOn); - void setDataSource(String path) throws IllegalStateException, IOException, - IllegalArgumentException, SecurityException; + IllegalArgumentException, SecurityException; void setDisplay(SurfaceHolder sh); - void setEnableSpeedAdjustment(boolean enableSpeedAdjustment); - - void setLooping(boolean looping); - - void setPitchStepsAdjustment(float pitchSteps); - - void setPlaybackPitch(float f); - void setPlaybackSpeed(float f); void setDownmix(boolean enable); @@ -67,7 +47,5 @@ public interface IPlayer { void stop(); - public void setVideoScalingMode(int mode); - - public void setWakeMode(Context context, int mode); + void setWakeMode(Context context, int mode); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/MediaPlayerError.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/MediaPlayerError.java index 6417ec919..b04c02075 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/MediaPlayerError.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/MediaPlayerError.java @@ -2,6 +2,7 @@ package de.danoeh.antennapod.core.util.playback; import android.content.Context; import android.media.MediaPlayer; + import de.danoeh.antennapod.core.R; /** Utility class for MediaPlayer errors. */ diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java index cb0757522..da9b96430 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java @@ -3,6 +3,8 @@ package de.danoeh.antennapod.core.util.playback; import android.content.Context; import android.content.SharedPreferences; import android.os.Parcelable; +import android.preference.PreferenceManager; +import android.support.annotation.Nullable; import android.util.Log; import java.util.List; @@ -11,6 +13,7 @@ import de.danoeh.antennapod.core.asynctask.ImageResource; 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.storage.DBReader; import de.danoeh.antennapod.core.util.ShownotesProvider; @@ -181,6 +184,23 @@ public interface Playable extends Parcelable, * Restores a playable object from a sharedPreferences file. This method might load data from the database, * depending on the type of playable that was restored. * + * @return The restored Playable object + */ + @Nullable + public static Playable createInstanceFromPreferences(Context context) { + long currentlyPlayingMedia = PlaybackPreferences.getCurrentlyPlayingMedia(); + if (currentlyPlayingMedia != PlaybackPreferences.NO_MEDIA_PLAYING) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()); + return PlayableUtils.createInstanceFromPreferences(context, + (int) currentlyPlayingMedia, prefs); + } + return null; + } + + /** + * Restores a playable object from a sharedPreferences file. This method might load data from the database, + * depending on the type of playable that was restored. + * * @param type An integer that represents the type of the Playable object * that is restored. * @param pref The SharedPreferences file from which the Playable object 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 9d3854f41..31067839a 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 @@ -14,6 +14,8 @@ import android.os.Build; import android.os.IBinder; import android.preference.PreferenceManager; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.content.ContextCompat; import android.text.TextUtils; import android.util.Log; import android.util.Pair; @@ -40,9 +42,12 @@ import de.danoeh.antennapod.core.service.playback.PlayerStatus; import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.util.Converter; import de.danoeh.antennapod.core.util.playback.Playable.PlayableUtils; +import rx.Completable; import rx.Observable; +import rx.Single; import rx.Subscription; import rx.android.schedulers.AndroidSchedulers; +import rx.observers.Subscribers; import rx.schedulers.Schedulers; /** @@ -59,7 +64,7 @@ public abstract class PlaybackController { private PlaybackService playbackService; private Playable media; - private PlayerStatus status; + private PlayerStatus status = PlayerStatus.STOPPED; private final ScheduledThreadPoolExecutor schedExecutor; private static final int SCHED_EX_POOLSIZE = 1; @@ -69,6 +74,7 @@ public abstract class PlaybackController { private boolean mediaInfoLoaded = false; private boolean released = false; + private boolean initialized = false; private Subscription serviceBinder; @@ -87,21 +93,27 @@ public abstract class PlaybackController { Thread t = new Thread(r); t.setPriority(Thread.MIN_PRIORITY); return t; - }, new RejectedExecutionHandler() { - @Override - public void rejectedExecution(Runnable r, - ThreadPoolExecutor executor) { - Log.w(TAG, "Rejected execution of runnable in schedExecutor"); - } - } + }, (r, executor) -> Log.w(TAG, "Rejected execution of runnable in schedExecutor") ); } /** - * Creates a new connection to the playbackService. Should be called in the - * activity's onResume() method. + * Creates a new connection to the playbackService. */ public void init() { + if (PlaybackService.isRunning) { + initServiceRunning(); + } else { + initServiceNotRunning(); + } + } + + private synchronized void initServiceRunning() { + if (initialized) { + return; + } + initialized = true; + activity.registerReceiver(statusUpdate, new IntentFilter( PlaybackService.ACTION_PLAYER_STATUS_CHANGED)); @@ -173,7 +185,7 @@ public abstract class PlaybackController { */ private void bindToService() { Log.d(TAG, "Trying to connect to service"); - if(serviceBinder != null) { + if (serviceBinder != null) { serviceBinder.unsubscribe(); } serviceBinder = Observable.fromCallable(this::getPlayLastPlayedMediaIntent) @@ -184,7 +196,7 @@ public abstract class PlaybackController { if (!PlaybackService.started) { if (intent != null) { Log.d(TAG, "Calling start service"); - activity.startService(intent); + ContextCompat.startForegroundService(activity, intent); bound = activity.bindService(intent, mConnection, 0); } else { status = PlayerStatus.STOPPED; @@ -204,31 +216,24 @@ public abstract class PlaybackController { * Returns an intent that starts the PlaybackService and plays the last * played media or null if no last played media could be found. */ - private Intent getPlayLastPlayedMediaIntent() { + @Nullable private Intent getPlayLastPlayedMediaIntent() { Log.d(TAG, "Trying to restore last played media"); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences( - activity.getApplicationContext()); - long currentlyPlayingMedia = PlaybackPreferences.getCurrentlyPlayingMedia(); - if (currentlyPlayingMedia != PlaybackPreferences.NO_MEDIA_PLAYING) { - Playable media = PlayableUtils.createInstanceFromPreferences(activity, - (int) currentlyPlayingMedia, prefs); - if (media != null) { - Intent serviceIntent = new Intent(activity, PlaybackService.class); - serviceIntent.putExtra(PlaybackService.EXTRA_PLAYABLE, media); - serviceIntent.putExtra(PlaybackService.EXTRA_START_WHEN_PREPARED, false); - serviceIntent.putExtra(PlaybackService.EXTRA_PREPARE_IMMEDIATELY, true); - boolean fileExists = media.localFileAvailable(); - boolean lastIsStream = PlaybackPreferences.getCurrentEpisodeIsStream(); - if (!fileExists && !lastIsStream && media instanceof FeedMedia) { - DBTasks.notifyMissingFeedMediaFile(activity, (FeedMedia) media); - } - serviceIntent.putExtra(PlaybackService.EXTRA_SHOULD_STREAM, - lastIsStream || !fileExists); - return serviceIntent; - } + Playable media = PlayableUtils.createInstanceFromPreferences(activity); + if (media == null) { + Log.d(TAG, "No last played media found"); + return null; } - Log.d(TAG, "No last played media found"); - return null; + + boolean fileExists = media.localFileAvailable(); + boolean lastIsStream = PlaybackPreferences.getCurrentEpisodeIsStream(); + if (!fileExists && !lastIsStream && media instanceof FeedMedia) { + DBTasks.notifyMissingFeedMediaFile(activity, (FeedMedia) media); + } + + return new PlaybackServiceStarter(activity, media) + .startWhenPrepared(false) + .shouldStream(lastIsStream || !fileExists) + .getIntent(); } @@ -517,7 +522,7 @@ public abstract class PlaybackController { "PlaybackService has no media object. Trying to restore last played media."); Intent serviceIntent = getPlayLastPlayedMediaIntent(); if (serviceIntent != null) { - activity.startService(serviceIntent); + ContextCompat.startForegroundService(activity, serviceIntent); } } */ @@ -582,6 +587,10 @@ public abstract class PlaybackController { public void playPause() { if (playbackService == null) { + new PlaybackServiceStarter(activity, media) + .startWhenPrepared(true) + .streamIfLastWasStream() + .start(); Log.w(TAG, "Play/Pause button was pressed, but playbackservice was null!"); return; } @@ -615,6 +624,8 @@ public abstract class PlaybackController { public int getPosition() { if (playbackService != null) { return playbackService.getCurrentPosition(); + } else if (media != null) { + return media.getPosition(); } else { return PlaybackService.INVALID_TIME; } @@ -623,12 +634,17 @@ public abstract class PlaybackController { public int getDuration() { if (playbackService != null) { return playbackService.getDuration(); + } else if (media != null) { + return media.getDuration(); } else { return PlaybackService.INVALID_TIME; } } public Playable getMedia() { + if (media == null) { + media = PlayableUtils.createInstanceFromPreferences(activity); + } return media; } @@ -720,8 +736,13 @@ public abstract class PlaybackController { } public boolean isPlayingVideoLocally() { - return playbackService != null && PlaybackService.getCurrentMediaType() == MediaType.VIDEO - && !PlaybackService.isCasting(); + if (PlaybackService.isCasting()) { + return false; + } else if (playbackService != null) { + return PlaybackService.getCurrentMediaType() == MediaType.VIDEO; + } else { + return getMedia() != null && getMedia().getMediaType() == MediaType.VIDEO; + } } public Pair<Integer, Integer> getVideoSize() { @@ -761,6 +782,27 @@ public abstract class PlaybackController { } } + private void initServiceNotRunning() { + Single.create(subscriber -> subscriber.onSuccess(getMedia())) + .subscribeOn(Schedulers.newThread()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe((Object media) -> { + if (media == null) { + return; + } + + if (((Playable) media).getMediaType() == MediaType.AUDIO) { + TypedArray res = activity.obtainStyledAttributes(new int[]{ + de.danoeh.antennapod.core.R.attr.av_play_big}); + getPlayButton().setImageResource( + res.getResourceId(0, de.danoeh.antennapod.core.R.drawable.ic_play_arrow_grey600_36dp)); + res.recycle(); + } else { + getPlayButton().setImageResource(R.drawable.ic_av_play_circle_outline_80dp); + } + }); + } + /** * Refreshes the current position of the media file that is playing. */ diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackServiceStarter.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackServiceStarter.java new file mode 100644 index 000000000..3ba553d12 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackServiceStarter.java @@ -0,0 +1,76 @@ +package de.danoeh.antennapod.core.util.playback; + +import android.content.Context; +import android.content.Intent; +import android.media.MediaPlayer; +import android.support.v4.content.ContextCompat; +import de.danoeh.antennapod.core.preferences.PlaybackPreferences; +import de.danoeh.antennapod.core.service.playback.PlaybackService; + +public class PlaybackServiceStarter { + private final Context context; + private final Playable media; + private boolean startWhenPrepared = false; + private boolean shouldStream = false; + private boolean callEvenIfRunning = false; + private boolean prepareImmediately = true; + + public PlaybackServiceStarter(Context context, Playable media) { + this.context = context; + this.media = media; + } + + /** + * Default value: false + */ + public PlaybackServiceStarter shouldStream(boolean shouldStream) { + this.shouldStream = shouldStream; + return this; + } + + public PlaybackServiceStarter streamIfLastWasStream() { + boolean lastIsStream = PlaybackPreferences.getCurrentEpisodeIsStream(); + return shouldStream(lastIsStream); + } + + /** + * Default value: false + */ + public PlaybackServiceStarter startWhenPrepared(boolean startWhenPrepared) { + this.startWhenPrepared = startWhenPrepared; + return this; + } + + /** + * Default value: false + */ + public PlaybackServiceStarter callEvenIfRunning(boolean callEvenIfRunning) { + this.callEvenIfRunning = callEvenIfRunning; + return this; + } + + /** + * Default value: true + */ + public PlaybackServiceStarter prepareImmediately(boolean prepareImmediately) { + this.prepareImmediately = prepareImmediately; + return this; + } + + public Intent getIntent() { + Intent launchIntent = new Intent(context, PlaybackService.class); + launchIntent.putExtra(PlaybackService.EXTRA_PLAYABLE, media); + launchIntent.putExtra(PlaybackService.EXTRA_START_WHEN_PREPARED, startWhenPrepared); + launchIntent.putExtra(PlaybackService.EXTRA_SHOULD_STREAM, shouldStream); + launchIntent.putExtra(PlaybackService.EXTRA_PREPARE_IMMEDIATELY, prepareImmediately); + + return launchIntent; + } + + public void start() { + if (PlaybackService.isRunning && !callEvenIfRunning) { + return; + } + ContextCompat.startForegroundService(context, getIntent()); + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/Timeline.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/Timeline.java index efdf46a97..34cfe6d05 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/Timeline.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/Timeline.java @@ -14,6 +14,7 @@ import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; +import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -42,22 +43,22 @@ public class Timeline { private final int pageMargin; public Timeline(Context context, ShownotesProvider shownotesProvider) { - if (shownotesProvider == null) throw new IllegalArgumentException("shownotesProvider = null"); + if (shownotesProvider == null) { + throw new IllegalArgumentException("shownotesProvider = null"); + } this.shownotesProvider = shownotesProvider; noShownotesLabel = context.getString(R.string.no_shownotes_label); - TypedArray res = context.getTheme().obtainStyledAttributes( - new int[]{ android.R.attr.textColorPrimary}); + TypedArray res = context.getTheme().obtainStyledAttributes(new int[]{android.R.attr.textColorPrimary}); @ColorInt int col = res.getColor(0, 0); colorPrimaryString = "rgba(" + Color.red(col) + "," + Color.green(col) + "," + - Color.blue(col) + "," + (Color.alpha(col)/256.0) + ")"; + Color.blue(col) + "," + (Color.alpha(col) / 255.0) + ")"; res.recycle(); - res = context.getTheme().obtainStyledAttributes( - new int[]{android.R.attr.textColorSecondary}); + res = context.getTheme().obtainStyledAttributes(new int[]{android.R.attr.textColorSecondary}); col = res.getColor(0, 0); colorSecondaryString = "rgba(" + Color.red(col) + "," + Color.green(col) + "," + - Color.blue(col) + "," + (Color.alpha(col)/256.0) + ")"; + Color.blue(col) + "," + (Color.alpha(col) / 255.0) + ")"; res.recycle(); pageMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8, @@ -93,9 +94,9 @@ public class Timeline { return null; } - if(TextUtils.isEmpty(shownotes)) { + if (TextUtils.isEmpty(shownotes)) { Log.d(TAG, "shownotesProvider contained no shownotes. Returning 'no shownotes' message"); - shownotes ="<html>" + + shownotes = "<html>" + "<head>" + "<style type='text/css'>" + "html, body { margin: 0; padding: 0; width: 100%; height: 100%; } " + @@ -113,15 +114,15 @@ public class Timeline { } // replace ASCII line breaks with HTML ones if shownotes don't contain HTML line breaks already - if(!LINE_BREAK_REGEX.matcher(shownotes).find() && !shownotes.contains("<p>")) { + if (!LINE_BREAK_REGEX.matcher(shownotes).find() && !shownotes.contains("<p>")) { shownotes = shownotes.replace("\n", "<br />"); } Document document = Jsoup.parse(shownotes); // apply style - String styleStr = String.format(WEBVIEW_STYLE, colorPrimaryString, "100%", pageMargin, - pageMargin, pageMargin, pageMargin); + String styleStr = String.format(Locale.getDefault(), WEBVIEW_STYLE, colorPrimaryString, "100%", + pageMargin, pageMargin, pageMargin, pageMargin); document.head().appendElement("style").attr("type", "text/css").text(styleStr); // apply timecode links @@ -139,7 +140,7 @@ public class Timeline { String rep; if (playable == null || playable.getDuration() > time) { - rep = String.format(TIMECODE_LINK, time, group); + rep = String.format(Locale.getDefault(), TIMECODE_LINK, time, group); } else { rep = group; } @@ -150,7 +151,7 @@ public class Timeline { element.html(buffer.toString()); } } - + return document.toString(); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/VideoPlayer.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/VideoPlayer.java index 368379509..1d04fb878 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/VideoPlayer.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/VideoPlayer.java @@ -7,11 +7,6 @@ public class VideoPlayer extends MediaPlayer implements IPlayer { private static final String TAG = "VideoPlayer"; @Override - public boolean canSetPitch() { - return false; - } - - @Override public boolean canSetSpeed() { return false; } @@ -22,44 +17,11 @@ public class VideoPlayer extends MediaPlayer implements IPlayer { } @Override - public float getCurrentPitchStepsAdjustment() { - return 1; - } - - @Override public float getCurrentSpeedMultiplier() { return 1; } @Override - public float getMaxSpeedMultiplier() { - return 1; - } - - @Override - public float getMinSpeedMultiplier() { - return 1; - } - - @Override - public void setEnableSpeedAdjustment(boolean enableSpeedAdjustment) throws UnsupportedOperationException { - Log.e(TAG, "Setting enable speed adjustment unsupported in video player"); - throw new UnsupportedOperationException("Setting enable speed adjustment unsupported in video player"); - } - - @Override - public void setPitchStepsAdjustment(float pitchSteps) { - Log.e(TAG, "Setting pitch steps adjustment unsupported in video player"); - throw new UnsupportedOperationException("Setting pitch steps adjustment unsupported in video player"); - } - - @Override - public void setPlaybackPitch(float f) { - Log.e(TAG, "Setting playback pitch unsupported in video player"); - throw new UnsupportedOperationException("Setting playback pitch unsupported in video player"); - } - - @Override public void setPlaybackSpeed(float f) { Log.e(TAG, "Setting playback speed unsupported in video player"); throw new UnsupportedOperationException("Setting playback speed unsupported in video player"); diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/syndication/FeedDiscoverer.java b/core/src/main/java/de/danoeh/antennapod/core/util/syndication/FeedDiscoverer.java index 13cb9f002..c5ad9cfd6 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/syndication/FeedDiscoverer.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/syndication/FeedDiscoverer.java @@ -41,7 +41,7 @@ public class FeedDiscoverer { * @return A map which contains the feed URLs as keys and titles as values (the feed URL is also used as a title if * a title cannot be found). */ - public Map<String, String> findLinks(String in, String baseUrl) throws IOException { + public Map<String, String> findLinks(String in, String baseUrl) { return findLinks(Jsoup.parse(in), baseUrl); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/syndication/HtmlToPlainText.java b/core/src/main/java/de/danoeh/antennapod/core/util/syndication/HtmlToPlainText.java index bd40f398d..cc6a8ec90 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/syndication/HtmlToPlainText.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/syndication/HtmlToPlainText.java @@ -40,9 +40,9 @@ public class HtmlToPlainText { } // the formatting rules, implemented in a breadth-first DOM traverse - private class FormattingVisitor implements NodeVisitor { + private static class FormattingVisitor implements NodeVisitor { - private StringBuilder accum = new StringBuilder(); // holds the accumulated text + private final StringBuilder accum = new StringBuilder(); // holds the accumulated text // hit when the node is first seen public void head(Node node, int depth) { diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/OggInputStream.java b/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/OggInputStream.java index 4799d3881..cdf171299 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/OggInputStream.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/OggInputStream.java @@ -6,8 +6,8 @@ import java.io.IOException; import java.io.InputStream; import java.util.Arrays; -public class OggInputStream extends InputStream { - private InputStream input; +class OggInputStream extends InputStream { + private final InputStream input; /** True if OggInputStream is currently inside an Ogg page. */ private boolean isInPage; diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentChapterReader.java b/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentChapterReader.java index 6243da5bc..569ff3438 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentChapterReader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentChapterReader.java @@ -1,13 +1,14 @@ package de.danoeh.antennapod.core.util.vorbiscommentreader; import android.util.Log; -import de.danoeh.antennapod.core.BuildConfig; -import de.danoeh.antennapod.core.feed.Chapter; -import de.danoeh.antennapod.core.feed.VorbisCommentChapter; import java.util.ArrayList; import java.util.List; +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.feed.Chapter; +import de.danoeh.antennapod.core.feed.VorbisCommentChapter; + public class VorbisCommentChapterReader extends VorbisCommentReader { private static final String TAG = "VorbisCommentChptrReadr"; diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentHeader.java b/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentHeader.java index 5f9dd0faf..ff7508390 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentHeader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentHeader.java @@ -1,7 +1,7 @@ package de.danoeh.antennapod.core.util.vorbiscommentreader; -public class VorbisCommentHeader { - private String vendorString; - private long userCommentLength; +class VorbisCommentHeader { + private final String vendorString; + private final long userCommentLength; public VorbisCommentHeader(String vendorString, long userCommentLength) { super(); diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentReader.java b/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentReader.java index 49ea18721..55498afcb 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentReader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentReader.java @@ -19,29 +19,29 @@ public abstract class VorbisCommentReader { private static final int PACKET_TYPE_COMMENT = 3; /** Called when Reader finds identification header. */ - public abstract void onVorbisCommentFound(); + protected abstract void onVorbisCommentFound(); - public abstract void onVorbisCommentHeaderFound(VorbisCommentHeader header); + protected abstract void onVorbisCommentHeaderFound(VorbisCommentHeader header); /** * Is called every time the Reader finds a content vector. The handler * should return true if it wants to handle the content vector. */ - public abstract boolean onContentVectorKey(String content); + protected abstract boolean onContentVectorKey(String content); /** * Is called if onContentVectorKey returned true for the key. * * @throws VorbisCommentReaderException */ - public abstract void onContentVectorValue(String key, String value) + protected abstract void onContentVectorValue(String key, String value) throws VorbisCommentReaderException; - public abstract void onNoVorbisCommentFound(); + protected abstract void onNoVorbisCommentFound(); - public abstract void onEndOfComment(); + protected abstract void onEndOfComment(); - public abstract void onError(VorbisCommentReaderException exception); + protected abstract void onError(VorbisCommentReaderException exception); public void readInputStream(InputStream input) throws VorbisCommentReaderException { |