summaryrefslogtreecommitdiff
path: root/core/src/main/java/de
diff options
context:
space:
mode:
authorRaghul Jagannathan <raghul@redmart.com>2016-01-27 20:16:24 +0800
committerRaghul Jagannathan <raghul@redmart.com>2016-01-27 20:16:24 +0800
commit5cc6f12acb0213dbca37e02c019ee27c28f4a0bb (patch)
tree432b483c14db8ec143407f3a4fba4072bb0dce9c /core/src/main/java/de
parentb4a363cecebdb15d885ab98cc48f5cc88142cfa1 (diff)
parentc1819864fe4e46945c42584a571cabc96c338644 (diff)
downloadAntennaPod-5cc6f12acb0213dbca37e02c019ee27c28f4a0bb.zip
Merge: Updated subscriptionview and synced develop
Diffstat (limited to 'core/src/main/java/de')
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/ApplicationCallbacks.java1
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java2
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/StorageCallbacks.java27
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/asynctask/DownloadObserver.java177
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/asynctask/FeedRemover.java32
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrClickWorker.java9
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrStatusFetcher.java2
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/asynctask/ImageResource.java (renamed from core/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoImageResource.java)2
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoProvider.java510
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/backup/OpmlBackupAgent.java2
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/dialog/ConfirmationDialog.java28
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/dialog/DownloadRequestErrorDialogCreator.java3
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/event/DownloadEvent.java28
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/event/DownloaderUpdate.java53
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/event/FavoritesEvent.java38
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/event/FeedItemEvent.java48
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/event/FeedMediaEvent.java23
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/event/ProgressEvent.java36
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/event/QueueEvent.java83
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/Chapter.java31
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/EventDistributor.java16
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/Feed.java92
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/FeedFilter.java111
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/FeedImage.java23
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java161
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilter.java84
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java188
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/FeedPreferences.java102
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/QueueEvent.java51
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/glide/ApGlideModule.java33
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/glide/ApGlideSettings.java11
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/glide/ApOkHttpUrlLoader.java141
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/glide/FastBlurTransformation.java267
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetService.java138
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetDevice.java8
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetEpisodeAction.java76
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetEpisodeActionGetResponse.java14
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetEpisodeActionPostResponse.java5
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetPodcast.java15
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetSubscriptionChange.java8
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetTag.java28
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetUploadChangesResponse.java5
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/opml/OpmlSymbols.java1
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/opml/OpmlWriter.java33
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/preferences/GpodnetPreferences.java24
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java193
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java790
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/receiver/AlarmUpdateReceiver.java26
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java6
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/GpodnetSyncService.java31
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/download/APRedirectHandler.java54
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/download/AntennapodHttpClient.java168
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadRequest.java22
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java348
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadStatus.java42
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java62
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/MediaButtonIntentReceiver.java26
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java462
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java479
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java135
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/ShakeListener.java64
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/APCleanupAlgorithm.java92
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/APDownloadAlgorithm.java39
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/APNullCleanupAlgorithm.java24
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/APQueueCleanupAlgorithm.java81
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/APSPCleanupAlgorithm.java139
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/APSPDownloadAlgorithm.java72
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/AutomaticDownloadAlgorithm.java4
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java854
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java194
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java1196
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java45
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/EpisodeCleanupAlgorithm.java51
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/FeedItemStatistics.java11
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java862
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/syndication/handler/HandlerState.java16
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/syndication/handler/TypeGetter.java38
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSITunes.java33
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSRSS20.java25
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/NSAtom.java24
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/Converter.java21
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/DateUtils.java65
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/FeedItemUtil.java78
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/IntentUtils.java18
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/LangUtils.java7
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/LongList.java33
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java108
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/QueueSorter.java2
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/RewindAfterPauseUtils.java47
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/ShareUtils.java49
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/StorageUtils.java26
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/URLChecker.java10
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrUtils.java59
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/gui/UndoBarController.java125
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/AudioPlayer.java15
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java29
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/IPlayer.java4
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java65
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java165
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/Timeline.java21
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/VideoPlayer.java11
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/syndication/FeedDiscoverer.java11
102 files changed, 5750 insertions, 4697 deletions
diff --git a/core/src/main/java/de/danoeh/antennapod/core/ApplicationCallbacks.java b/core/src/main/java/de/danoeh/antennapod/core/ApplicationCallbacks.java
index 3bc1ce4eb..1064e98ac 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/ApplicationCallbacks.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/ApplicationCallbacks.java
@@ -20,5 +20,4 @@ public interface ApplicationCallbacks {
*/
public Intent getStorageErrorActivity(Context context);
- public void setUpdateInterval(long updateInterval);
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java b/core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java
index 1a2671555..6619e706b 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java
@@ -21,7 +21,5 @@ public class ClientConfig {
public static FlattrCallbacks flattrCallbacks;
- public static StorageCallbacks storageCallbacks;
-
public static DBTasksCallbacks dbTasksCallbacks;
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/StorageCallbacks.java b/core/src/main/java/de/danoeh/antennapod/core/StorageCallbacks.java
deleted file mode 100644
index 5d1a0fffc..000000000
--- a/core/src/main/java/de/danoeh/antennapod/core/StorageCallbacks.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package de.danoeh.antennapod.core;
-
-import android.database.sqlite.SQLiteDatabase;
-
-/**
- * Callbacks for the classes in the storage package of the core module.
- */
-public interface StorageCallbacks {
-
- /**
- * Returns the current version of the database.
- *
- * @return The non-negative version number of the database.
- */
- public int getDatabaseVersion();
-
- /**
- * Upgrades the given database from an old version to a newer version.
- *
- * @param db The database that is supposed to be upgraded.
- * @param oldVersion The old version of the database.
- * @param newVersion The version that the database is supposed to be upgraded to.
- */
- public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion);
-
-
-}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/asynctask/DownloadObserver.java b/core/src/main/java/de/danoeh/antennapod/core/asynctask/DownloadObserver.java
deleted file mode 100644
index a13130082..000000000
--- a/core/src/main/java/de/danoeh/antennapod/core/asynctask/DownloadObserver.java
+++ /dev/null
@@ -1,177 +0,0 @@
-package de.danoeh.antennapod.core.asynctask;
-
-import android.app.Activity;
-import android.content.*;
-import android.os.Handler;
-import android.os.IBinder;
-import android.util.Log;
-
-import org.apache.commons.lang3.Validate;
-
-import de.danoeh.antennapod.core.BuildConfig;
-import de.danoeh.antennapod.core.service.download.DownloadService;
-import de.danoeh.antennapod.core.service.download.Downloader;
-
-import java.util.List;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-/**
- * Provides access to the DownloadService's list of items that are currently being downloaded.
- * The DownloadObserver object should be created in the activity's onCreate() method. resume() and pause()
- * should be called in the activity's onResume() and onPause() methods
- */
-public class DownloadObserver {
- private static final String TAG = "DownloadObserver";
-
- /**
- * Time period between update notifications.
- */
- public static final int WAITING_INTERVAL_MS = 3000;
-
- private volatile Activity activity;
- private final Handler handler;
- private final Callback callback;
-
- private DownloadService downloadService = null;
- private AtomicBoolean mIsBound = new AtomicBoolean(false);
-
- private Thread refresherThread;
- private AtomicBoolean refresherThreadRunning = new AtomicBoolean(false);
-
-
- /**
- * Creates a new download observer.
- *
- * @param activity Used for registering receivers
- * @param handler All callback methods are executed on this handler. The handler MUST run on the GUI thread.
- * @param callback Callback methods for posting content updates
- * @throws java.lang.IllegalArgumentException if one of the arguments is null.
- */
- public DownloadObserver(Activity activity, Handler handler, Callback callback) {
- Validate.notNull(activity);
- Validate.notNull(handler);
- Validate.notNull(callback);
-
- this.activity = activity;
- this.handler = handler;
- this.callback = callback;
- }
-
- public void onResume() {
- if (BuildConfig.DEBUG) Log.d(TAG, "DownloadObserver resumed");
- activity.registerReceiver(contentChangedReceiver, new IntentFilter(DownloadService.ACTION_DOWNLOADS_CONTENT_CHANGED));
- connectToDownloadService();
- }
-
- public void onPause() {
- if (BuildConfig.DEBUG) Log.d(TAG, "DownloadObserver paused");
- try {
- activity.unregisterReceiver(contentChangedReceiver);
- } catch (IllegalArgumentException e) {
- e.printStackTrace();
- }
- try {
- activity.unbindService(mConnection);
- } catch (IllegalArgumentException e) {
- e.printStackTrace();
- }
- stopRefresher();
- }
-
- private BroadcastReceiver contentChangedReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- // reconnect to DownloadService if connection has been closed
- if (downloadService == null) {
- connectToDownloadService();
- }
- callback.onContentChanged();
- startRefresher();
- }
- };
-
- public interface Callback {
- void onContentChanged();
-
- void onDownloadDataAvailable(List<Downloader> downloaderList);
- }
-
- private void connectToDownloadService() {
- activity.bindService(new Intent(activity, DownloadService.class), mConnection, 0);
- }
-
- private ServiceConnection mConnection = new ServiceConnection() {
- public void onServiceDisconnected(ComponentName className) {
- downloadService = null;
- mIsBound.set(false);
- stopRefresher();
- Log.i(TAG, "Closed connection with DownloadService.");
- }
-
- public void onServiceConnected(ComponentName name, IBinder service) {
- downloadService = ((DownloadService.LocalBinder) service)
- .getService();
- mIsBound.set(true);
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Connection to service established");
- List<Downloader> downloaderList = downloadService.getDownloads();
- if (downloaderList != null && !downloaderList.isEmpty()) {
- callback.onDownloadDataAvailable(downloaderList);
- startRefresher();
- }
- }
- };
-
- private void stopRefresher() {
- if (refresherThread != null) {
- refresherThread.interrupt();
- }
- }
-
- private void startRefresher() {
- if (refresherThread == null || refresherThread.isInterrupted()) {
- refresherThread = new Thread(new RefresherThread());
- refresherThread.start();
- }
- }
-
- private class RefresherThread implements Runnable {
-
- public void run() {
- refresherThreadRunning.set(true);
- while (!Thread.interrupted()) {
- try {
- Thread.sleep(WAITING_INTERVAL_MS);
- } catch (InterruptedException e) {
- Log.d(TAG, "Refresher thread was interrupted");
- }
- if (mIsBound.get()) {
- postUpdate();
- }
- }
- refresherThreadRunning.set(false);
- }
-
- private void postUpdate() {
- handler.post(new Runnable() {
- @Override
- public void run() {
- callback.onContentChanged();
- if (downloadService != null) {
- List<Downloader> downloaderList = downloadService.getDownloads();
- if (downloaderList == null || downloaderList.isEmpty()) {
- Thread.currentThread().interrupt();
- }
- }
- }
- });
- }
- }
-
- public void setActivity(Activity activity) {
- Validate.notNull(activity);
- this.activity = activity;
- }
-
-}
-
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 255b95119..7ff622f34 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
@@ -3,20 +3,22 @@ package de.danoeh.antennapod.core.asynctask;
import android.annotation.SuppressLint;
import android.app.ProgressDialog;
import android.content.Context;
-import android.content.DialogInterface;
-import android.content.DialogInterface.OnCancelListener;
+import android.content.Intent;
import android.os.AsyncTask;
+
+import java.util.concurrent.ExecutionException;
+
import de.danoeh.antennapod.core.R;
import de.danoeh.antennapod.core.feed.Feed;
+import de.danoeh.antennapod.core.service.playback.PlaybackService;
import de.danoeh.antennapod.core.storage.DBWriter;
-import java.util.concurrent.ExecutionException;
-
/** Removes a feed in the background. */
public class FeedRemover extends AsyncTask<Void, Void, Void> {
Context context;
ProgressDialog dialog;
Feed feed;
+ public boolean skipOnCompletion = false;
public FeedRemover(Context context, Feed feed) {
super();
@@ -35,30 +37,22 @@ public class FeedRemover extends AsyncTask<Void, Void, Void> {
}
return null;
}
-
- @Override
- protected void onCancelled() {
- dialog.dismiss();
- }
-
+
@Override
protected void onPostExecute(Void result) {
dialog.dismiss();
+ if(skipOnCompletion) {
+ context.sendBroadcast(new Intent(
+ PlaybackService.ACTION_SKIP_CURRENT_EPISODE));
+ }
}
@Override
protected void onPreExecute() {
dialog = new ProgressDialog(context);
dialog.setMessage(context.getString(R.string.feed_remover_msg));
- dialog.setOnCancelListener(new OnCancelListener() {
-
- @Override
- public void onCancel(DialogInterface dialog) {
- cancel(true);
-
- }
-
- });
+ dialog.setIndeterminate(true);
+ dialog.setCancelable(false);
dialog.show();
}
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 5d2d5d441..ac032fcc0 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
@@ -6,11 +6,11 @@ import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.os.AsyncTask;
+import android.support.annotation.NonNull;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
import android.widget.Toast;
-import org.apache.commons.lang3.Validate;
import org.shredzone.flattr4j.exception.FlattrException;
import java.util.LinkedList;
@@ -63,8 +63,7 @@ public class FlattrClickWorker extends AsyncTask<Void, Integer, FlattrClickWorke
*
* @param context A context for accessing the database and posting notifications. Must not be null.
*/
- public FlattrClickWorker(Context context) {
- Validate.notNull(context);
+ public FlattrClickWorker(@NonNull Context context) {
this.context = context.getApplicationContext();
}
@@ -90,11 +89,11 @@ public class FlattrClickWorker extends AsyncTask<Void, Integer, FlattrClickWorke
return ExitCode.NO_TOKEN;
}
- if (!NetworkUtils.networkAvailable(context)) {
+ if (!NetworkUtils.networkAvailable()) {
return ExitCode.NO_NETWORK;
}
- final List<FlattrThing> flattrQueue = DBReader.getFlattrQueue(context);
+ final List<FlattrThing> flattrQueue = DBReader.getFlattrQueue();
if (extraFlattrThing != null) {
flattrQueue.add(extraFlattrThing);
} else if (flattrQueue.size() == 1) {
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 c4aa76ac7..888591e89 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
@@ -32,7 +32,7 @@ public class FlattrStatusFetcher extends Thread {
try {
List<Flattr> flattredThings = FlattrUtils.retrieveFlattredThings();
- DBWriter.setFlattredStatus(context, flattredThings).get();
+ DBWriter.setFlattredStatus(flattredThings).get();
} catch (FlattrException e) {
e.printStackTrace();
Log.d(TAG, "flattrQueue exception retrieving list with flattred items " + e.getMessage());
diff --git a/core/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoImageResource.java b/core/src/main/java/de/danoeh/antennapod/core/asynctask/ImageResource.java
index c0d8049db..edd69f15b 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoImageResource.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/asynctask/ImageResource.java
@@ -6,7 +6,7 @@ import android.net.Uri;
* Classes that implement this interface provide access to an image resource that can
* be loaded by the Picasso library.
*/
-public interface PicassoImageResource {
+public interface ImageResource {
/**
* This scheme should be used by PicassoImageResources to
diff --git a/core/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoProvider.java b/core/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoProvider.java
deleted file mode 100644
index 4f2d5b204..000000000
--- a/core/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoProvider.java
+++ /dev/null
@@ -1,510 +0,0 @@
-package de.danoeh.antennapod.core.asynctask;
-
-import android.content.ContentResolver;
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.media.MediaMetadataRetriever;
-import android.net.Uri;
-import android.text.TextUtils;
-import android.util.Log;
-
-import com.squareup.okhttp.Interceptor;
-import com.squareup.okhttp.OkHttpClient;
-import com.squareup.okhttp.Response;
-import com.squareup.picasso.Cache;
-import com.squareup.picasso.LruCache;
-import com.squareup.picasso.OkHttpDownloader;
-import com.squareup.picasso.Picasso;
-import com.squareup.picasso.Request;
-import com.squareup.picasso.RequestHandler;
-import com.squareup.picasso.Transformation;
-
-import org.apache.commons.io.IOUtils;
-import org.apache.commons.lang3.StringUtils;
-
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.HttpURLConnection;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-
-import de.danoeh.antennapod.core.service.download.HttpDownloader;
-import de.danoeh.antennapod.core.storage.DBReader;
-
-/**
- * Provides access to Picasso instances.
- */
-public class PicassoProvider {
-
- private static final String TAG = "PicassoProvider";
-
- private static final boolean DEBUG = false;
-
- private static ExecutorService executorService;
- private static Cache memoryCache;
-
- private static synchronized ExecutorService getExecutorService() {
- if (executorService == null) {
- executorService = Executors.newFixedThreadPool(3);
- }
- return executorService;
- }
-
- private static synchronized Cache getMemoryCache(Context context) {
- if (memoryCache == null) {
- memoryCache = new LruCache(context);
- }
- return memoryCache;
- }
-
- private static volatile boolean picassoSetup = false;
-
- public static synchronized void setupPicassoInstance(Context appContext) {
- if (picassoSetup) {
- return;
- }
- OkHttpClient client = new OkHttpClient();
- client.interceptors().add(new BasicAuthenticationInterceptor(appContext));
- Picasso picasso = new Picasso.Builder(appContext)
- .indicatorsEnabled(DEBUG)
- .loggingEnabled(DEBUG)
- .downloader(new OkHttpDownloader(client))
- .addRequestHandler(new MediaRequestHandler(appContext))
- .executor(getExecutorService())
- .memoryCache(getMemoryCache(appContext))
- .listener(new Picasso.Listener() {
- @Override
- public void onImageLoadFailed(Picasso picasso, Uri uri, Exception e) {
- Log.e(TAG, "Failed to load Uri:" + uri.toString());
- e.printStackTrace();
- }
- })
- .build();
- Picasso.setSingletonInstance(picasso);
- picassoSetup = true;
- }
-
- private static class BasicAuthenticationInterceptor implements Interceptor {
-
- private final Context context;
-
- public BasicAuthenticationInterceptor(Context context) {
- this.context = context;
- }
-
- @Override
- public Response intercept(Chain chain) throws IOException {
- com.squareup.okhttp.Request request = chain.request();
- String url = request.urlString();
- String authentication = DBReader.getImageAuthentication(context, url);
-
- if(TextUtils.isEmpty(authentication)) {
- Log.d(TAG, "no credentials for '" + url + "'");
- return chain.proceed(request);
- }
-
- // add authentication
- String[] auth = authentication.split(":");
- String credentials = HttpDownloader.encodeCredentials(auth[0], auth[1], "ISO-8859-1");
- com.squareup.okhttp.Request newRequest = request
- .newBuilder()
- .addHeader("Authorization", credentials)
- .build();
- Log.d(TAG, "Basic authentication with ISO-8859-1 encoding");
- Response response = chain.proceed(newRequest);
- if (!response.isSuccessful() && response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {
- credentials = HttpDownloader.encodeCredentials(auth[0], auth[1], "UTF-8");
- newRequest = request
- .newBuilder()
- .addHeader("Authorization", credentials)
- .build();
- Log.d(TAG, "Basic authentication with UTF-8 encoding");
- return chain.proceed(newRequest);
- } else {
- return response;
- }
- }
- }
-
- private static class MediaRequestHandler extends RequestHandler {
-
- final Context context;
-
- public MediaRequestHandler(Context context) {
- super();
- this.context = context;
- }
-
- @Override
- public boolean canHandleRequest(Request data) {
- return StringUtils.equals(data.uri.getScheme(), PicassoImageResource.SCHEME_MEDIA);
- }
-
- @Override
- public Result load(Request data, int networkPolicy) throws IOException {
- Bitmap bitmap = null;
- MediaMetadataRetriever mmr = null;
- try {
- mmr = new MediaMetadataRetriever();
- mmr.setDataSource(data.uri.getPath());
- byte[] image = mmr.getEmbeddedPicture();
- if (image != null) {
- bitmap = decodeStreamFromByteArray(data, image);
- }
- } catch (RuntimeException e) {
- Log.e(TAG, "Failed to decode image in media file", e);
- } finally {
- if (mmr != null) {
- mmr.release();
- }
- }
-
- if (bitmap == null) {
- Log.wtf(TAG, "THIS SHOULD NEVER EVER HAPPEN!!");
- }
- return new Result(bitmap, Picasso.LoadedFrom.DISK);
-
- }
-
- /* Copied/Adapted from Picasso RequestHandler classes */
-
- private Bitmap decodeStreamFromByteArray(Request data, byte[] bytes) throws IOException {
-
- final BitmapFactory.Options options = createBitmapOptions(data);
- final ByteArrayInputStream in = new ByteArrayInputStream(bytes);
- in.mark(0);
- if (requiresInSampleSize(options)) {
- try {
- BitmapFactory.decodeStream(in, null, options);
- } finally {
- in.reset();
- }
- calculateInSampleSize(data.targetWidth, data.targetHeight, options, data);
- }
- try {
- return BitmapFactory.decodeStream(in, null, options);
- } finally {
- IOUtils.closeQuietly(in);
- }
- }
-
- private Bitmap decodeStreamFromFile(Request data, Uri uri) throws IOException {
- ContentResolver contentResolver = context.getContentResolver();
- final BitmapFactory.Options options = createBitmapOptions(data);
- if (requiresInSampleSize(options)) {
- InputStream is = null;
- try {
- is = contentResolver.openInputStream(uri);
- BitmapFactory.decodeStream(is, null, options);
- } finally {
- IOUtils.closeQuietly(is);
- }
- calculateInSampleSize(data.targetWidth, data.targetHeight, options, data);
- }
- InputStream is = contentResolver.openInputStream(uri);
- try {
- return BitmapFactory.decodeStream(is, null, options);
- } finally {
- IOUtils.closeQuietly(is);
- }
- }
-
- private BitmapFactory.Options createBitmapOptions(Request data) {
- final boolean justBounds = data.hasSize();
- final boolean hasConfig = data.config != null;
- BitmapFactory.Options options = null;
- if (justBounds || hasConfig) {
- options = new BitmapFactory.Options();
- options.inJustDecodeBounds = justBounds;
- if (hasConfig) {
- options.inPreferredConfig = data.config;
- }
- }
- return options;
- }
-
- private static boolean requiresInSampleSize(BitmapFactory.Options options) {
- return options != null && options.inJustDecodeBounds;
- }
-
- private static void calculateInSampleSize(int reqWidth, int reqHeight, BitmapFactory.Options options,
- Request request) {
- calculateInSampleSize(reqWidth, reqHeight, options.outWidth, options.outHeight, options,
- request);
- }
-
- private static void calculateInSampleSize(int reqWidth, int reqHeight, int width, int height,
- BitmapFactory.Options options, Request request) {
- int sampleSize = 1;
- if (height > reqHeight || width > reqWidth) {
- final int heightRatio;
- final int widthRatio;
- if (reqHeight == 0) {
- sampleSize = (int) Math.floor((float) width / (float) reqWidth);
- } else if (reqWidth == 0) {
- sampleSize = (int) Math.floor((float) height / (float) reqHeight);
- } else {
- heightRatio = (int) Math.floor((float) height / (float) reqHeight);
- widthRatio = (int) Math.floor((float) width / (float) reqWidth);
- sampleSize = request.centerInside
- ? Math.max(heightRatio, widthRatio)
- : Math.min(heightRatio, widthRatio);
- }
- }
- options.inSampleSize = sampleSize;
- options.inJustDecodeBounds = false;
- }
- }
-
- public static final int BLUR_RADIUS = 1;
- public static final int BLUR_IMAGE_SIZE = 100;
- public static final String BLUR_KEY = "blur";
-
- public static final Transformation blurTransformation = new Transformation() {
- @Override
- public Bitmap transform(Bitmap source) {
- Bitmap result = fastblur(source, BLUR_RADIUS);
- source.recycle();
- return result;
- }
-
- @Override
- public String key() {
- return BLUR_KEY;
- }
- };
-
- public static Bitmap fastblur(Bitmap sentBitmap, int radius) {
-
- // Stack Blur v1.0 from
- // http://www.quasimondo.com/StackBlurForCanvas/StackBlurDemo.html
- //
- // Java Author: Mario Klingemann <mario at quasimondo.com>
- // http://incubator.quasimondo.com
- // created Feburary 29, 2004
- // Android port : Yahel Bouaziz <yahel at kayenko.com>
- // http://www.kayenko.com
- // ported april 5th, 2012
-
- // This is a compromise between Gaussian Blur and Box blur
- // It creates much better looking blurs than Box Blur, but is
- // 7x faster than my Gaussian Blur implementation.
- //
- // I called it Stack Blur because this describes best how this
- // filter works internally: it creates a kind of moving stack
- // of colors whilst scanning through the image. Thereby it
- // just has to add one new block of color to the right side
- // of the stack and remove the leftmost color. The remaining
- // colors on the topmost layer of the stack are either added on
- // or reduced by one, depending on if they are on the right or
- // on the left side of the stack.
- //
- // If you are using this algorithm in your code please add
- // the following line:
- //
- // Stack Blur Algorithm by Mario Klingemann <mario@quasimondo.com>
-
- Bitmap bitmap = sentBitmap.copy(sentBitmap.getConfig(), true);
-
- if (radius < 1) {
- return (null);
- }
-
- int w = bitmap.getWidth();
- int h = bitmap.getHeight();
-
- int[] pix = new int[w * h];
- Log.e("pix", w + " " + h + " " + pix.length);
- bitmap.getPixels(pix, 0, w, 0, 0, w, h);
-
- int wm = w - 1;
- int hm = h - 1;
- int wh = w * h;
- int div = radius + radius + 1;
-
- int r[] = new int[wh];
- int g[] = new int[wh];
- int b[] = new int[wh];
- int rsum, gsum, bsum, x, y, i, p, yp, yi, yw;
- int vmin[] = new int[Math.max(w, h)];
-
- int divsum = (div + 1) >> 1;
- divsum *= divsum;
- int dv[] = new int[256 * divsum];
- for (i = 0; i < 256 * divsum; i++) {
- dv[i] = (i / divsum);
- }
-
- yw = yi = 0;
-
- int[][] stack = new int[div][3];
- int stackpointer;
- int stackstart;
- int[] sir;
- int rbs;
- int r1 = radius + 1;
- int routsum, goutsum, boutsum;
- int rinsum, ginsum, binsum;
-
- for (y = 0; y < h; y++) {
- rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0;
- for (i = -radius; i <= radius; i++) {
- p = pix[yi + Math.min(wm, Math.max(i, 0))];
- sir = stack[i + radius];
- sir[0] = (p & 0xff0000) >> 16;
- sir[1] = (p & 0x00ff00) >> 8;
- sir[2] = (p & 0x0000ff);
- rbs = r1 - Math.abs(i);
- rsum += sir[0] * rbs;
- gsum += sir[1] * rbs;
- bsum += sir[2] * rbs;
- if (i > 0) {
- rinsum += sir[0];
- ginsum += sir[1];
- binsum += sir[2];
- } else {
- routsum += sir[0];
- goutsum += sir[1];
- boutsum += sir[2];
- }
- }
- stackpointer = radius;
-
- for (x = 0; x < w; x++) {
-
- r[yi] = dv[rsum];
- g[yi] = dv[gsum];
- b[yi] = dv[bsum];
-
- rsum -= routsum;
- gsum -= goutsum;
- bsum -= boutsum;
-
- stackstart = stackpointer - radius + div;
- sir = stack[stackstart % div];
-
- routsum -= sir[0];
- goutsum -= sir[1];
- boutsum -= sir[2];
-
- if (y == 0) {
- vmin[x] = Math.min(x + radius + 1, wm);
- }
- p = pix[yw + vmin[x]];
-
- sir[0] = (p & 0xff0000) >> 16;
- sir[1] = (p & 0x00ff00) >> 8;
- sir[2] = (p & 0x0000ff);
-
- rinsum += sir[0];
- ginsum += sir[1];
- binsum += sir[2];
-
- rsum += rinsum;
- gsum += ginsum;
- bsum += binsum;
-
- stackpointer = (stackpointer + 1) % div;
- sir = stack[(stackpointer) % div];
-
- routsum += sir[0];
- goutsum += sir[1];
- boutsum += sir[2];
-
- rinsum -= sir[0];
- ginsum -= sir[1];
- binsum -= sir[2];
-
- yi++;
- }
- yw += w;
- }
- for (x = 0; x < w; x++) {
- rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0;
- yp = -radius * w;
- for (i = -radius; i <= radius; i++) {
- yi = Math.max(0, yp) + x;
-
- sir = stack[i + radius];
-
- sir[0] = r[yi];
- sir[1] = g[yi];
- sir[2] = b[yi];
-
- rbs = r1 - Math.abs(i);
-
- rsum += r[yi] * rbs;
- gsum += g[yi] * rbs;
- bsum += b[yi] * rbs;
-
- if (i > 0) {
- rinsum += sir[0];
- ginsum += sir[1];
- binsum += sir[2];
- } else {
- routsum += sir[0];
- goutsum += sir[1];
- boutsum += sir[2];
- }
-
- if (i < hm) {
- yp += w;
- }
- }
- yi = x;
- stackpointer = radius;
- for (y = 0; y < h; y++) {
- // Preserve alpha channel: ( 0xff000000 & pix[yi] )
- pix[yi] = (0xff000000 & pix[yi]) | (dv[rsum] << 16) | (dv[gsum] << 8) | dv[bsum];
-
- rsum -= routsum;
- gsum -= goutsum;
- bsum -= boutsum;
-
- stackstart = stackpointer - radius + div;
- sir = stack[stackstart % div];
-
- routsum -= sir[0];
- goutsum -= sir[1];
- boutsum -= sir[2];
-
- if (x == 0) {
- vmin[y] = Math.min(y + r1, hm) * w;
- }
- p = x + vmin[y];
-
- sir[0] = r[p];
- sir[1] = g[p];
- sir[2] = b[p];
-
- rinsum += sir[0];
- ginsum += sir[1];
- binsum += sir[2];
-
- rsum += rinsum;
- gsum += ginsum;
- bsum += binsum;
-
- stackpointer = (stackpointer + 1) % div;
- sir = stack[stackpointer];
-
- routsum += sir[0];
- goutsum += sir[1];
- boutsum += sir[2];
-
- rinsum -= sir[0];
- ginsum -= sir[1];
- binsum -= sir[2];
-
- yi += w;
- }
- }
-
- Log.e("pix", w + " " + h + " " + pix.length);
- bitmap.setPixels(pix, 0, w, 0, 0, w, h);
-
- return (bitmap);
- }
-}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/backup/OpmlBackupAgent.java b/core/src/main/java/de/danoeh/antennapod/core/backup/OpmlBackupAgent.java
index 1535e2e9a..5ea0ba904 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/backup/OpmlBackupAgent.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/backup/OpmlBackupAgent.java
@@ -89,7 +89,7 @@ public class OpmlBackupAgent extends BackupAgentHelper {
try {
// Write OPML
- new OpmlWriter().writeDocument(DBReader.getFeedList(mContext), writer);
+ new OpmlWriter().writeDocument(DBReader.getFeedList(), writer);
// Compare checksum of new and old file to see if we need to perform a backup at all
if (digester != null) {
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 ba1add895..abb75e5e7 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
@@ -1,10 +1,10 @@
package de.danoeh.antennapod.core.dialog;
-import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
+import android.support.v7.app.AlertDialog;
import android.util.Log;
-import de.danoeh.antennapod.core.BuildConfig;
+
import de.danoeh.antennapod.core.R;
/**
@@ -12,12 +12,16 @@ import de.danoeh.antennapod.core.R;
* classes can handle events like confirmation or cancellation.
*/
public abstract class ConfirmationDialog {
- private static final String TAG = "ConfirmationDialog";
- Context context;
+ private static final String TAG = ConfirmationDialog.class.getSimpleName();
+
+ protected Context context;
int titleId;
int messageId;
+ int positiveText;
+ int negativeText;
+
public ConfirmationDialog(Context context, int titleId, int messageId) {
this.context = context;
this.titleId = titleId;
@@ -25,18 +29,26 @@ public abstract class ConfirmationDialog {
}
public void onCancelButtonPressed(DialogInterface dialog) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Dialog was cancelled");
+ Log.d(TAG, "Dialog was cancelled");
dialog.dismiss();
}
+ public void setPositiveText(int id) {
+ this.positiveText = id;
+ }
+
+ public void setNegativeText(int id) {
+ this.negativeText = id;
+ }
+
+
public abstract void onConfirmButtonPressed(DialogInterface dialog);
public final AlertDialog createNewDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle(titleId);
builder.setMessage(messageId);
- builder.setPositiveButton(R.string.confirm_label,
+ builder.setPositiveButton(positiveText != 0 ? positiveText : R.string.confirm_label,
new DialogInterface.OnClickListener() {
@Override
@@ -44,7 +56,7 @@ public abstract class ConfirmationDialog {
onConfirmButtonPressed(dialog);
}
});
- builder.setNegativeButton(R.string.cancel_label,
+ builder.setNegativeButton(negativeText != 0 ? negativeText : R.string.cancel_label,
new DialogInterface.OnClickListener() {
@Override
diff --git a/core/src/main/java/de/danoeh/antennapod/core/dialog/DownloadRequestErrorDialogCreator.java b/core/src/main/java/de/danoeh/antennapod/core/dialog/DownloadRequestErrorDialogCreator.java
index 3d174bd8e..b7e79431d 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/dialog/DownloadRequestErrorDialogCreator.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/dialog/DownloadRequestErrorDialogCreator.java
@@ -1,8 +1,9 @@
package de.danoeh.antennapod.core.dialog;
-import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
+import android.support.v7.app.AlertDialog;
+
import de.danoeh.antennapod.core.R;
/** Creates Alert Dialogs if a DownloadRequestException has happened. */
diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/DownloadEvent.java b/core/src/main/java/de/danoeh/antennapod/core/event/DownloadEvent.java
new file mode 100644
index 000000000..124fd3e64
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/event/DownloadEvent.java
@@ -0,0 +1,28 @@
+package de.danoeh.antennapod.core.event;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import de.danoeh.antennapod.core.service.download.Downloader;
+
+public class DownloadEvent {
+
+ public final DownloaderUpdate update;
+
+ private DownloadEvent(DownloaderUpdate downloader) {
+ this.update = downloader;
+ }
+
+ public static DownloadEvent refresh(List<Downloader> list) {
+ list = new ArrayList<>(list);
+ DownloaderUpdate update = new DownloaderUpdate(list);
+ return new DownloadEvent(update);
+ }
+
+ @Override
+ public String toString() {
+ return "DownloadEvent{" +
+ "update=" + update +
+ '}';
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/DownloaderUpdate.java b/core/src/main/java/de/danoeh/antennapod/core/event/DownloaderUpdate.java
new file mode 100644
index 000000000..dcb033267
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/event/DownloaderUpdate.java
@@ -0,0 +1,53 @@
+package de.danoeh.antennapod.core.event;
+
+import java.util.Arrays;
+import java.util.List;
+
+import de.danoeh.antennapod.core.feed.Feed;
+import de.danoeh.antennapod.core.feed.FeedMedia;
+import de.danoeh.antennapod.core.service.download.Downloader;
+import de.danoeh.antennapod.core.util.LongList;
+
+public class DownloaderUpdate {
+
+ /* Downloaders that are currently running */
+ public final List<Downloader> downloaders;
+
+ /**
+ * IDs of feeds that are currently being downloaded
+ * Often used to show some progress wheel in the action bar
+ */
+ public final long[] feedIds;
+
+ /**
+ * IDs of feed media that are currently being downloaded
+ * Can be used to show and update download progress bars
+ */
+ public final long[] mediaIds;
+
+ public DownloaderUpdate(List<Downloader> downloaders) {
+ this.downloaders = downloaders;
+ LongList feedIds1 = new LongList(), mediaIds1 = new LongList();
+ for(Downloader d1 : downloaders) {
+ int type = d1.getDownloadRequest().getFeedfileType();
+ long id = d1.getDownloadRequest().getFeedfileId();
+ if(type == Feed.FEEDFILETYPE_FEED) {
+ feedIds1.add(id);
+ } else if(type == FeedMedia.FEEDFILETYPE_FEEDMEDIA) {
+ mediaIds1.add(id);
+ }
+ }
+
+ this.feedIds = feedIds1.toArray();
+ this.mediaIds = mediaIds1.toArray();
+ }
+
+ @Override
+ public String toString() {
+ return "DownloaderUpdate{" +
+ "downloaders=" + downloaders +
+ ", feedIds=" + Arrays.toString(feedIds) +
+ ", mediaIds=" + Arrays.toString(mediaIds) +
+ '}';
+ }
+}
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
new file mode 100644
index 000000000..d09f6802f
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/event/FavoritesEvent.java
@@ -0,0 +1,38 @@
+package de.danoeh.antennapod.core.event;
+
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+import de.danoeh.antennapod.core.feed.FeedItem;
+
+public class FavoritesEvent {
+
+ public enum Action {
+ ADDED, REMOVED
+ }
+
+ public final Action action;
+ public final FeedItem item;
+
+ private FavoritesEvent(Action action, FeedItem item) {
+ this.action = action;
+ this.item = item;
+ }
+
+ public static FavoritesEvent added(FeedItem item) {
+ return new FavoritesEvent(Action.ADDED, item);
+ }
+
+ public static FavoritesEvent removed(FeedItem item) {
+ return new FavoritesEvent(Action.REMOVED, item);
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
+ .append("action", action)
+ .append("item", item)
+ .toString();
+ }
+
+}
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
new file mode 100644
index 000000000..7ff241456
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/event/FeedItemEvent.java
@@ -0,0 +1,48 @@
+package de.danoeh.antennapod.core.event;
+
+
+import android.support.annotation.NonNull;
+
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+import java.util.Arrays;
+import java.util.List;
+
+import de.danoeh.antennapod.core.feed.FeedItem;
+
+public class FeedItemEvent {
+
+ public enum Action {
+ UPDATE, DELETE_MEDIA
+ }
+
+ @NonNull public final Action action;
+ @NonNull public final List<FeedItem> items;
+
+ private FeedItemEvent(Action action, List<FeedItem> items) {
+ this.action = action;
+ this.items = items;
+ }
+
+ public static FeedItemEvent deletedMedia(List<FeedItem> items) {
+ return new FeedItemEvent(Action.DELETE_MEDIA, items);
+ }
+
+ public static FeedItemEvent updated(List<FeedItem> items) {
+ return new FeedItemEvent(Action.UPDATE, items);
+ }
+
+ public static FeedItemEvent updated(FeedItem... items) {
+ return new FeedItemEvent(Action.UPDATE, Arrays.asList(items));
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
+ .append("action", action)
+ .append("items", items)
+ .toString();
+ }
+
+}
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
new file mode 100644
index 000000000..864d0a405
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/event/FeedMediaEvent.java
@@ -0,0 +1,23 @@
+package de.danoeh.antennapod.core.event;
+
+import de.danoeh.antennapod.core.feed.FeedMedia;
+
+public class FeedMediaEvent {
+
+ public enum Action {
+ UPDATE
+ }
+
+ public final Action action;
+ public final FeedMedia media;
+
+ private FeedMediaEvent(Action action, FeedMedia media) {
+ this.action = action;
+ this.media = media;
+ }
+
+ public static FeedMediaEvent update(FeedMedia media) {
+ return new FeedMediaEvent(Action.UPDATE, media);
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/ProgressEvent.java b/core/src/main/java/de/danoeh/antennapod/core/event/ProgressEvent.java
new file mode 100644
index 000000000..3769d6bb1
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/event/ProgressEvent.java
@@ -0,0 +1,36 @@
+package de.danoeh.antennapod.core.event;
+
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+public class ProgressEvent {
+
+ public enum Action {
+ START, END
+ }
+
+ public final Action action;
+ public final String message;
+
+ private ProgressEvent(Action action, String message) {
+ this.action = action;
+ this.message = message;
+ }
+
+ public static ProgressEvent start(String message) {
+ return new ProgressEvent(Action.START, message);
+ }
+
+ public static ProgressEvent end() {
+ return new ProgressEvent(Action.END, null);
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
+ .append("action", action)
+ .append("message", message)
+ .toString();
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/QueueEvent.java b/core/src/main/java/de/danoeh/antennapod/core/event/QueueEvent.java
new file mode 100644
index 000000000..a84e8456f
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/event/QueueEvent.java
@@ -0,0 +1,83 @@
+package de.danoeh.antennapod.core.event;
+
+import android.support.annotation.Nullable;
+
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+import java.util.List;
+
+import de.danoeh.antennapod.core.feed.FeedItem;
+
+public class QueueEvent {
+
+ public enum Action {
+ ADDED, ADDED_ITEMS, SET_QUEUE, REMOVED, IRREVERSIBLE_REMOVED, CLEARED, DELETED_MEDIA, SORTED, MOVED
+ }
+
+ public final Action action;
+ public final FeedItem item;
+ public final int position;
+ public final List<FeedItem> items;
+
+
+ private QueueEvent(Action action,
+ @Nullable FeedItem item,
+ @Nullable List<FeedItem> items,
+ int position) {
+ this.action = action;
+ this.item = item;
+ this.items = items;
+ this.position = position;
+ }
+
+ public static QueueEvent added(FeedItem item, int position) {
+ return new QueueEvent(Action.ADDED, item, null, position);
+ }
+
+ public static QueueEvent setQueue(List<FeedItem> queue) {
+ return new QueueEvent(Action.SET_QUEUE, null, queue, -1);
+ }
+
+ public static QueueEvent removed(FeedItem item) {
+ return new QueueEvent(Action.REMOVED, item, null, -1);
+ }
+
+ public static QueueEvent irreversibleRemoved(FeedItem item) {
+ return new QueueEvent(Action.IRREVERSIBLE_REMOVED, item, null, -1);
+ }
+
+ public static QueueEvent cleared() {
+ return new QueueEvent(Action.CLEARED, null, null, -1);
+ }
+
+ public static QueueEvent sorted(List<FeedItem> sortedQueue) {
+ return new QueueEvent(Action.SORTED, null, sortedQueue, -1);
+ }
+
+ public static QueueEvent moved(FeedItem item, int newPosition) {
+ return new QueueEvent(Action.MOVED, item, null, newPosition);
+ }
+
+ public boolean contains(long id) {
+ if(item != null) {
+ return item.getId() == id;
+ }
+ for(FeedItem item : items) {
+ if(item.getId() == id) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
+ .append("action", action)
+ .append("item", item)
+ .append("items", items)
+ .append("position", position)
+ .toString();
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/Chapter.java b/core/src/main/java/de/danoeh/antennapod/core/feed/Chapter.java
index ce3352ed6..bb594ff87 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
@@ -1,5 +1,9 @@
package de.danoeh.antennapod.core.feed;
+import android.database.Cursor;
+
+import de.danoeh.antennapod.core.storage.PodDBAdapter;
+
public abstract class Chapter extends FeedComponent {
/** Defines starting point in milliseconds. */
@@ -22,6 +26,33 @@ public abstract class Chapter extends FeedComponent {
this.link = link;
}
+ public static Chapter fromCursor(Cursor cursor, FeedItem item) {
+ int indexTitle = cursor.getColumnIndex(PodDBAdapter.KEY_TITLE);
+ int indexStart = cursor.getColumnIndex(PodDBAdapter.KEY_START);
+ int indexLink = cursor.getColumnIndex(PodDBAdapter.KEY_LINK);
+ int indexChapterType = cursor.getColumnIndex(PodDBAdapter.KEY_CHAPTER_TYPE);
+
+ String title = cursor.getString(indexTitle);
+ long start = cursor.getLong(indexStart);
+ String link = cursor.getString(indexLink);
+ int chapterType = cursor.getInt(indexChapterType);
+
+ Chapter chapter = null;
+ switch (chapterType) {
+ case SimpleChapter.CHAPTERTYPE_SIMPLECHAPTER:
+ chapter = new SimpleChapter(start, title, item, link);
+ break;
+ case ID3Chapter.CHAPTERTYPE_ID3CHAPTER:
+ chapter = new ID3Chapter(start, title, item, link);
+ break;
+ case VorbisCommentChapter.CHAPTERTYPE_VORBISCOMMENT_CHAPTER:
+ chapter = new VorbisCommentChapter(start, title, item, link);
+ break;
+ }
+ return chapter;
+ }
+
+
public abstract int getChapterType();
public long getStart() {
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 20a85d43f..7ccb742fb 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
@@ -3,8 +3,6 @@ package de.danoeh.antennapod.core.feed;
import android.os.Handler;
import android.util.Log;
-import org.apache.commons.lang3.Validate;
-
import java.util.AbstractQueue;
import java.util.Observable;
import java.util.Observer;
@@ -26,7 +24,6 @@ public class EventDistributor extends Observable {
public static final int UNREAD_ITEMS_UPDATE = 2;
public static final int DOWNLOADLOG_UPDATE = 8;
public static final int PLAYBACK_HISTORY_UPDATE = 16;
- public static final int DOWNLOAD_QUEUED = 32;
public static final int DOWNLOAD_HANDLED = 64;
public static final int PLAYER_STATUS_UPDATE = 128;
@@ -85,11 +82,6 @@ public class EventDistributor extends Observable {
@Override
public void addObserver(Observer observer) {
super.addObserver(observer);
- Validate.isInstanceOf(EventListener.class, observer);
- }
-
- public void sendDownloadQueuedBroadcast() {
- addEvent(DOWNLOAD_QUEUED);
}
public void sendUnreadItemsUpdateBroadcast() {
@@ -108,13 +100,7 @@ public class EventDistributor extends Observable {
addEvent(DOWNLOADLOG_UPDATE);
}
- public void sendDownloadHandledBroadcast() {
- addEvent(DOWNLOAD_HANDLED);
- }
-
- public void sendPlayerStatusUpdateBroadcast() {
- addEvent(PLAYER_STATUS_UPDATE);
- }
+ public void sendPlayerStatusUpdateBroadcast() { addEvent(PLAYER_STATUS_UPDATE); }
public static abstract class EventListener implements Observer {
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 29ba721fe..4be788f33 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
@@ -1,17 +1,18 @@
package de.danoeh.antennapod.core.feed;
import android.content.Context;
+import android.database.Cursor;
import android.net.Uri;
import android.support.annotation.Nullable;
-
-import org.apache.commons.lang3.StringUtils;
+import android.text.TextUtils;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
-import de.danoeh.antennapod.core.asynctask.PicassoImageResource;
+import de.danoeh.antennapod.core.asynctask.ImageResource;
import de.danoeh.antennapod.core.storage.DBWriter;
+import de.danoeh.antennapod.core.storage.PodDBAdapter;
import de.danoeh.antennapod.core.util.flattr.FlattrStatus;
import de.danoeh.antennapod.core.util.flattr.FlattrThing;
@@ -20,7 +21,7 @@ import de.danoeh.antennapod.core.util.flattr.FlattrThing;
*
* @author daniel
*/
-public class Feed extends FeedFile implements FlattrThing, PicassoImageResource {
+public class Feed extends FeedFile implements FlattrThing, ImageResource {
public static final int FEEDFILETYPE_FEED = 0;
public static final String TYPE_RSS2 = "rss";
public static final String TYPE_RSS091 = "rss";
@@ -167,20 +168,80 @@ public class Feed extends FeedFile implements FlattrThing, PicassoImageResource
*/
public Feed(String url, Date lastUpdate, String title, String username, String password) {
this(url, lastUpdate, title);
- preferences = new FeedPreferences(0, true, username, password);
+ preferences = new FeedPreferences(0, true, FeedPreferences.AutoDeleteAction.GLOBAL, username, password);
+ }
+
+ public static Feed fromCursor(Cursor cursor) {
+ int indexId = cursor.getColumnIndex(PodDBAdapter.KEY_ID);
+ int indexLastUpdate = cursor.getColumnIndex(PodDBAdapter.KEY_LASTUPDATE);
+ int indexTitle = cursor.getColumnIndex(PodDBAdapter.KEY_TITLE);
+ int indexLink = cursor.getColumnIndex(PodDBAdapter.KEY_LINK);
+ int indexDescription = cursor.getColumnIndex(PodDBAdapter.KEY_DESCRIPTION);
+ int indexPaymentLink = cursor.getColumnIndex(PodDBAdapter.KEY_PAYMENT_LINK);
+ int indexAuthor = cursor.getColumnIndex(PodDBAdapter.KEY_AUTHOR);
+ int indexLanguage = cursor.getColumnIndex(PodDBAdapter.KEY_LANGUAGE);
+ int indexType = cursor.getColumnIndex(PodDBAdapter.KEY_TYPE);
+ int indexFeedIdentifier = cursor.getColumnIndex(PodDBAdapter.KEY_FEED_IDENTIFIER);
+ int indexFileUrl = cursor.getColumnIndex(PodDBAdapter.KEY_FILE_URL);
+ int indexDownloadUrl = cursor.getColumnIndex(PodDBAdapter.KEY_DOWNLOAD_URL);
+ int indexDownloaded = cursor.getColumnIndex(PodDBAdapter.KEY_DOWNLOADED);
+ int indexFlattrStatus = cursor.getColumnIndex(PodDBAdapter.KEY_FLATTR_STATUS);
+ int indexIsPaged = cursor.getColumnIndex(PodDBAdapter.KEY_IS_PAGED);
+ 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);
+
+ Date lastUpdate = new Date(cursor.getLong(indexLastUpdate));
+
+ Feed feed = new Feed(
+ cursor.getLong(indexId),
+ lastUpdate,
+ cursor.getString(indexTitle),
+ cursor.getString(indexLink),
+ cursor.getString(indexDescription),
+ cursor.getString(indexPaymentLink),
+ cursor.getString(indexAuthor),
+ cursor.getString(indexLanguage),
+ cursor.getString(indexType),
+ cursor.getString(indexFeedIdentifier),
+ null,
+ cursor.getString(indexFileUrl),
+ cursor.getString(indexDownloadUrl),
+ cursor.getInt(indexDownloaded) > 0,
+ new FlattrStatus(cursor.getLong(indexFlattrStatus)),
+ cursor.getInt(indexIsPaged) > 0,
+ cursor.getString(indexNextPageLink),
+ cursor.getString(indexHide),
+ cursor.getInt(indexLastUpdateFailed) > 0
+ );
+
+ FeedPreferences preferences = FeedPreferences.fromCursor(cursor);
+ feed.setPreferences(preferences);
+ return feed;
+ }
+
+
+ /**
+ * Returns true if at least one item in the itemlist is unread.
+ *
+ */
+ public boolean hasNewItems() {
+ for (FeedItem item : items) {
+ if (item.isNew()) {
+ return true;
+ }
+ }
+ return false;
}
-
/**
* Returns true if at least one item in the itemlist is unread.
*
*/
- public boolean hasNewItems() {
+ public boolean hasUnplayedItems() {
for (FeedItem item : items) {
- if (item.getState() == FeedItem.State.UNREAD) {
- if (item.getMedia() != null) {
- return true;
- }
+ if (false == item.isNew() && false == item.isPlayed()) {
+ return true;
}
}
return false;
@@ -230,7 +291,8 @@ public class Feed extends FeedFile implements FlattrThing, PicassoImageResource
}
public void updateFromOther(Feed other) {
- super.updateFromOther(other);
+ // don't update feed's download_url, we do that manually if redirected
+ // see AntennapodHttpClient
if (other.title != null) {
title = other.title;
}
@@ -304,7 +366,7 @@ public class Feed extends FeedFile implements FlattrThing, PicassoImageResource
if (other.isPaged() && !this.isPaged()) {
return true;
}
- if (!StringUtils.equals(other.getNextPageLink(), this.getNextPageLink())) {
+ if (!TextUtils.equals(other.getNextPageLink(), this.getNextPageLink())) {
return true;
}
return false;
@@ -433,7 +495,7 @@ public class Feed extends FeedFile implements FlattrThing, PicassoImageResource
}
public void savePreferences(Context context) {
- DBWriter.setFeedPreferences(context, preferences);
+ DBWriter.setFeedPreferences(preferences);
}
@Override
@@ -482,7 +544,7 @@ public class Feed extends FeedFile implements FlattrThing, PicassoImageResource
return itemfilter;
}
- public void setHiddenItemProperties(String[] properties) {
+ public void setItemFilter(String[] properties) {
if (properties != null) {
this.itemfilter = new FeedItemFilter(properties);
}
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
new file mode 100644
index 000000000..35abb8de6
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedFilter.java
@@ -0,0 +1,111 @@
+package de.danoeh.antennapod.core.feed;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class FeedFilter {
+
+ private static final String TAG = "FeedFilter";
+
+ private String includeFilter;
+ private String excludeFilter;
+
+ public FeedFilter() {
+ this("", "");
+ }
+
+ public FeedFilter(String includeFilter, String excludeFilter) {
+ // We're storing the strings and not the parsed terms because
+ // 1. It's easier to show the user exactly what they typed in this way
+ // (we don't have to recreate it)
+ // 2. We don't know if we'll actually be asked to parse anything anyways.
+ this.includeFilter = includeFilter;
+ this.excludeFilter = excludeFilter;
+ }
+
+ /**
+ * Parses the text in to a list of single words or quoted strings.
+ * Example: "One "Two Three"" returns ["One", "Two Three"]
+ * @param filter string to parse in to terms
+ * @return list of terms
+ */
+ private List<String> parseTerms(String filter) {
+ // from http://stackoverflow.com/questions/7804335/split-string-on-spaces-in-java-except-if-between-quotes-i-e-treat-hello-wor
+ List<String> list = new ArrayList<>();
+ Matcher m = Pattern.compile("([^\"]\\S*|\".+?\")\\s*").matcher(filter);
+ while (m.find())
+ list.add(m.group(1).replace("\"", ""));
+ return list;
+ }
+
+ /**
+ * @param item
+ * @return true if the item should be downloaded
+ */
+ public boolean shouldAutoDownload(FeedItem item) {
+
+ List<String> includeTerms = parseTerms(includeFilter);
+ List<String> excludeTerms = parseTerms(excludeFilter);
+
+ if (includeTerms.size() == 0 && excludeTerms.size() == 0) {
+ // nothing has been specified, so include everything
+ return true;
+ }
+
+ // check using lowercase so the users don't have to worry about case.
+ String title = item.getTitle().toLowerCase();
+
+ // if it's explicitly excluded, it shouldn't be autodownloaded
+ // even if it has include terms
+ for (String term : excludeTerms) {
+ if (title.contains(term.trim().toLowerCase())) {
+ return false;
+ }
+ }
+
+ for (String term : includeTerms) {
+ if (title.contains(term.trim().toLowerCase())) {
+ return true;
+ }
+ }
+
+ // now's the tricky bit
+ // if they haven't set an include filter, but they have set an exclude filter
+ // default to including, but if they've set both, then exclude
+ if (!hasIncludeFilter() && hasExcludeFilter()) {
+ return true;
+ }
+
+ return false;
+ }
+
+ public String getIncludeFilter() {
+ return includeFilter;
+ }
+
+ public String getExcludeFilter() { return excludeFilter; }
+
+ /**
+ * @return true if only include is set
+ */
+ public boolean includeOnly() {
+ return hasIncludeFilter() && !hasExcludeFilter();
+ }
+
+ /**
+ * @return true if only exclude is set
+ */
+ public boolean excludeOnly() {
+ return hasExcludeFilter() && !hasIncludeFilter();
+ }
+
+ public boolean hasIncludeFilter() {
+ return includeFilter.length() > 0;
+ }
+
+ public boolean hasExcludeFilter() {
+ return excludeFilter.length() > 0;
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedImage.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedImage.java
index c6f24367e..bd7ceb54f 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedImage.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedImage.java
@@ -1,13 +1,15 @@
package de.danoeh.antennapod.core.feed;
+import android.database.Cursor;
import android.net.Uri;
import java.io.File;
-import de.danoeh.antennapod.core.asynctask.PicassoImageResource;
+import de.danoeh.antennapod.core.asynctask.ImageResource;
+import de.danoeh.antennapod.core.storage.PodDBAdapter;
-public class FeedImage extends FeedFile implements PicassoImageResource {
+public class FeedImage extends FeedFile implements ImageResource {
public static final int FEEDFILETYPE_FEEDIMAGE = 1;
protected String title;
@@ -31,6 +33,23 @@ public class FeedImage extends FeedFile implements PicassoImageResource {
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) {
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 11348953e..d8c32f55e 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,17 +1,21 @@
package de.danoeh.antennapod.core.feed;
+import android.database.Cursor;
import android.net.Uri;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import java.util.Date;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
-import de.danoeh.antennapod.core.ClientConfig;
-import de.danoeh.antennapod.core.asynctask.PicassoImageResource;
+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;
import de.danoeh.antennapod.core.util.flattr.FlattrStatus;
import de.danoeh.antennapod.core.util.flattr.FlattrThing;
@@ -21,7 +25,12 @@ import de.danoeh.antennapod.core.util.flattr.FlattrThing;
*
* @author daniel
*/
-public class FeedItem extends FeedComponent implements ShownotesProvider, FlattrThing, PicassoImageResource {
+public class FeedItem extends FeedComponent implements ShownotesProvider, FlattrThing, ImageResource {
+
+ /** tag that indicates this item is in the queue */
+ public static final String TAG_QUEUE = "Queue";
+ /** tag that indicates this item is in favorites */
+ public static final String TAG_FAVORITE = "Favorite";
/**
* The id/guid that can be found in the rss/atom feed. Might not be set.
@@ -44,7 +53,11 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr
private Feed feed;
private long feedId;
- private boolean read;
+ private int state;
+ public final static int NEW = -1;
+ public final static int UNPLAYED = 0;
+ public final static int PLAYED = 1;
+
private String paymentLink;
private FlattrStatus flattrStatus;
@@ -63,10 +76,21 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr
private List<Chapter> chapters;
private FeedImage image;
- private boolean autoDownload = true;
+ /*
+ * 0: auto download disabled
+ * 1: auto download enabled (default)
+ * > 1: auto download enabled, (approx.) timestamp of the last failed attempt
+ * where last digit denotes the number of failed attempts
+ */
+ private long autoDownload = 1;
+
+ /**
+ * Any tags assigned to this item
+ */
+ private Set<String> tags = new HashSet<>();
public FeedItem() {
- this.read = true;
+ this.state = UNPLAYED;
this.flattrStatus = new FlattrStatus();
this.hasChapters = false;
}
@@ -75,8 +99,8 @@ 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, boolean read,
- String itemIdentifier, boolean autoDownload) {
+ FlattrStatus flattrStatus, boolean hasChapters, FeedImage image, int state,
+ String itemIdentifier, long autoDownload) {
this.id = id;
this.title = title;
this.link = link;
@@ -86,7 +110,7 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr
this.flattrStatus = flattrStatus;
this.hasChapters = hasChapters;
this.image = image;
- this.read = read;
+ this.state = state;
this.itemIdentifier = itemIdentifier;
this.autoDownload = autoDownload;
}
@@ -94,13 +118,13 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr
/**
* This constructor should be used for creating test objects.
*/
- public FeedItem(long id, String title, String itemIdentifier, String link, Date pubDate, boolean read, Feed feed) {
+ public FeedItem(long id, String title, String itemIdentifier, String link, Date pubDate, int state, Feed feed) {
this.id = id;
this.title = title;
this.itemIdentifier = itemIdentifier;
this.link = link;
this.pubDate = (pubDate != null) ? (Date) pubDate.clone() : null;
- this.read = read;
+ this.state = state;
this.feed = feed;
this.flattrStatus = new FlattrStatus();
this.hasChapters = false;
@@ -109,18 +133,49 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr
/**
* This constructor should be used for creating test objects involving chapter marks.
*/
- public FeedItem(long id, String title, String itemIdentifier, String link, Date pubDate, boolean read, Feed feed, boolean hasChapters) {
+ public FeedItem(long id, String title, String itemIdentifier, String link, Date pubDate, int state, Feed feed, boolean hasChapters) {
this.id = id;
this.title = title;
this.itemIdentifier = itemIdentifier;
this.link = link;
this.pubDate = (pubDate != null) ? (Date) pubDate.clone() : null;
- this.read = read;
+ this.state = state;
this.feed = feed;
this.flattrStatus = new FlattrStatus();
this.hasChapters = hasChapters;
}
+ public static FeedItem fromCursor(Cursor cursor) {
+ int indexId = cursor.getColumnIndex(PodDBAdapter.KEY_ID);
+ int indexTitle = cursor.getColumnIndex(PodDBAdapter.KEY_TITLE);
+ int indexLink = cursor.getColumnIndex(PodDBAdapter.KEY_LINK);
+ int indexPubDate = cursor.getColumnIndex(PodDBAdapter.KEY_PUBDATE);
+ int indexPaymentLink = cursor.getColumnIndex(PodDBAdapter.KEY_PAYMENT_LINK);
+ int indexFeedId = cursor.getColumnIndex(PodDBAdapter.KEY_FEED);
+ int indexFlattrStatus = cursor.getColumnIndex(PodDBAdapter.KEY_FLATTR_STATUS);
+ int indexHasChapters = cursor.getColumnIndex(PodDBAdapter.KEY_HAS_CHAPTERS);
+ int indexRead = cursor.getColumnIndex(PodDBAdapter.KEY_READ);
+ int indexItemIdentifier = cursor.getColumnIndex(PodDBAdapter.KEY_ITEM_IDENTIFIER);
+ int indexAutoDownload = cursor.getColumnIndex(PodDBAdapter.KEY_AUTO_DOWNLOAD);
+
+ 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));
+ String paymentLink = cursor.getString(indexPaymentLink);
+ long feedId = cursor.getLong(indexFeedId);
+ boolean hasChapters = cursor.getInt(indexHasChapters) > 0;
+ FlattrStatus flattrStatus = new FlattrStatus(cursor.getLong(indexFlattrStatus));
+ int state = cursor.getInt(indexRead);
+ String itemIdentifier = cursor.getString(indexItemIdentifier);
+ long autoDownload = cursor.getLong(indexAutoDownload);
+
+ FeedItem item = new FeedItem(id, title, link, pubDate, paymentLink, feedId, flattrStatus,
+ hasChapters, null, state, itemIdentifier, autoDownload);
+ return item;
+ }
+
public void updateFromOther(FeedItem other) {
super.updateFromOther(other);
if (other.title != null) {
@@ -238,12 +293,25 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr
this.feed = feed;
}
- public boolean isRead() {
- return read;
+ public boolean isNew() {
+ return state == NEW;
}
- public void setRead(boolean read) {
- this.read = read;
+
+ public void setNew() {
+ state = NEW;
+ }
+
+ public boolean isPlayed() {
+ return state == PLAYED;
+ }
+
+ public void setPlayed(boolean played) {
+ if(played) {
+ state = PLAYED;
+ } else {
+ state = UNPLAYED;
+ }
}
private boolean isInProgress() {
@@ -257,11 +325,7 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr
public void setContentEncoded(String contentEncoded) {
this.contentEncoded = contentEncoded;
}
-
- public void setFlattrStatus(FlattrStatus status) {
- this.flattrStatus = status;
- }
-
+
public FlattrStatus getFlattrStatus() {
return flattrStatus;
}
@@ -308,7 +372,7 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr
public String call() throws Exception {
if (contentEncoded == null || description == null) {
- DBReader.loadExtraInformationOfFeedItem(ClientConfig.applicationCallbacks.getApplicationInstance(), FeedItem.this);
+ DBReader.loadExtraInformationOfFeedItem(FeedItem.this);
}
return (contentEncoded != null) ? contentEncoded : description;
@@ -320,7 +384,7 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr
public Uri getImageUri() {
if(media != null && media.hasEmbeddedPicture()) {
return media.getImageUri();
- } else if (hasItemImageDownloaded()) {
+ } else if (image != null) {
return image.getImageUri();
} else if (feed != null) {
return feed.getImageUri();
@@ -342,7 +406,7 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr
return State.IN_PROGRESS;
}
}
- return (isRead() ? State.READ : State.UNREAD);
+ return (isPlayed() ? State.READ : State.UNREAD);
}
public long getFeedId() {
@@ -392,21 +456,54 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr
}
public void setAutoDownload(boolean autoDownload) {
- this.autoDownload = autoDownload;
+ this.autoDownload = autoDownload ? 1 : 0;
}
public boolean getAutoDownload() {
- return this.autoDownload;
+ return this.autoDownload > 0;
+ }
+
+ public int getFailedAutoDownloadAttempts() {
+ if (autoDownload <= 1) {
+ return 0;
+ }
+ int failedAttempts = (int)(autoDownload % 10);
+ if (failedAttempts == 0) {
+ failedAttempts = 10;
+ }
+ return failedAttempts;
}
public boolean isAutoDownloadable() {
- return this.hasMedia() &&
- false == this.getMedia().isPlaying() &&
- false == this.getMedia().isDownloaded() &&
- false == this.isRead() &&
- this.getAutoDownload();
+ if (media == null || media.isPlaying() || media.isDownloaded() || autoDownload == 0) {
+ return false;
+ }
+ if (autoDownload == 1) {
+ return true;
+ }
+ int failedAttempts = getFailedAutoDownloadAttempts();
+ double magicValue = 1.767; // 1.767^(10[=#maxNumAttempts]-1) = 168 hours / 7 days
+ int millisecondsInHour = 3600000;
+ long waitingTime = (long) (Math.pow(magicValue, failedAttempts - 1) * millisecondsInHour);
+ long grace = TimeUnit.MINUTES.toMillis(5);
+ return System.currentTimeMillis() > (autoDownload + waitingTime - grace);
}
+ /**
+ * @return true if the item has this tag
+ */
+ public boolean isTagged(String tag) { return tags.contains(tag); }
+
+ /**
+ * @param tag adds this tag to the item. NOTE: does NOT persist to the database
+ */
+ public void addTag(String tag) { tags.add(tag); }
+
+ /**
+ * @param tag the to remove
+ */
+ public void removeTag(String tag) { tags.remove(tag); }
+
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilter.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilter.java
index 4ad084b39..fdde4b34c 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
@@ -1,8 +1,6 @@
package de.danoeh.antennapod.core.feed;
-import android.content.Context;
-
-import org.apache.commons.lang3.StringUtils;
+import android.text.TextUtils;
import java.util.ArrayList;
import java.util.List;
@@ -10,73 +8,87 @@ import java.util.List;
import de.danoeh.antennapod.core.storage.DBReader;
public class FeedItemFilter {
+ private final String[] mProperties;
- private final String[] properties;
-
- private boolean hideUnplayed = false;
- private boolean hidePaused = false;
- private boolean hidePlayed = false;
- private boolean hideQueued = false;
- private boolean hideNotQueued = false;
- private boolean hideDownloaded = false;
- private boolean hideNotDownloaded = false;
+ private boolean showPlayed = false;
+ private boolean showUnplayed = false;
+ private boolean showPaused = false;
+ private boolean showQueued = false;
+ private boolean showNotQueued = false;
+ private boolean showDownloaded = false;
+ private boolean showNotDownloaded = false;
public FeedItemFilter(String properties) {
- this(StringUtils.split(properties, ','));
+ this(TextUtils.split(properties, ","));
}
public FeedItemFilter(String[] properties) {
- this.properties = properties;
+ this.mProperties = properties;
for(String property : properties) {
// see R.arrays.feed_filter_values
switch(property) {
case "unplayed":
- hideUnplayed = true;
+ showUnplayed = true;
break;
case "paused":
- hidePaused = true;
+ showPaused = true;
break;
case "played":
- hidePlayed = true;
+ showPlayed = true;
break;
case "queued":
- hideQueued = true;
+ showQueued = true;
break;
case "not_queued":
- hideNotQueued = true;
+ showNotQueued = true;
break;
case "downloaded":
- hideDownloaded = true;
+ showDownloaded = true;
break;
case "not_downloaded":
- hideNotDownloaded = true;
+ showNotDownloaded = true;
break;
}
}
}
- public List<FeedItem> filter(Context context, List<FeedItem> items) {
- if(properties.length == 0) {
- return items;
- }
- List<FeedItem> result = new ArrayList<FeedItem>();
+ /**
+ * Run a list of feed items through the filter.
+ */
+ public List<FeedItem> filter(List<FeedItem> items) {
+ if(mProperties.length == 0) return items;
+
+ List<FeedItem> result = new ArrayList<>();
+
+ // Check for filter combinations that will always return an empty list
+ // (e.g. requiring played and unplayed at the same time)
+ if (showPlayed && showUnplayed) return result;
+ if (showQueued && showNotQueued) return result;
+ if (showDownloaded && showNotDownloaded) return result;
+
for(FeedItem item : items) {
- if(hideUnplayed && false == item.isRead()) continue;
- if(hidePaused && item.getState() == FeedItem.State.IN_PROGRESS) continue;
- if(hidePlayed && item.isRead()) continue;
- boolean isQueued = DBReader.getQueueIDList(context).contains(item.getId());
- if(hideQueued && isQueued) continue;
- if(hideNotQueued && false == isQueued) continue;
- boolean isDownloaded = item.getMedia() != null && item.getMedia().isDownloaded();
- if(hideDownloaded && isDownloaded) continue;
- if(hideNotDownloaded && false == isDownloaded) continue;
+ // If the item does not meet a requirement, skip it.
+ if (showPlayed && !item.isPlayed()) continue;
+ if (showUnplayed && item.isPlayed()) continue;
+ if (showPaused && item.getState() != FeedItem.State.IN_PROGRESS) continue;
+
+ boolean queued = DBReader.getQueueIDList().contains(item.getId());
+ if (showQueued && !queued) continue;
+ if (showNotQueued && queued) continue;
+
+ boolean downloaded = item.getMedia() != null && item.getMedia().isDownloaded();
+ if (showDownloaded && !downloaded) continue;
+ if (showNotDownloaded && downloaded) continue;
+
+ // If the item reaches here, it meets all criteria
result.add(item);
}
+
return result;
}
public String[] getValues() {
- return properties.clone();
+ return mProperties.clone();
}
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java
index f875eb812..56b996d1c 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java
@@ -2,20 +2,23 @@ package de.danoeh.antennapod.core.feed;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
+import android.database.Cursor;
import android.media.MediaMetadataRetriever;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
import java.util.Date;
import java.util.List;
import java.util.concurrent.Callable;
-import de.danoeh.antennapod.core.ClientConfig;
import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBWriter;
+import de.danoeh.antennapod.core.storage.PodDBAdapter;
import de.danoeh.antennapod.core.util.ChapterUtils;
import de.danoeh.antennapod.core.util.playback.Playable;
@@ -28,14 +31,26 @@ public class FeedMedia extends FeedFile implements Playable {
public static final String PREF_MEDIA_ID = "FeedMedia.PrefMediaId";
public static final String PREF_FEED_ID = "FeedMedia.PrefFeedId";
+ /**
+ * Indicates we've checked on the size of the item via the network
+ * and got an invalid response. Using Integer.MIN_VALUE because
+ * 1) we'll still check on it in case it gets downloaded (it's <= 0)
+ * 2) By default all FeedMedia have a size of 0 if we don't know it,
+ * so this won't conflict with existing practice.
+ */
+ private static final int CHECKED_ON_SIZE_BUT_UNKNOWN = Integer.MIN_VALUE;
+
private int duration;
private int position; // Current position in file
+ private long lastPlayedTime; // Last time this media was played (in ms)
private int played_duration; // How many ms of this file have been played (for autoflattring)
private long size; // File size in Byte
private String mime_type;
- private volatile FeedItem item;
+ @Nullable private volatile FeedItem item;
private Date playbackCompletionDate;
- private boolean hasEmbeddedPicture;
+
+ // if null: unknown, will be checked
+ private Boolean hasEmbeddedPicture;
/* Used for loading item when restoring from parcel. */
private long itemID;
@@ -50,9 +65,9 @@ public class FeedMedia extends FeedFile implements Playable {
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 downloaded, Date playbackCompletionDate, int played_duration,
+ long lastPlayedTime) {
super(file_url, download_url, downloaded);
- checkEmbeddedPicture();
this.id = id;
this.item = item;
this.duration = duration;
@@ -62,8 +77,69 @@ public class FeedMedia extends FeedFile implements Playable {
this.mime_type = mime_type;
this.playbackCompletionDate = playbackCompletionDate == null
? null : (Date) playbackCompletionDate.clone();
+ 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) {
+ this(id, item, duration, position, size, mime_type, file_url, download_url, downloaded,
+ playbackCompletionDate, played_duration, lastPlayedTime);
+ this.hasEmbeddedPicture = hasEmbeddedPicture;
+ }
+
+ public static FeedMedia fromCursor(Cursor cursor) {
+ int indexId = cursor.getColumnIndex(PodDBAdapter.KEY_ID);
+ int indexPlaybackCompletionDate = cursor.getColumnIndex(PodDBAdapter.KEY_PLAYBACK_COMPLETION_DATE);
+ int indexDuration = cursor.getColumnIndex(PodDBAdapter.KEY_DURATION);
+ int indexPosition = cursor.getColumnIndex(PodDBAdapter.KEY_POSITION);
+ int indexSize = cursor.getColumnIndex(PodDBAdapter.KEY_SIZE);
+ int indexMimeType = cursor.getColumnIndex(PodDBAdapter.KEY_MIME_TYPE);
+ int indexFileUrl = cursor.getColumnIndex(PodDBAdapter.KEY_FILE_URL);
+ int indexDownloadUrl = cursor.getColumnIndex(PodDBAdapter.KEY_DOWNLOAD_URL);
+ int indexDownloaded = cursor.getColumnIndex(PodDBAdapter.KEY_DOWNLOADED);
+ int indexPlayedDuration = cursor.getColumnIndex(PodDBAdapter.KEY_PLAYED_DURATION);
+ int indexLastPlayedTime = cursor.getColumnIndex(PodDBAdapter.KEY_LAST_PLAYED_TIME);
+
+ long mediaId = cursor.getLong(indexId);
+ Date playbackCompletionDate = null;
+ long playbackCompletionTime = cursor.getLong(indexPlaybackCompletionDate);
+ if (playbackCompletionTime > 0) {
+ playbackCompletionDate = new Date(playbackCompletionTime);
+ }
+
+ Boolean hasEmbeddedPicture;
+ switch(cursor.getInt(cursor.getColumnIndex(PodDBAdapter.KEY_HAS_EMBEDDED_PICTURE))) {
+ case 1:
+ hasEmbeddedPicture = Boolean.TRUE;
+ break;
+ case 0:
+ hasEmbeddedPicture = Boolean.FALSE;
+ break;
+ default:
+ hasEmbeddedPicture = null;
+ break;
+ }
+
+ return new FeedMedia(
+ mediaId,
+ null,
+ cursor.getInt(indexDuration),
+ cursor.getInt(indexPosition),
+ cursor.getLong(indexSize),
+ cursor.getString(indexMimeType),
+ cursor.getString(indexFileUrl),
+ cursor.getString(indexDownloadUrl),
+ cursor.getInt(indexDownloaded) > 0,
+ playbackCompletionDate,
+ cursor.getInt(indexPlayedDuration),
+ hasEmbeddedPicture,
+ cursor.getLong(indexLastPlayedTime)
+ );
+ }
+
+
@Override
public String getHumanReadableIdentifier() {
if (item != null && item.getTitle() != null) {
@@ -92,6 +168,10 @@ public class FeedMedia extends FeedFile implements Playable {
}
public void updateFromOther(FeedMedia other) {
+ // reset to new if feed item did link to a file before
+ if(TextUtils.isEmpty(download_url) && !TextUtils.isEmpty(other.download_url)) {
+ item.setNew();
+ }
super.updateFromOther(other);
if (other.size > 0) {
size = other.size;
@@ -162,6 +242,11 @@ public class FeedMedia extends FeedFile implements Playable {
this.duration = duration;
}
+ @Override
+ public void setLastPlayedTime(long lastPlayedTime) {
+ this.lastPlayedTime = lastPlayedTime;
+ }
+
public int getPlayedDuration() {
return played_duration;
}
@@ -174,8 +259,16 @@ public class FeedMedia extends FeedFile implements Playable {
return position;
}
+ @Override
+ public long getLastPlayedTime() {
+ return lastPlayedTime;
+ }
+
public void setPosition(int position) {
this.position = position;
+ if(position > 0 && item != null && item.isNew()) {
+ this.item.setPlayed(false);
+ }
}
public long getSize() {
@@ -186,6 +279,18 @@ public class FeedMedia extends FeedFile implements Playable {
this.size = size;
}
+ /**
+ * Indicates we asked the service what the size was, but didn't
+ * get a valid answer and we shoudln't check using the network again.
+ */
+ public void setCheckedOnSizeButUnknown() {
+ this.size = CHECKED_ON_SIZE_BUT_UNKNOWN;
+ }
+
+ public boolean checkedOnSizeButUnknown() {
+ return (CHECKED_ON_SIZE_BUT_UNKNOWN == this.size);
+ }
+
public String getMime_type() {
return mime_type;
}
@@ -194,6 +299,7 @@ public class FeedMedia extends FeedFile implements Playable {
this.mime_type = mime_type;
}
+ @Nullable
public FeedItem getItem() {
return item;
}
@@ -230,13 +336,18 @@ public class FeedMedia extends FeedFile implements Playable {
}
public boolean hasEmbeddedPicture() {
- return this.hasEmbeddedPicture;
+ return false;
+ // TODO: reenable!
+ //if(hasEmbeddedPicture == null) {
+ // checkEmbeddedPicture();
+ //}
+ //return hasEmbeddedPicture;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeLong(id);
- dest.writeLong(item.getId());
+ dest.writeLong(item != null ? item.getId() : 0L);
dest.writeInt(duration);
dest.writeInt(position);
@@ -247,33 +358,38 @@ public class FeedMedia extends FeedFile implements Playable {
dest.writeByte((byte) ((downloaded) ? 1 : 0));
dest.writeLong((playbackCompletionDate != null) ? playbackCompletionDate.getTime() : 0);
dest.writeInt(played_duration);
+ dest.writeLong(lastPlayedTime);
}
@Override
public void writeToPreferences(Editor prefEditor) {
- prefEditor.putLong(PREF_FEED_ID, item.getFeed().getId());
+ if(item != null && item.getFeed() != null) {
+ prefEditor.putLong(PREF_FEED_ID, item.getFeed().getId());
+ } else {
+ prefEditor.putLong(PREF_FEED_ID, 0L);
+ }
prefEditor.putLong(PREF_MEDIA_ID, id);
}
@Override
public void loadMetadata() throws PlayableException {
if (item == null && itemID != 0) {
- item = DBReader.getFeedItem(ClientConfig.applicationCallbacks.getApplicationInstance(), itemID);
+ item = DBReader.getFeedItem(itemID);
}
}
@Override
public void loadChapterMarks() {
if (item == null && itemID != 0) {
- item = DBReader.getFeedItem(ClientConfig.applicationCallbacks.getApplicationInstance(), itemID);
+ item = DBReader.getFeedItem(itemID);
}
// check if chapters are stored in db and not loaded yet.
if (item != null && item.hasChapters() && item.getChapters() == null) {
- DBReader.loadChaptersOfFeedItem(ClientConfig.applicationCallbacks.getApplicationInstance(), item);
+ DBReader.loadChaptersOfFeedItem(item);
} else if (item != null && item.getChapters() == null && !localFileAvailable()) {
ChapterUtils.loadChaptersFromStreamUrl(this);
if (getChapters() != null && item != null) {
- DBWriter.setFeedItem(ClientConfig.applicationCallbacks.getApplicationInstance(),
+ DBWriter.setFeedItem(
item);
}
}
@@ -349,15 +465,18 @@ public class FeedMedia extends FeedFile implements Playable {
}
@Override
- public void saveCurrentPosition(SharedPreferences pref, int newPosition) {
+ public void saveCurrentPosition(SharedPreferences pref, int newPosition, long timeStamp) {
+ if(item != null && item.isNew()) {
+ DBWriter.markItemPlayed(FeedItem.UNPLAYED, item.getId());
+ }
setPosition(newPosition);
- DBWriter.setFeedMediaPlaybackInformation(ClientConfig.applicationCallbacks.getApplicationInstance(), this);
+ setLastPlayedTime(timeStamp);
+ DBWriter.setFeedMediaPlaybackInformation(this);
}
@Override
public void onPlaybackStart() {
}
-
@Override
public void onPlaybackCompleted() {
@@ -380,11 +499,11 @@ public class FeedMedia extends FeedFile implements Playable {
public String call() throws Exception {
if (item == null) {
item = DBReader.getFeedItem(
- ClientConfig.applicationCallbacks.getApplicationInstance(), itemID);
+ itemID);
}
if (item.getContentEncoded() == null || item.getDescription() == null) {
DBReader.loadExtraInformationOfFeedItem(
- ClientConfig.applicationCallbacks.getApplicationInstance(), item);
+ item);
}
return (item.getContentEncoded() != null) ? item.getContentEncoded() : item.getDescription();
@@ -397,7 +516,7 @@ public class FeedMedia extends FeedFile implements Playable {
final long id = in.readLong();
final long itemID = in.readLong();
FeedMedia result = new FeedMedia(id, null, in.readInt(), in.readInt(), in.readLong(), in.readString(), in.readString(),
- in.readString(), in.readByte() != 0, new Date(in.readLong()), in.readInt());
+ in.readString(), in.readByte() != 0, new Date(in.readLong()), in.readInt(), in.readLong());
result.itemID = itemID;
return result;
}
@@ -409,30 +528,44 @@ public class FeedMedia extends FeedFile implements Playable {
@Override
public Uri getImageUri() {
- if (hasEmbeddedPicture) {
+ if (hasEmbeddedPicture()) {
Uri.Builder builder = new Uri.Builder();
builder.scheme(SCHEME_MEDIA).encodedPath(getLocalMediaUrl());
+
+ if (item != null && item.getFeed() != null) {
+ final Uri feedImgUri = item.getFeed().getImageUri();
+ if (feedImgUri != null) {
+ builder.appendQueryParameter(PARAM_FALLBACK, feedImgUri.toString());
+ }
+ }
return builder.build();
- } else {
+ } else if(item != null) {
return item.getImageUri();
+ } else {
+ return null;
}
}
+ public void setHasEmbeddedPicture(Boolean hasEmbeddedPicture) {
+ this.hasEmbeddedPicture = hasEmbeddedPicture;
+ }
+
@Override
public void setDownloaded(boolean downloaded) {
super.setDownloaded(downloaded);
- checkEmbeddedPicture();
+ if(item != null && downloaded) {
+ item.setPlayed(false);
+ }
}
@Override
public void setFile_url(String file_url) {
super.setFile_url(file_url);
- checkEmbeddedPicture();
}
private void checkEmbeddedPicture() {
if (!localFileAvailable()) {
- hasEmbeddedPicture = false;
+ hasEmbeddedPicture = Boolean.FALSE;
return;
}
MediaMetadataRetriever mmr = new MediaMetadataRetriever();
@@ -440,14 +573,13 @@ public class FeedMedia extends FeedFile implements Playable {
mmr.setDataSource(getLocalMediaUrl());
byte[] image = mmr.getEmbeddedPicture();
if(image != null) {
- hasEmbeddedPicture = true;
- }
- else {
- hasEmbeddedPicture = false;
+ hasEmbeddedPicture = Boolean.TRUE;
+ } else {
+ hasEmbeddedPicture = Boolean.FALSE;
}
} catch (Exception e) {
e.printStackTrace();
- hasEmbeddedPicture = false;
+ hasEmbeddedPicture = Boolean.FALSE;
}
}
}
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 2f0304182..faf23a37a 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
@@ -1,29 +1,95 @@
package de.danoeh.antennapod.core.feed;
import android.content.Context;
+import android.database.Cursor;
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
+
+import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.storage.DBWriter;
-import org.apache.commons.lang3.StringUtils;
+import de.danoeh.antennapod.core.storage.PodDBAdapter;
/**
* Contains preferences for a single feed.
*/
public class FeedPreferences {
+ @NonNull
+ private FeedFilter filter;
private long feedID;
private boolean autoDownload;
+ private boolean keepUpdated;
+
+ public enum AutoDeleteAction {
+ GLOBAL,
+ YES,
+ NO
+ }
+ private AutoDeleteAction auto_delete_action;
private String username;
private String password;
- public FeedPreferences(long feedID, boolean autoDownload, String username, String password) {
+ public FeedPreferences(long feedID, boolean autoDownload, AutoDeleteAction auto_delete_action, String username, String password) {
+ 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) {
this.feedID = feedID;
this.autoDownload = autoDownload;
+ this.keepUpdated = keepUpdated;
+ this.auto_delete_action = auto_delete_action;
this.username = username;
this.password = password;
+ this.filter = filter;
+ }
+
+ public static FeedPreferences fromCursor(Cursor cursor) {
+ int indexId = cursor.getColumnIndex(PodDBAdapter.KEY_ID);
+ int indexAutoDownload = cursor.getColumnIndex(PodDBAdapter.KEY_AUTO_DOWNLOAD);
+ int indexAutoRefresh = cursor.getColumnIndex(PodDBAdapter.KEY_KEEP_UPDATED);
+ int indexAutoDeleteAction = cursor.getColumnIndex(PodDBAdapter.KEY_AUTO_DELETE_ACTION);
+ int indexUsername = cursor.getColumnIndex(PodDBAdapter.KEY_USERNAME);
+ int indexPassword = cursor.getColumnIndex(PodDBAdapter.KEY_PASSWORD);
+ int indexIncludeFilter = cursor.getColumnIndex(PodDBAdapter.KEY_INCLUDE_FILTER);
+ int indexExcludeFilter = cursor.getColumnIndex(PodDBAdapter.KEY_EXCLUDE_FILTER);
+
+ long feedId = cursor.getLong(indexId);
+ boolean autoDownload = cursor.getInt(indexAutoDownload) > 0;
+ boolean autoRefresh = cursor.getInt(indexAutoRefresh) > 0;
+ int autoDeleteActionIndex = cursor.getInt(indexAutoDeleteAction);
+ AutoDeleteAction autoDeleteAction = AutoDeleteAction.values()[autoDeleteActionIndex];
+ String username = cursor.getString(indexUsername);
+ String password = cursor.getString(indexPassword);
+ String includeFilter = cursor.getString(indexIncludeFilter);
+ String excludeFilter = cursor.getString(indexExcludeFilter);
+ return new FeedPreferences(feedId, autoDownload, autoRefresh, autoDeleteAction, username, password, new FeedFilter(includeFilter, excludeFilter));
}
+ /**
+ * @return the filter for this feed
+ */
+ public FeedFilter getFilter() {
+ return filter;
+ }
+
+ public void setFilter(@NonNull FeedFilter filter) {
+ this.filter = filter;
+ }
/**
- * Compare another FeedPreferences with this one. The feedID and autoDownload attribute are excluded from the
+ * @return true if this feed should be refreshed when everything else is being refreshed
+ * if false the feed should only be refreshed if requested directly.
+ */
+ public boolean getKeepUpdated() {
+ return keepUpdated;
+ }
+
+ public void setKeepUpdated(boolean keepUpdated) {
+ this.keepUpdated = keepUpdated;
+ }
+
+ /**
+ * Compare another FeedPreferences with this one. The feedID, autoDownload and AutoDeleteAction attribute are excluded from the
* comparison.
*
* @return True if the two objects are different.
@@ -31,17 +97,17 @@ public class FeedPreferences {
public boolean compareWithOther(FeedPreferences other) {
if (other == null)
return true;
- if (!StringUtils.equals(username, other.username)) {
+ if (!TextUtils.equals(username, other.username)) {
return true;
}
- if (!StringUtils.equals(password, other.password)) {
+ if (!TextUtils.equals(password, other.password)) {
return true;
}
return false;
}
/**
- * Update this FeedPreferences object from another one. The feedID and autoDownload attributes are excluded
+ * Update this FeedPreferences object from another one. The feedID, autoDownload and AutoDeleteAction attributes are excluded
* from the update.
*/
public void updateFromOther(FeedPreferences other) {
@@ -67,8 +133,30 @@ public class FeedPreferences {
this.autoDownload = autoDownload;
}
+ public AutoDeleteAction getAutoDeleteAction() {
+ return auto_delete_action;
+ }
+
+ public void setAutoDeleteAction(AutoDeleteAction auto_delete_action) {
+ this.auto_delete_action = auto_delete_action;
+ }
+
+ public boolean getCurrentAutoDelete() {
+ switch (auto_delete_action) {
+ case GLOBAL:
+ return UserPreferences.isAutoDelete();
+
+ case YES:
+ return true;
+
+ case NO:
+ return false;
+ }
+ return false; // TODO - add exceptions here
+ }
+
public void save(Context context) {
- DBWriter.setFeedPreferences(context, this);
+ DBWriter.setFeedPreferences(this);
}
public String getUsername() {
diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/QueueEvent.java b/core/src/main/java/de/danoeh/antennapod/core/feed/QueueEvent.java
deleted file mode 100644
index c8497f509..000000000
--- a/core/src/main/java/de/danoeh/antennapod/core/feed/QueueEvent.java
+++ /dev/null
@@ -1,51 +0,0 @@
-package de.danoeh.antennapod.core.feed;
-
-import org.apache.commons.lang3.builder.ToStringBuilder;
-import org.apache.commons.lang3.builder.ToStringStyle;
-
-import java.util.List;
-
-public class QueueEvent {
-
- public enum Action {
- ADDED, ADDED_ITEMS, REMOVED, CLEARED, DELETED_MEDIA, SORTED, MOVED
- }
-
- public final Action action;
- public final FeedItem item;
- public final int position;
- public final List<FeedItem> items;
-
- public QueueEvent(Action action) {
- this(action, null, null, -1);
- }
-
- public QueueEvent(Action action, FeedItem item) {
- this(action, item, null, -1);
- }
-
- public QueueEvent(Action action, FeedItem item, int position) {
- this(action, item, null, position);
- }
-
- public QueueEvent(Action action, List<FeedItem> items) {
- this(action, null, items, -1);
- }
-
- private QueueEvent(Action action, FeedItem item, List<FeedItem> items, int position) {
- this.action = action;
- this.item = item;
- this.items = items;
- this.position = position;
- }
-
- @Override
- public String toString() {
- return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
- .append("action", action)
- .append("item", item)
- .append("items", items)
- .append("position", position)
- .toString();
- }
-}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/glide/ApGlideModule.java b/core/src/main/java/de/danoeh/antennapod/core/glide/ApGlideModule.java
new file mode 100644
index 000000000..0baff9723
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/glide/ApGlideModule.java
@@ -0,0 +1,33 @@
+package de.danoeh.antennapod.core.glide;
+
+import android.content.Context;
+
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.GlideBuilder;
+import com.bumptech.glide.load.DecodeFormat;
+import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory;
+import com.bumptech.glide.load.model.GlideUrl;
+import com.bumptech.glide.module.GlideModule;
+
+import java.io.InputStream;
+
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+
+/**
+ * {@see com.bumptech.glide.integration.okhttp.OkHttpGlideModule}
+ */
+public class ApGlideModule implements GlideModule {
+
+ @Override
+ public void applyOptions(Context context, GlideBuilder builder) {
+ builder.setDecodeFormat(DecodeFormat.PREFER_ARGB_8888);
+ builder.setDiskCache(new InternalCacheDiskCacheFactory(context,
+ UserPreferences.getImageCacheSize()));
+ }
+
+ @Override
+ public void registerComponents(Context context, Glide glide) {
+ glide.register(GlideUrl.class, InputStream.class, new ApOkHttpUrlLoader.Factory());
+ }
+
+} \ No newline at end of file
diff --git a/core/src/main/java/de/danoeh/antennapod/core/glide/ApGlideSettings.java b/core/src/main/java/de/danoeh/antennapod/core/glide/ApGlideSettings.java
new file mode 100644
index 000000000..fc1acd0e1
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/glide/ApGlideSettings.java
@@ -0,0 +1,11 @@
+package de.danoeh.antennapod.core.glide;
+
+import com.bumptech.glide.load.engine.DiskCacheStrategy;
+
+/**
+ * The settings that AntennaPod will use for various Glide options
+ */
+public class ApGlideSettings {
+
+ public static final DiskCacheStrategy AP_DISK_CACHE_STRATEGY = DiskCacheStrategy.ALL;
+}
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
new file mode 100644
index 000000000..86baa459c
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/glide/ApOkHttpUrlLoader.java
@@ -0,0 +1,141 @@
+package de.danoeh.antennapod.core.glide;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.bumptech.glide.integration.okhttp.OkHttpStreamFetcher;
+import com.bumptech.glide.load.data.DataFetcher;
+import com.bumptech.glide.load.model.GenericLoaderFactory;
+import com.bumptech.glide.load.model.GlideUrl;
+import com.bumptech.glide.load.model.ModelLoader;
+import com.bumptech.glide.load.model.ModelLoaderFactory;
+import com.squareup.okhttp.Interceptor;
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Response;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+
+import de.danoeh.antennapod.core.ClientConfig;
+import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
+import de.danoeh.antennapod.core.service.download.HttpDownloader;
+import de.danoeh.antennapod.core.storage.DBReader;
+import de.danoeh.antennapod.core.util.NetworkUtils;
+
+/**
+ * @see com.bumptech.glide.integration.okhttp.OkHttpUrlLoader
+ */
+public class ApOkHttpUrlLoader implements ModelLoader<GlideUrl, InputStream> {
+
+ private static final String TAG = ApOkHttpUrlLoader.class.getSimpleName();
+
+ /**
+ * The default factory for {@link ApOkHttpUrlLoader}s.
+ */
+ public static class Factory implements ModelLoaderFactory<GlideUrl, InputStream> {
+
+ private static volatile OkHttpClient internalClient;
+ private OkHttpClient client;
+
+ private static OkHttpClient getInternalClient() {
+ if (internalClient == null) {
+ synchronized (Factory.class) {
+ if (internalClient == null) {
+ internalClient = AntennapodHttpClient.newHttpClient();
+ internalClient.interceptors().add(new NetworkAllowanceInterceptor());
+ internalClient.interceptors().add(new BasicAuthenticationInterceptor());
+ }
+ }
+ }
+ return internalClient;
+ }
+
+ /**
+ * Constructor for a new Factory that runs requests using a static singleton client.
+ */
+ public Factory() {
+ this(getInternalClient());
+ }
+
+ /**
+ * Constructor for a new Factory that runs requests using given client.
+ */
+ public Factory(OkHttpClient client) {
+ this.client = client;
+ }
+
+ @Override
+ public ModelLoader<GlideUrl, InputStream> build(Context context, GenericLoaderFactory factories) {
+ return new ApOkHttpUrlLoader(client);
+ }
+
+ @Override
+ public void teardown() {
+ // Do nothing, this instance doesn't own the client.
+ }
+ }
+
+ private final OkHttpClient client;
+
+ public ApOkHttpUrlLoader(OkHttpClient client) {
+ this.client = client;
+ }
+
+ @Override
+ public DataFetcher<InputStream> getResourceFetcher(GlideUrl model, int width, int height) {
+ return new OkHttpStreamFetcher(client, model);
+ }
+
+ private static class NetworkAllowanceInterceptor implements Interceptor {
+
+ @Override
+ public Response intercept(Chain chain) throws IOException {
+ if (NetworkUtils.isDownloadAllowed()) {
+ return chain.proceed(chain.request());
+ } else {
+ return null;
+ }
+ }
+
+ }
+
+ private static class BasicAuthenticationInterceptor implements Interceptor {
+
+ @Override
+ public Response intercept(Chain chain) throws IOException {
+ com.squareup.okhttp.Request request = chain.request();
+ String url = request.urlString();
+ Context context = ClientConfig.applicationCallbacks.getApplicationInstance();
+ String authentication = DBReader.getImageAuthentication(url);
+
+ if(TextUtils.isEmpty(authentication)) {
+ Log.d(TAG, "no credentials for '" + url + "'");
+ return chain.proceed(request);
+ }
+
+ // add authentication
+ String[] auth = authentication.split(":");
+ String credentials = HttpDownloader.encodeCredentials(auth[0], auth[1], "ISO-8859-1");
+ com.squareup.okhttp.Request newRequest = request
+ .newBuilder()
+ .addHeader("Authorization", credentials)
+ .build();
+ Log.d(TAG, "Basic authentication with ISO-8859-1 encoding");
+ Response response = chain.proceed(newRequest);
+ if (!response.isSuccessful() && response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {
+ credentials = HttpDownloader.encodeCredentials(auth[0], auth[1], "UTF-8");
+ newRequest = request
+ .newBuilder()
+ .addHeader("Authorization", credentials)
+ .build();
+ Log.d(TAG, "Basic authentication with UTF-8 encoding");
+ return chain.proceed(newRequest);
+ } else {
+ return response;
+ }
+ }
+ }
+
+} \ No newline at end of file
diff --git a/core/src/main/java/de/danoeh/antennapod/core/glide/FastBlurTransformation.java b/core/src/main/java/de/danoeh/antennapod/core/glide/FastBlurTransformation.java
new file mode 100644
index 000000000..ee58c2f39
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/glide/FastBlurTransformation.java
@@ -0,0 +1,267 @@
+package de.danoeh.antennapod.core.glide;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.media.ThumbnailUtils;
+import android.util.Log;
+
+import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
+import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
+
+public class FastBlurTransformation extends BitmapTransformation {
+
+ private static final String TAG = FastBlurTransformation.class.getSimpleName();
+
+ private static final int STACK_BLUR_RADIUS = 1;
+ private static final int BLUR_IMAGE_WIDTH = 150;
+
+ public FastBlurTransformation(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected Bitmap transform(BitmapPool pool, Bitmap source,
+ int outWidth, int outHeight) {
+ int targetWidth = BLUR_IMAGE_WIDTH;
+ int targetHeight = (int) (1.0 * outHeight * targetWidth / outWidth);
+ Bitmap resized = ThumbnailUtils.extractThumbnail(source, targetWidth, targetHeight);
+ Bitmap result = fastBlur(resized, STACK_BLUR_RADIUS);
+ if (result == null) {
+ Log.w(TAG, "result was null");
+ return source;
+ }
+ return result;
+ }
+
+ @Override
+ public String getId() {
+ return "FastBlurTransformation[width=" + BLUR_IMAGE_WIDTH + "px,radius=" + STACK_BLUR_RADIUS +"]";
+ }
+
+ private static Bitmap fastBlur(Bitmap bitmap, int radius) {
+
+ // Stack Blur v1.0 from
+ // http://www.quasimondo.com/StackBlurForCanvas/StackBlurDemo.html
+ //
+ // Java Author: Mario Klingemann <mario at quasimondo.com>
+ // http://incubator.quasimondo.com
+ // created Feburary 29, 2004
+ // Android port : Yahel Bouaziz <yahel at kayenko.com>
+ // http://www.kayenko.com
+ // ported april 5th, 2012
+
+ // This is a compromise between Gaussian Blur and Box blur
+ // It creates much better looking blurs than Box Blur, but is
+ // 7x faster than my Gaussian Blur implementation.
+ //
+ // I called it Stack Blur because this describes best how this
+ // filter works internally: it creates a kind of moving stack
+ // of colors whilst scanning through the image. Thereby it
+ // just has to add one new block of color to the right side
+ // of the stack and remove the leftmost color. The remaining
+ // colors on the topmost layer of the stack are either added on
+ // or reduced by one, depending on if they are on the right or
+ // on the left side of the stack.
+ //
+ // If you are using this algorithm in your code please add
+ // the following line:
+ //
+ // Stack Blur Algorithm by Mario Klingemann <mario@quasimondo.com>
+
+ if (radius < 1) {
+ return null;
+ }
+
+ int w = bitmap.getWidth();
+ int h = bitmap.getHeight();
+
+ int[] pix = new int[w * h];
+ bitmap.getPixels(pix, 0, w, 0, 0, w, h);
+
+ int wm = w - 1;
+ int hm = h - 1;
+ int wh = w * h;
+ int div = radius + radius + 1;
+
+ int r[] = new int[wh];
+ int g[] = new int[wh];
+ int b[] = new int[wh];
+ int rsum, gsum, bsum, x, y, i, p, yp, yi, yw;
+ int vmin[] = new int[Math.max(w, h)];
+
+ int divsum = (div + 1) >> 1;
+ divsum *= divsum;
+ int dv[] = new int[256 * divsum];
+ for (i = 0; i < 256 * divsum; i++) {
+ dv[i] = (i / divsum);
+ }
+
+ yw = yi = 0;
+
+ int[][] stack = new int[div][3];
+ int stackpointer;
+ int stackstart;
+ int[] sir;
+ int rbs;
+ int r1 = radius + 1;
+ int routsum, goutsum, boutsum;
+ int rinsum, ginsum, binsum;
+
+ for (y = 0; y < h; y++) {
+ rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0;
+ for (i = -radius; i <= radius; i++) {
+ p = pix[yi + Math.min(wm, Math.max(i, 0))];
+ sir = stack[i + radius];
+ sir[0] = (p & 0xff0000) >> 16;
+ sir[1] = (p & 0x00ff00) >> 8;
+ sir[2] = (p & 0x0000ff);
+ rbs = r1 - Math.abs(i);
+ rsum += sir[0] * rbs;
+ gsum += sir[1] * rbs;
+ bsum += sir[2] * rbs;
+ if (i > 0) {
+ rinsum += sir[0];
+ ginsum += sir[1];
+ binsum += sir[2];
+ } else {
+ routsum += sir[0];
+ goutsum += sir[1];
+ boutsum += sir[2];
+ }
+ }
+ stackpointer = radius;
+
+ for (x = 0; x < w; x++) {
+
+ r[yi] = dv[rsum];
+ g[yi] = dv[gsum];
+ b[yi] = dv[bsum];
+
+ rsum -= routsum;
+ gsum -= goutsum;
+ bsum -= boutsum;
+
+ stackstart = stackpointer - radius + div;
+ sir = stack[stackstart % div];
+
+ routsum -= sir[0];
+ goutsum -= sir[1];
+ boutsum -= sir[2];
+
+ if (y == 0) {
+ vmin[x] = Math.min(x + radius + 1, wm);
+ }
+ p = pix[yw + vmin[x]];
+
+ sir[0] = (p & 0xff0000) >> 16;
+ sir[1] = (p & 0x00ff00) >> 8;
+ sir[2] = (p & 0x0000ff);
+
+ rinsum += sir[0];
+ ginsum += sir[1];
+ binsum += sir[2];
+
+ rsum += rinsum;
+ gsum += ginsum;
+ bsum += binsum;
+
+ stackpointer = (stackpointer + 1) % div;
+ sir = stack[(stackpointer) % div];
+
+ routsum += sir[0];
+ goutsum += sir[1];
+ boutsum += sir[2];
+
+ rinsum -= sir[0];
+ ginsum -= sir[1];
+ binsum -= sir[2];
+
+ yi++;
+ }
+ yw += w;
+ }
+ for (x = 0; x < w; x++) {
+ rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0;
+ yp = -radius * w;
+ for (i = -radius; i <= radius; i++) {
+ yi = Math.max(0, yp) + x;
+
+ sir = stack[i + radius];
+
+ sir[0] = r[yi];
+ sir[1] = g[yi];
+ sir[2] = b[yi];
+
+ rbs = r1 - Math.abs(i);
+
+ rsum += r[yi] * rbs;
+ gsum += g[yi] * rbs;
+ bsum += b[yi] * rbs;
+
+ if (i > 0) {
+ rinsum += sir[0];
+ ginsum += sir[1];
+ binsum += sir[2];
+ } else {
+ routsum += sir[0];
+ goutsum += sir[1];
+ boutsum += sir[2];
+ }
+
+ if (i < hm) {
+ yp += w;
+ }
+ }
+ yi = x;
+ stackpointer = radius;
+ for (y = 0; y < h; y++) {
+ // Preserve alpha channel: ( 0xff000000 & pix[yi] )
+ pix[yi] = (0xff000000 & pix[yi]) | (dv[rsum] << 16) | (dv[gsum] << 8) | dv[bsum];
+
+ rsum -= routsum;
+ gsum -= goutsum;
+ bsum -= boutsum;
+
+ stackstart = stackpointer - radius + div;
+ sir = stack[stackstart % div];
+
+ routsum -= sir[0];
+ goutsum -= sir[1];
+ boutsum -= sir[2];
+
+ if (x == 0) {
+ vmin[y] = Math.min(y + r1, hm) * w;
+ }
+ p = x + vmin[y];
+
+ sir[0] = r[p];
+ sir[1] = g[p];
+ sir[2] = b[p];
+
+ rinsum += sir[0];
+ ginsum += sir[1];
+ binsum += sir[2];
+
+ rsum += rinsum;
+ gsum += ginsum;
+ bsum += binsum;
+
+ stackpointer = (stackpointer + 1) % div;
+ sir = stack[stackpointer];
+
+ routsum += sir[0];
+ goutsum += sir[1];
+ boutsum += sir[2];
+
+ rinsum -= sir[0];
+ ginsum -= sir[1];
+ binsum -= sir[2];
+
+ yi += w;
+ }
+ }
+ bitmap.setPixels(pix, 0, w, 0, 0, w, h);
+ return bitmap;
+ }
+
+} \ No newline at end of file
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 1a40120e2..a24e3a485 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,7 +1,6 @@
package de.danoeh.antennapod.core.gpoddernet;
-import android.os.Build;
-import android.util.Log;
+import android.support.annotation.NonNull;
import com.squareup.okhttp.Credentials;
import com.squareup.okhttp.MediaType;
@@ -11,9 +10,6 @@ import com.squareup.okhttp.RequestBody;
import com.squareup.okhttp.Response;
import com.squareup.okhttp.ResponseBody;
-import org.apache.commons.lang3.Validate;
-import org.apache.http.HttpStatus;
-import org.apache.http.client.ClientProtocolException;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
@@ -21,27 +17,15 @@ import org.json.JSONObject;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
-import java.security.KeyStore;
-import java.security.Principal;
-import java.security.cert.CertificateException;
-import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
-import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
-import java.util.Map;
-
-import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLSocketFactory;
-import javax.net.ssl.TrustManager;
-import javax.net.ssl.TrustManagerFactory;
-import javax.net.ssl.X509TrustManager;
-import javax.security.auth.x500.X500Principal;
import de.danoeh.antennapod.core.ClientConfig;
import de.danoeh.antennapod.core.gpoddernet.model.GpodnetDevice;
@@ -117,10 +101,9 @@ public class GpodnetService {
*
* @throws IllegalArgumentException if tag is null
*/
- public List<GpodnetPodcast> getPodcastsForTag(GpodnetTag tag, int count)
+ public List<GpodnetPodcast> getPodcastsForTag(@NonNull GpodnetTag tag,
+ int count)
throws GpodnetServiceException {
- Validate.notNull(tag);
-
try {
URL url = new URI(BASE_SCHEME, BASE_HOST, String.format(
"/api/2/tag/%s/%d.json", tag.getTag(), count), null).toURL();
@@ -144,7 +127,9 @@ public class GpodnetService {
*/
public List<GpodnetPodcast> getPodcastToplist(int count)
throws GpodnetServiceException {
- Validate.isTrue(count >= 1 && count <= 100, "Count must be in range 1..100");
+ if(count < 1 || count > 100) {
+ throw new IllegalArgumentException("Count must be in range 1..100");
+ }
try {
URL url = new URI(BASE_SCHEME, BASE_HOST, String.format(
@@ -174,7 +159,9 @@ public class GpodnetService {
* @throws GpodnetServiceAuthenticationException If there is an authentication error.
*/
public List<GpodnetPodcast> getSuggestions(int count) throws GpodnetServiceException {
- Validate.isTrue(count >= 1 && count <= 100, "Count must be in range 1..100");
+ if(count < 1 || count > 100) {
+ throw new IllegalArgumentException("Count must be in range 1..100");
+ }
try {
URL url = new URI(BASE_SCHEME, BASE_HOST, String.format(
@@ -231,10 +218,8 @@ public class GpodnetService {
* @throws IllegalArgumentException If username is null.
* @throws GpodnetServiceAuthenticationException If there is an authentication error.
*/
- public List<GpodnetDevice> getDevices(String username)
+ public List<GpodnetDevice> getDevices(@NonNull String username)
throws GpodnetServiceException {
- Validate.notNull(username);
-
try {
URL url = new URI(BASE_SCHEME, BASE_HOST, String.format(
"/api/2/devices/%s.json", username), null).toURL();
@@ -259,12 +244,11 @@ public class GpodnetService {
* @throws IllegalArgumentException If username or deviceId is null.
* @throws GpodnetServiceAuthenticationException If there is an authentication error.
*/
- public void configureDevice(String username, String deviceId,
- String caption, GpodnetDevice.DeviceType type)
+ public void configureDevice(@NonNull String username,
+ @NonNull String deviceId,
+ String caption,
+ GpodnetDevice.DeviceType type)
throws GpodnetServiceException {
- Validate.notNull(username);
- Validate.notNull(deviceId);
-
try {
URL url = new URI(BASE_SCHEME, BASE_HOST, String.format(
"/api/2/devices/%s/%s.json", username, deviceId), null).toURL();
@@ -302,11 +286,9 @@ public class GpodnetService {
* @throws IllegalArgumentException If username or deviceId is null.
* @throws GpodnetServiceAuthenticationException If there is an authentication error.
*/
- public String getSubscriptionsOfDevice(String username, String deviceId)
+ public String getSubscriptionsOfDevice(@NonNull String username,
+ @NonNull String deviceId)
throws GpodnetServiceException {
- Validate.notNull(username);
- Validate.notNull(deviceId);
-
try {
URL url = new URI(BASE_SCHEME, BASE_HOST, String.format(
"/subscriptions/%s/%s.opml", username, deviceId), null).toURL();
@@ -329,9 +311,8 @@ public class GpodnetService {
* @throws IllegalArgumentException If username is null.
* @throws GpodnetServiceAuthenticationException If there is an authentication error.
*/
- public String getSubscriptionsOfUser(String username)
+ public String getSubscriptionsOfUser(@NonNull String username)
throws GpodnetServiceException {
- Validate.notNull(username);
try {
URL url = new URI(BASE_SCHEME, BASE_HOST, String.format(
@@ -358,12 +339,11 @@ public class GpodnetService {
* @throws IllegalArgumentException If username, deviceId or subscriptions is null.
* @throws GpodnetServiceAuthenticationException If there is an authentication error.
*/
- public void uploadSubscriptions(String username, String deviceId,
- List<String> subscriptions) throws GpodnetServiceException {
- if (username == null || deviceId == null || subscriptions == null) {
- throw new IllegalArgumentException(
- "Username, device ID and subscriptions must not be null");
- }
+ public void uploadSubscriptions(@NonNull String username,
+ @NonNull String deviceId,
+ @NonNull List<String> subscriptions)
+ throws GpodnetServiceException {
+
try {
URL url = new URI(BASE_SCHEME, BASE_HOST, String.format(
"/subscriptions/%s/%s.txt", username, deviceId), null).toURL();
@@ -379,6 +359,7 @@ public class GpodnetService {
e.printStackTrace();
throw new GpodnetServiceException(e);
}
+
}
/**
@@ -397,12 +378,11 @@ public class GpodnetService {
* @throws de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException if added or removed contain duplicates or if there
* is an authentication error.
*/
- public GpodnetUploadChangesResponse uploadChanges(String username, String deviceId, Collection<String> added,
- Collection<String> removed) throws GpodnetServiceException {
- Validate.notNull(username);
- Validate.notNull(deviceId);
- Validate.notNull(added);
- Validate.notNull(removed);
+ public GpodnetUploadChangesResponse uploadChanges(@NonNull String username,
+ @NonNull String deviceId,
+ @NonNull Collection<String> added,
+ @NonNull Collection<String> removed)
+ throws GpodnetServiceException {
try {
URL url = new URI(BASE_SCHEME, BASE_HOST, String.format(
@@ -438,10 +418,9 @@ public class GpodnetService {
* @throws IllegalArgumentException If username or deviceId is null.
* @throws GpodnetServiceAuthenticationException If there is an authentication error.
*/
- public GpodnetSubscriptionChange getSubscriptionChanges(String username,
- String deviceId, long timestamp) throws GpodnetServiceException {
- Validate.notNull(username);
- Validate.notNull(deviceId);
+ public GpodnetSubscriptionChange getSubscriptionChanges(@NonNull String username,
+ @NonNull String deviceId,
+ long timestamp) throws GpodnetServiceException {
String params = String.format("since=%d", timestamp);
String path = String.format("/api/2/subscriptions/%s/%s.json",
@@ -476,11 +455,9 @@ public class GpodnetService {
* @throws de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException if added or removed contain duplicates or if there
* is an authentication error.
*/
- public GpodnetEpisodeActionPostResponse uploadEpisodeActions(Collection<GpodnetEpisodeAction> episodeActions)
+ public GpodnetEpisodeActionPostResponse uploadEpisodeActions(@NonNull Collection<GpodnetEpisodeAction> episodeActions)
throws GpodnetServiceException {
- Validate.notNull(episodeActions);
-
String username = GpodnetPreferences.getUsername();
try {
@@ -549,11 +526,9 @@ public class GpodnetService {
*
* @throws IllegalArgumentException If username or password is null.
*/
- public void authenticate(String username, String password)
+ public void authenticate(@NonNull String username,
+ @NonNull String password)
throws GpodnetServiceException {
- Validate.notNull(username);
- Validate.notNull(password);
-
URL url;
try {
url = new URI(BASE_SCHEME, BASE_HOST, String.format(
@@ -562,7 +537,8 @@ public class GpodnetService {
e.printStackTrace();
throw new GpodnetServiceException(e);
}
- Request.Builder request = new Request.Builder().url(url).post(null);
+ RequestBody body = RequestBody.create(TEXT, "");
+ Request.Builder request = new Request.Builder().url(url).post(body);
executeRequestWithAuthentication(request, username, password);
}
@@ -579,10 +555,8 @@ public class GpodnetService {
}.start();
}
- private String executeRequest(Request.Builder requestB)
+ private String executeRequest(@NonNull Request.Builder requestB)
throws GpodnetServiceException {
- Validate.notNull(requestB);
-
Request request = requestB.header("User-Agent", ClientConfig.USER_AGENT).build();
String responseString = null;
Response response = null;
@@ -627,10 +601,7 @@ public class GpodnetService {
checkStatusCode(response);
body = response.body();
result = getStringFromResponseBody(body);
- } catch (ClientProtocolException e) {
- e.printStackTrace();
- throw new GpodnetServiceException(e);
- } catch (IOException e) {
+ } catch (Exception e) {
e.printStackTrace();
throw new GpodnetServiceException(e);
} finally {
@@ -646,10 +617,8 @@ public class GpodnetService {
return result;
}
- private String getStringFromResponseBody(ResponseBody body)
+ private String getStringFromResponseBody(@NonNull ResponseBody body)
throws GpodnetServiceException {
- Validate.notNull(body);
-
ByteArrayOutputStream outputStream;
int contentLength = 0;
try {
@@ -676,31 +645,27 @@ public class GpodnetService {
return outputStream.toString();
}
- private void checkStatusCode(Response response)
+ private void checkStatusCode(@NonNull Response response)
throws GpodnetServiceException {
- Validate.notNull(response);
int responseCode = response.code();
- if (responseCode != HttpStatus.SC_OK) {
- if (responseCode == HttpStatus.SC_UNAUTHORIZED) {
+ if (responseCode != HttpURLConnection.HTTP_OK) {
+ if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) {
throw new GpodnetServiceAuthenticationException("Wrong username or password");
} else {
- throw new GpodnetServiceBadStatusCodeException(
- "Bad response code: " + responseCode, responseCode);
+ throw new GpodnetServiceBadStatusCodeException("Bad response code: "
+ + responseCode, responseCode);
}
}
}
- private List<GpodnetPodcast> readPodcastListFromJSONArray(JSONArray array)
+ private List<GpodnetPodcast> readPodcastListFromJSONArray(@NonNull JSONArray array)
throws JSONException {
- Validate.notNull(array);
-
List<GpodnetPodcast> result = new ArrayList<GpodnetPodcast>(
array.length());
for (int i = 0; i < array.length(); i++) {
result.add(readPodcastFromJSONObject(array.getJSONObject(i)));
}
return result;
-
}
private GpodnetPodcast readPodcastFromJSONObject(JSONObject object)
@@ -745,10 +710,8 @@ public class GpodnetService {
logoUrl, website, mygpoLink);
}
- private List<GpodnetDevice> readDeviceListFromJSONArray(JSONArray array)
+ private List<GpodnetDevice> readDeviceListFromJSONArray(@NonNull JSONArray array)
throws JSONException {
- Validate.notNull(array);
-
List<GpodnetDevice> result = new ArrayList<GpodnetDevice>(
array.length());
for (int i = 0; i < array.length(); i++) {
@@ -767,8 +730,7 @@ public class GpodnetService {
}
private GpodnetSubscriptionChange readSubscriptionChangesFromJSONObject(
- JSONObject object) throws JSONException {
- Validate.notNull(object);
+ @NonNull JSONObject object) throws JSONException {
List<String> added = new LinkedList<String>();
JSONArray jsonAdded = object.getJSONArray("add");
@@ -787,8 +749,7 @@ public class GpodnetService {
}
private GpodnetEpisodeActionGetResponse readEpisodeActionsFromJSONObject(
- JSONObject object) throws JSONException {
- Validate.notNull(object);
+ @NonNull JSONObject object) throws JSONException {
List<GpodnetEpisodeAction> episodeActions = new ArrayList<GpodnetEpisodeAction>();
@@ -804,5 +765,4 @@ public class GpodnetService {
return new GpodnetEpisodeActionGetResponse(episodeActions, timestamp);
}
-
}
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 4885a243a..2d49c170a 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
@@ -1,6 +1,6 @@
package de.danoeh.antennapod.core.gpoddernet.model;
-import org.apache.commons.lang3.Validate;
+import android.support.annotation.NonNull;
public class GpodnetDevice {
@@ -9,10 +9,10 @@ public class GpodnetDevice {
private DeviceType type;
private int subscriptions;
- public GpodnetDevice(String id, String caption, String type,
+ public GpodnetDevice(@NonNull String id,
+ String caption,
+ String type,
int subscriptions) {
- Validate.notNull(id);
-
this.id = id;
this.caption = caption;
this.type = DeviceType.fromString(type);
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 bd6210d13..2d174a6bc 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
@@ -1,13 +1,9 @@
package de.danoeh.antennapod.core.gpoddernet.model;
+import android.text.TextUtils;
import android.util.Log;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.commons.lang3.builder.EqualsBuilder;
-import org.apache.commons.lang3.builder.HashCodeBuilder;
-import org.apache.commons.lang3.builder.ToStringBuilder;
-import org.apache.commons.lang3.builder.ToStringStyle;
import org.json.JSONException;
import org.json.JSONObject;
@@ -90,15 +86,20 @@ public class GpodnetEpisodeAction {
String podcast = object.optString("podcast", null);
String episode = object.optString("episode", null);
String actionString = object.optString("action", null);
- if(StringUtils.isEmpty(podcast) || StringUtils.isEmpty(episode) || StringUtils.isEmpty(actionString)) {
+ if(TextUtils.isEmpty(podcast) || TextUtils.isEmpty(episode) || TextUtils.isEmpty(actionString)) {
+ return null;
+ }
+ GpodnetEpisodeAction.Action action;
+ try {
+ action = GpodnetEpisodeAction.Action.valueOf(actionString.toUpperCase());
+ } catch (IllegalArgumentException e) {
return null;
}
- GpodnetEpisodeAction.Action action = GpodnetEpisodeAction.Action.valueOf(actionString.toUpperCase());
String deviceId = object.optString("device", "");
GpodnetEpisodeAction.Builder builder = new GpodnetEpisodeAction.Builder(podcast, episode, action)
.deviceId(deviceId);
String utcTimestamp = object.optString("timestamp", null);
- if(StringUtils.isNotEmpty(utcTimestamp)) {
+ if(!TextUtils.isEmpty(utcTimestamp)) {
builder.timestamp(DateUtils.parse(utcTimestamp));
}
if(action == GpodnetEpisodeAction.Action.PLAY) {
@@ -168,34 +169,34 @@ public class GpodnetEpisodeAction {
@Override
public boolean equals(Object o) {
- if(o == null) return false;
- if(this == o) return true;
- if(this.getClass() != o.getClass()) return false;
- GpodnetEpisodeAction that = (GpodnetEpisodeAction)o;
- return new EqualsBuilder()
- .append(this.podcast, that.podcast)
- .append(this.episode, that.episode)
- .append(this.deviceId, that.deviceId)
- .append(this.action, that.action)
- .append(this.timestamp, that.timestamp)
- .append(this.started, that.started)
- .append(this.position, that.position)
- .append(this.total, that.total)
- .isEquals();
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ GpodnetEpisodeAction that = (GpodnetEpisodeAction) o;
+
+ if (started != that.started) return false;
+ if (position != that.position) return false;
+ if (total != that.total) return false;
+ if (podcast != null ? !podcast.equals(that.podcast) : that.podcast != null) return false;
+ if (episode != null ? !episode.equals(that.episode) : that.episode != null) return false;
+ if (deviceId != null ? !deviceId.equals(that.deviceId) : that.deviceId != null)
+ return false;
+ if (action != that.action) return false;
+ return !(timestamp != null ? !timestamp.equals(that.timestamp) : that.timestamp != null);
+
}
@Override
public int hashCode() {
- return new HashCodeBuilder()
- .append(this.podcast)
- .append(this.episode)
- .append(this.deviceId)
- .append(this.action)
- .append(this.timestamp)
- .append(this.started)
- .append(this.position)
- .append(this.total)
- .toHashCode();
+ int result = podcast != null ? podcast.hashCode() : 0;
+ result = 31 * result + (episode != null ? episode.hashCode() : 0);
+ result = 31 * result + (deviceId != null ? deviceId.hashCode() : 0);
+ result = 31 * result + (action != null ? action.hashCode() : 0);
+ result = 31 * result + (timestamp != null ? timestamp.hashCode() : 0);
+ result = 31 * result + started;
+ result = 31 * result + position;
+ result = 31 * result + total;
+ return result;
}
public String writeToString() {
@@ -240,7 +241,16 @@ public class GpodnetEpisodeAction {
@Override
public String toString() {
- return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
+ return "GpodnetEpisodeAction{" +
+ "podcast='" + podcast + '\'' +
+ ", episode='" + episode + '\'' +
+ ", deviceId='" + deviceId + '\'' +
+ ", action=" + action +
+ ", timestamp=" + timestamp +
+ ", started=" + started +
+ ", position=" + position +
+ ", total=" + total +
+ '}';
}
public static class Builder {
diff --git a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetEpisodeActionGetResponse.java b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetEpisodeActionGetResponse.java
index 50420f0a3..1e21efcda 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetEpisodeActionGetResponse.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetEpisodeActionGetResponse.java
@@ -1,9 +1,7 @@
package de.danoeh.antennapod.core.gpoddernet.model;
-import org.apache.commons.lang3.Validate;
-import org.apache.commons.lang3.builder.ToStringBuilder;
-import org.apache.commons.lang3.builder.ToStringStyle;
+import android.support.annotation.NonNull;
import java.util.List;
@@ -12,8 +10,8 @@ public class GpodnetEpisodeActionGetResponse {
private final List<GpodnetEpisodeAction> episodeActions;
private final long timestamp;
- public GpodnetEpisodeActionGetResponse(List<GpodnetEpisodeAction> episodeActions, long timestamp) {
- Validate.notNull(episodeActions);
+ public GpodnetEpisodeActionGetResponse(@NonNull List<GpodnetEpisodeAction> episodeActions,
+ long timestamp) {
this.episodeActions = episodeActions;
this.timestamp = timestamp;
}
@@ -28,7 +26,9 @@ public class GpodnetEpisodeActionGetResponse {
@Override
public String toString() {
- return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
+ return "GpodnetEpisodeActionGetResponse{" +
+ "episodeActions=" + episodeActions +
+ ", timestamp=" + timestamp +
+ '}';
}
-
}
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 e06a88d5c..5f096db14 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
@@ -1,12 +1,13 @@
package de.danoeh.antennapod.core.gpoddernet.model;
+import android.support.v4.util.ArrayMap;
+
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
-import java.util.HashMap;
import java.util.Map;
public class GpodnetEpisodeActionPostResponse {
@@ -36,8 +37,8 @@ public class GpodnetEpisodeActionPostResponse {
public static GpodnetEpisodeActionPostResponse fromJSONObject(String objectString) throws JSONException {
final JSONObject object = new JSONObject(objectString);
final long timestamp = object.getLong("timestamp");
- Map<String, String> updatedUrls = new HashMap<String, String>();
JSONArray urls = object.getJSONArray("update_urls");
+ Map<String, String> updatedUrls = new ArrayMap<String, String>(urls.length());
for (int i = 0; i < urls.length(); i++) {
JSONArray urlPair = urls.getJSONArray(i);
updatedUrls.put(urlPair.getString(0), urlPair.getString(1));
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 afebf66ac..191c0fa39 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
@@ -1,6 +1,6 @@
package de.danoeh.antennapod.core.gpoddernet.model;
-import org.apache.commons.lang3.Validate;
+import android.support.annotation.NonNull;
public class GpodnetPodcast {
private String url;
@@ -11,12 +11,13 @@ public class GpodnetPodcast {
private String website;
private String mygpoLink;
- public GpodnetPodcast(String url, String title, String description,
- int subscribers, String logoUrl, String website, String mygpoLink) {
- Validate.notNull(url);
- Validate.notNull(title);
- Validate.notNull(description);
-
+ public GpodnetPodcast(@NonNull String url,
+ @NonNull String title,
+ @NonNull String description,
+ int subscribers,
+ String logoUrl,
+ String website,
+ String mygpoLink) {
this.url = url;
this.title = title;
this.description = description;
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 a5cb8c0f0..6cc9b79a3 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
@@ -1,6 +1,6 @@
package de.danoeh.antennapod.core.gpoddernet.model;
-import org.apache.commons.lang3.Validate;
+import android.support.annotation.NonNull;
import java.util.List;
@@ -9,11 +9,9 @@ public class GpodnetSubscriptionChange {
private List<String> removed;
private long timestamp;
- public GpodnetSubscriptionChange(List<String> added, List<String> removed,
+ public GpodnetSubscriptionChange(@NonNull List<String> added,
+ @NonNull List<String> removed,
long timestamp) {
- Validate.notNull(added);
- Validate.notNull(removed);
-
this.added = added;
this.removed = removed;
this.timestamp = timestamp;
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 cd865731b..42a31afc5 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
@@ -2,8 +2,7 @@ package de.danoeh.antennapod.core.gpoddernet.model;
import android.os.Parcel;
import android.os.Parcelable;
-
-import org.apache.commons.lang3.Validate;
+import android.support.annotation.NonNull;
public class GpodnetTag implements Parcelable {
@@ -11,20 +10,16 @@ public class GpodnetTag implements Parcelable {
private final String tag;
private final int usage;
- public GpodnetTag(String title, String tag, int usage) {
- Validate.notNull(title);
- Validate.notNull(tag);
-
+ public GpodnetTag(@NonNull String title, @NonNull String tag, int usage) {
this.title = title;
this.tag = tag;
this.usage = usage;
}
- public static GpodnetTag createFromParcel(Parcel in) {
- final String title = in.readString();
- final String tag = in.readString();
- final int usage = in.readInt();
- return new GpodnetTag(title, tag, usage);
+ protected GpodnetTag(Parcel in) {
+ title = in.readString();
+ tag = in.readString();
+ usage = in.readInt();
}
@Override
@@ -56,5 +51,16 @@ public class GpodnetTag implements Parcelable {
dest.writeInt(usage);
}
+ public static final Creator<GpodnetTag> CREATOR = new Creator<GpodnetTag>() {
+ @Override
+ public GpodnetTag createFromParcel(Parcel in) {
+ return new GpodnetTag(in);
+ }
+
+ @Override
+ public GpodnetTag[] newArray(int size) {
+ return new GpodnetTag[size];
+ }
+ };
}
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 5a37efa5e..9bd1881e4 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
@@ -1,10 +1,11 @@
package de.danoeh.antennapod.core.gpoddernet.model;
+import android.support.v4.util.ArrayMap;
+
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
-import java.util.HashMap;
import java.util.Map;
/**
@@ -37,7 +38,7 @@ public class GpodnetUploadChangesResponse {
public static GpodnetUploadChangesResponse fromJSONObject(String objectString) throws JSONException {
final JSONObject object = new JSONObject(objectString);
final long timestamp = object.getLong("timestamp");
- Map<String, String> updatedUrls = new HashMap<String, String>();
+ Map<String, String> updatedUrls = new ArrayMap<>();
JSONArray urls = object.getJSONArray("update_urls");
for (int i = 0; i < urls.length(); i++) {
JSONArray urlPair = urls.getJSONArray(i);
diff --git a/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlSymbols.java b/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlSymbols.java
index 2b831ca2a..c973713cb 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlSymbols.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlSymbols.java
@@ -13,6 +13,7 @@ public final class OpmlSymbols {
public static final String VERSION = "version";
public static final String HEAD = "head";
public static final String TITLE = "title";
+ public static final String DATE_CREATED = "dateCreated";
private OpmlSymbols() {
diff --git a/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlWriter.java b/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlWriter.java
index 641190f62..673c602df 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlWriter.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlWriter.java
@@ -2,14 +2,17 @@ package de.danoeh.antennapod.core.opml;
import android.util.Log;
import android.util.Xml;
-import de.danoeh.antennapod.core.BuildConfig;
-import de.danoeh.antennapod.core.feed.Feed;
+
import org.xmlpull.v1.XmlSerializer;
import java.io.IOException;
import java.io.Writer;
+import java.util.Date;
import java.util.List;
+import de.danoeh.antennapod.core.feed.Feed;
+import de.danoeh.antennapod.core.util.DateUtils;
+
/** Writes OPML documents. */
public class OpmlWriter {
private static final String TAG = "OpmlWriter";
@@ -27,23 +30,38 @@ public class OpmlWriter {
*/
public void writeDocument(List<Feed> feeds, Writer writer)
throws IllegalArgumentException, IllegalStateException, IOException {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Starting to write document");
+ Log.d(TAG, "Starting to write document");
XmlSerializer xs = Xml.newSerializer();
xs.setOutput(writer);
xs.startDocument(ENCODING, false);
+ xs.text("\n");
xs.startTag(null, OpmlSymbols.OPML);
xs.attribute(null, OpmlSymbols.VERSION, OPML_VERSION);
+ xs.text("\n");
+ xs.text(" ");
xs.startTag(null, OpmlSymbols.HEAD);
+ xs.text("\n");
+ xs.text(" ");
xs.startTag(null, OpmlSymbols.TITLE);
xs.text(OPML_TITLE);
xs.endTag(null, OpmlSymbols.TITLE);
+ xs.text("\n");
+ xs.text(" ");
+ xs.startTag(null, OpmlSymbols.DATE_CREATED);
+ xs.text(DateUtils.formatRFC822Date(new Date()));
+ xs.endTag(null, OpmlSymbols.DATE_CREATED);
+ xs.text("\n");
+ xs.text(" ");
xs.endTag(null, OpmlSymbols.HEAD);
+ xs.text("\n");
+ xs.text(" ");
xs.startTag(null, OpmlSymbols.BODY);
+ xs.text("\n");
for (Feed feed : feeds) {
+ xs.text(" ");
xs.startTag(null, OpmlSymbols.OUTLINE);
xs.attribute(null, OpmlSymbols.TEXT, feed.getTitle());
xs.attribute(null, OpmlSymbols.TITLE, feed.getTitle());
@@ -55,11 +73,14 @@ public class OpmlWriter {
xs.attribute(null, OpmlSymbols.HTMLURL, feed.getLink());
}
xs.endTag(null, OpmlSymbols.OUTLINE);
+ xs.text("\n");
}
+ xs.text(" ");
xs.endTag(null, OpmlSymbols.BODY);
+ xs.text("\n");
xs.endTag(null, OpmlSymbols.OPML);
+ xs.text("\n");
xs.endDocument();
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Finished writing document");
+ Log.d(TAG, "Finished writing document");
}
}
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 c3c6ce8c5..edd7b807a 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
@@ -2,13 +2,11 @@ package de.danoeh.antennapod.core.preferences;
import android.content.Context;
import android.content.SharedPreferences;
+import android.text.TextUtils;
import android.util.Log;
-import org.apache.commons.lang3.StringUtils;
-
import java.util.ArrayList;
import java.util.Collection;
-import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -217,26 +215,36 @@ public class GpodnetPreferences {
public static void removeRemovedFeeds(Collection<String> removed) {
ensurePreferencesLoaded();
+ feedListLock.lock();
removedFeeds.removeAll(removed);
writePreference(PREF_SYNC_REMOVED, removedFeeds);
+ feedListLock.unlock();
}
- public static synchronized void enqueueEpisodeAction(GpodnetEpisodeAction action) {
+ public static void enqueueEpisodeAction(GpodnetEpisodeAction action) {
ensurePreferencesLoaded();
+ feedListLock.lock();
queuedEpisodeActions.add(action);
writePreference(PREF_SYNC_EPISODE_ACTIONS, writeEpisodeActionsToString(queuedEpisodeActions));
+ feedListLock.unlock();
GpodnetSyncService.sendSyncActionsIntent(ClientConfig.applicationCallbacks.getApplicationInstance());
}
public static List<GpodnetEpisodeAction> getQueuedEpisodeActions() {
ensurePreferencesLoaded();
- return Collections.unmodifiableList(queuedEpisodeActions);
+ List<GpodnetEpisodeAction> copy = new ArrayList();
+ feedListLock.lock();
+ copy.addAll(queuedEpisodeActions);
+ feedListLock.unlock();
+ return copy;
}
- public static synchronized void removeQueuedEpisodeActions(Collection<GpodnetEpisodeAction> queued) {
+ public static void removeQueuedEpisodeActions(Collection<GpodnetEpisodeAction> queued) {
ensurePreferencesLoaded();
+ feedListLock.lock();
queuedEpisodeActions.removeAll(queued);
writePreference(PREF_SYNC_EPISODE_ACTIONS, writeEpisodeActionsToString(queuedEpisodeActions));
+ feedListLock.unlock();
}
/**
@@ -252,12 +260,14 @@ public class GpodnetPreferences {
setUsername(null);
setPassword(null);
setDeviceID(null);
+ feedListLock.lock();
addedFeeds.clear();
writePreference(PREF_SYNC_ADDED, addedFeeds);
removedFeeds.clear();
writePreference(PREF_SYNC_REMOVED, removedFeeds);
queuedEpisodeActions.clear();
writePreference(PREF_SYNC_EPISODE_ACTIONS, writeEpisodeActionsToString(queuedEpisodeActions));
+ feedListLock.unlock();
setLastSubscriptionSyncTimestamp(0);
}
@@ -282,7 +292,7 @@ public class GpodnetPreferences {
String[] lines = s.split("\n");
List<GpodnetEpisodeAction> result = new ArrayList<GpodnetEpisodeAction>(lines.length);
for(String line : lines) {
- if(StringUtils.isNotBlank(line)) {
+ if(TextUtils.isEmpty(line)) {
GpodnetEpisodeAction action = GpodnetEpisodeAction.readFromString(line);
if(action != null) {
result.add(GpodnetEpisodeAction.readFromString(line));
diff --git a/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java
index 714f1b051..dfe056f14 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java
@@ -3,11 +3,7 @@ package de.danoeh.antennapod.core.preferences;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
-import android.util.Log;
-import org.apache.commons.lang3.Validate;
-
-import de.danoeh.antennapod.core.BuildConfig;
import de.danoeh.antennapod.core.feed.EventDistributor;
/**
@@ -15,159 +11,104 @@ import de.danoeh.antennapod.core.feed.EventDistributor;
* instance of this class must first be instantiated via createInstance() or
* otherwise every public method will throw an Exception when called.
*/
-public class PlaybackPreferences implements
- SharedPreferences.OnSharedPreferenceChangeListener {
- private static final String TAG = "PlaybackPreferences";
-
- /**
- * Contains the feed id of the currently playing item if it is a FeedMedia
- * object.
- */
- public static final String PREF_CURRENTLY_PLAYING_FEED_ID = "de.danoeh.antennapod.preferences.lastPlayedFeedId";
-
- /**
- * Contains the id of the currently playing FeedMedia object or
- * NO_MEDIA_PLAYING if the currently playing media is no FeedMedia object.
- */
- public static final String PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID = "de.danoeh.antennapod.preferences.lastPlayedFeedMediaId";
-
- /**
- * Type of the media object that is currently being played. This preference
- * is set to NO_MEDIA_PLAYING after playback has been completed and is set
- * as soon as the 'play' button is pressed.
- */
- public static final String PREF_CURRENTLY_PLAYING_MEDIA = "de.danoeh.antennapod.preferences.currentlyPlayingMedia";
-
- /** True if last played media was streamed. */
- public static final String PREF_CURRENT_EPISODE_IS_STREAM = "de.danoeh.antennapod.preferences.lastIsStream";
-
- /** True if last played media was a video. */
- public static final String PREF_CURRENT_EPISODE_IS_VIDEO = "de.danoeh.antennapod.preferences.lastIsVideo";
-
- /** The current player status as int. */
+public class PlaybackPreferences implements SharedPreferences.OnSharedPreferenceChangeListener {
+
+ private static final String TAG = "PlaybackPreferences";
+
+ /**
+ * Contains the feed id of the currently playing item if it is a FeedMedia
+ * object.
+ */
+ public static final String PREF_CURRENTLY_PLAYING_FEED_ID = "de.danoeh.antennapod.preferences.lastPlayedFeedId";
+
+ /**
+ * Contains the id of the currently playing FeedMedia object or
+ * NO_MEDIA_PLAYING if the currently playing media is no FeedMedia object.
+ */
+ public static final String PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID = "de.danoeh.antennapod.preferences.lastPlayedFeedMediaId";
+
+ /**
+ * Type of the media object that is currently being played. This preference
+ * is set to NO_MEDIA_PLAYING after playback has been completed and is set
+ * as soon as the 'play' button is pressed.
+ */
+ public static final String PREF_CURRENTLY_PLAYING_MEDIA = "de.danoeh.antennapod.preferences.currentlyPlayingMedia";
+
+ /**
+ * True if last played media was streamed.
+ */
+ public static final String PREF_CURRENT_EPISODE_IS_STREAM = "de.danoeh.antennapod.preferences.lastIsStream";
+
+ /**
+ * True if last played media was a video.
+ */
+ public static final String PREF_CURRENT_EPISODE_IS_VIDEO = "de.danoeh.antennapod.preferences.lastIsVideo";
+
+ /**
+ * The current player status as int.
+ */
public static final String PREF_CURRENT_PLAYER_STATUS = "de.danoeh.antennapod.preferences.currentPlayerStatus";
- /** Value of PREF_CURRENTLY_PLAYING_MEDIA if no media is playing. */
- public static final long NO_MEDIA_PLAYING = -1;
+ /**
+ * Value of PREF_CURRENTLY_PLAYING_MEDIA if no media is playing.
+ */
+ public static final long NO_MEDIA_PLAYING = -1;
- /** Value of PREF_CURRENT_PLAYER_STATUS if media player status is playing. */
+ /**
+ * Value of PREF_CURRENT_PLAYER_STATUS if media player status is playing.
+ */
public static final int PLAYER_STATUS_PLAYING = 1;
- /** Value of PREF_CURRENT_PLAYER_STATUS if media player status is paused. */
+ /**
+ * Value of PREF_CURRENT_PLAYER_STATUS if media player status is paused.
+ */
public static final int PLAYER_STATUS_PAUSED = 2;
- /** Value of PREF_CURRENT_PLAYER_STATUS if media player status is neither playing nor paused. */
+ /**
+ * Value of PREF_CURRENT_PLAYER_STATUS if media player status is neither playing nor paused.
+ */
public static final int PLAYER_STATUS_OTHER = 3;
- private long currentlyPlayingFeedId;
- private long currentlyPlayingFeedMediaId;
- private long currentlyPlayingMedia;
- private boolean currentEpisodeIsStream;
- private boolean currentEpisodeIsVideo;
- private int currentPlayerStatus;
-
- private static PlaybackPreferences instance;
- private Context context;
-
- private PlaybackPreferences(Context context) {
- this.context = context;
- loadPreferences();
- }
+ private static PlaybackPreferences instance;
+ private static SharedPreferences prefs;
- /**
- * Sets up the UserPreferences class.
- *
- * @throws IllegalArgumentException
- * if context is null
- * */
- public static void createInstance(Context context) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Creating new instance of UserPreferences");
- Validate.notNull(context);
-
- instance = new PlaybackPreferences(context);
-
- PreferenceManager.getDefaultSharedPreferences(context)
- .registerOnSharedPreferenceChangeListener(instance);
- }
+ private PlaybackPreferences() {
+ }
- private void loadPreferences() {
- SharedPreferences sp = PreferenceManager
- .getDefaultSharedPreferences(context);
- currentlyPlayingFeedId = sp.getLong(PREF_CURRENTLY_PLAYING_FEED_ID, -1);
- currentlyPlayingFeedMediaId = sp.getLong(
- PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, NO_MEDIA_PLAYING);
- currentlyPlayingMedia = sp.getLong(PREF_CURRENTLY_PLAYING_MEDIA,
- NO_MEDIA_PLAYING);
- currentEpisodeIsStream = sp.getBoolean(PREF_CURRENT_EPISODE_IS_STREAM, true);
- currentEpisodeIsVideo = sp.getBoolean(PREF_CURRENT_EPISODE_IS_VIDEO, false);
- currentPlayerStatus = sp.getInt(PREF_CURRENT_PLAYER_STATUS,
- PLAYER_STATUS_OTHER);
- }
+ public static void init(Context context) {
+ instance = new PlaybackPreferences();
+ prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ prefs.registerOnSharedPreferenceChangeListener(instance);
+ }
- @Override
- public void onSharedPreferenceChanged(SharedPreferences sp, String key) {
- if (key.equals(PREF_CURRENTLY_PLAYING_FEED_ID)) {
- currentlyPlayingFeedId = sp.getLong(PREF_CURRENTLY_PLAYING_FEED_ID,
- -1);
-
- } else if (key.equals(PREF_CURRENTLY_PLAYING_MEDIA)) {
- currentlyPlayingMedia = sp
- .getLong(PREF_CURRENTLY_PLAYING_MEDIA, -1);
-
- } else if (key.equals(PREF_CURRENT_EPISODE_IS_STREAM)) {
- currentEpisodeIsStream = sp.getBoolean(PREF_CURRENT_EPISODE_IS_STREAM, true);
-
- } else if (key.equals(PREF_CURRENT_EPISODE_IS_VIDEO)) {
- currentEpisodeIsVideo = sp.getBoolean(PREF_CURRENT_EPISODE_IS_VIDEO, false);
-
- } else if (key.equals(PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID)) {
- currentlyPlayingFeedMediaId = sp.getLong(
- PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, NO_MEDIA_PLAYING);
- }
- else if (key.equals(PREF_CURRENT_PLAYER_STATUS)) {
- currentPlayerStatus = sp.getInt(PREF_CURRENT_PLAYER_STATUS,
- PLAYER_STATUS_OTHER);
+ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
+ if (key.equals(PREF_CURRENT_PLAYER_STATUS)) {
EventDistributor.getInstance().sendPlayerStatusUpdateBroadcast();
}
- }
-
- private static void instanceAvailable() {
- if (instance == null) {
- throw new IllegalStateException(
- "UserPreferences was used before being set up");
- }
- }
-
+ }
public static long getLastPlayedFeedId() {
- instanceAvailable();
- return instance.currentlyPlayingFeedId;
+ return prefs.getLong(PREF_CURRENTLY_PLAYING_FEED_ID, -1);
}
public static long getCurrentlyPlayingMedia() {
- instanceAvailable();
- return instance.currentlyPlayingMedia;
+ return prefs.getLong(PREF_CURRENTLY_PLAYING_MEDIA, NO_MEDIA_PLAYING);
}
public static long getCurrentlyPlayingFeedMediaId() {
- return instance.currentlyPlayingFeedMediaId;
+ return prefs.getLong(PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, NO_MEDIA_PLAYING);
}
public static boolean getCurrentEpisodeIsStream() {
- instanceAvailable();
- return instance.currentEpisodeIsStream;
+ return prefs.getBoolean(PREF_CURRENT_EPISODE_IS_STREAM, true);
}
public static boolean getCurrentEpisodeIsVideo() {
- instanceAvailable();
- return instance.currentEpisodeIsVideo;
+ return prefs.getBoolean(PREF_CURRENT_EPISODE_IS_VIDEO, false);
}
public static int getCurrentPlayerStatus() {
- instanceAvailable();
- return instance.currentPlayerStatus;
+ return prefs.getInt(PREF_CURRENT_PLAYER_STATUS, PLAYER_STATUS_OTHER);
}
-
}
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 594241573..6c0aff15e 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
@@ -5,12 +5,13 @@ 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.NonNull;
import android.support.v4.app.NotificationCompat;
+import android.text.TextUtils;
import android.util.Log;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.commons.lang3.Validate;
import org.json.JSONArray;
import org.json.JSONException;
@@ -18,32 +19,38 @@ import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.LinkedList;
+import java.util.Calendar;
import java.util.List;
import java.util.concurrent.TimeUnit;
-import de.danoeh.antennapod.core.ClientConfig;
import de.danoeh.antennapod.core.R;
import de.danoeh.antennapod.core.receiver.FeedUpdateReceiver;
+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;
/**
* Provides access to preferences set by the user in the settings screen. A
* private instance of this class must first be instantiated via
- * createInstance() or otherwise every public method will throw an Exception
+ * init() or otherwise every public method will throw an Exception
* when called.
*/
-public class UserPreferences implements
- SharedPreferences.OnSharedPreferenceChangeListener {
+public class UserPreferences {
public static final String IMPORT_DIR = "import/";
private static final String TAG = "UserPreferences";
- // User Infercasce
+ // 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";
public static final String PREF_EXPANDED_NOTIFICATION = "prefExpandNotify";
public static final String PREF_PERSISTENT_NOTIFICATION = "prefPersistNotify";
+ public static final String PREF_LOCKSCREEN_BACKGROUND = "prefLockscreenBackground";
+ public static final String PREF_SHOW_DOWNLOAD_REPORT = "prefShowDownloadReport";
// Queue
public static final String PREF_QUEUE_ADD_TO_FRONT = "prefQueueAddToFront";
@@ -51,16 +58,20 @@ public class UserPreferences implements
// 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_FOLLOW_QUEUE = "prefFollowQueue";
+ public static final String PREF_SKIP_KEEPS_EPISODE = "prefSkipKeepsEpisode";
public static final String PREF_AUTO_DELETE = "prefAutoDelete";
public static final String PREF_SMART_MARK_AS_PLAYED_SECS = "prefSmartMarkAsPlayedSecs";
- private static final String PREF_PLAYBACK_SPEED_ARRAY = "prefPlaybackSpeedArray";
+ public static final String PREF_PLAYBACK_SPEED_ARRAY = "prefPlaybackSpeedArray";
public static final String PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS = "prefPauseForFocusLoss";
public static final String PREF_RESUME_AFTER_CALL = "prefResumeAfterCall";
// Network
public static final String PREF_UPDATE_INTERVAL = "prefAutoUpdateIntervall";
public static final String PREF_MOBILE_UPDATE = "prefMobileUpdate";
+ public static final String PREF_EPISODE_CLEANUP = "prefEpisodeCleanup";
public static final String PREF_PARALLEL_DOWNLOADS = "prefParallelDownloads";
public static final String PREF_EPISODE_CACHE_SIZE = "prefEpisodeCacheSize";
public static final String PREF_ENABLE_AUTODL = "prefEnableAutoDl";
@@ -74,193 +85,52 @@ public class UserPreferences implements
// Other
public 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";
private static final String PREF_FAST_FORWARD_SECS = "prefFastForwardSecs";
private static final String PREF_REWIND_SECS = "prefRewindSecs";
public static final String PREF_QUEUE_LOCKED = "prefQueueLocked";
-
- // TODO: Make this value configurable
- private static final float PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD_DEFAULT = 0.8f;
-
+ 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";
+
+ // Experimental
+ public static final String PREF_SONIC = "prefSonic";
+ public static final String PREF_STEREO_TO_MONO = "PrefStereoToMono";
+ public static final String PREF_NORMALIZER = "prefNormalizer";
+ public static final int EPISODE_CLEANUP_QUEUE = -1;
+ public static final int EPISODE_CLEANUP_NULL = -2;
+ public static final int EPISODE_CLEANUP_DEFAULT = 0;
+
+ // Constants
private static int EPISODE_CACHE_SIZE_UNLIMITED = -1;
+ public static int FEED_ORDER_COUNTER = 0;
+ public static int FEED_ORDER_ALPHABETICAL = 1;
+ public static int FEED_ORDER_LAST_UPDATE = 2;
+ public static int FEED_COUNTER_SHOW_NEW_UNPLAYED_SUM = 0;
+ public static int FEED_COUNTER_SHOW_NEW = 1;
+ public static int FEED_COUNTER_SHOW_UNPLAYED = 2;
+ public static int FEED_COUNTER_SHOW_NONE = 3;
- private static UserPreferences instance;
- private final Context context;
-
- // User Interface
- private int theme;
- private List<String> hiddenDrawerItems;
- private int notifyPriority;
- private boolean persistNotify;
-
- // Queue
- private boolean enqueueAtFront;
-
- // Playback
- private boolean pauseOnHeadsetDisconnect;
- private boolean unpauseOnHeadsetReconnect;
- private boolean followQueue;
- private boolean autoDelete;
- private int smartMarkAsPlayedSecs;
- private String[] playbackSpeedArray;
- private boolean pauseForFocusLoss;
- private boolean resumeAfterCall;
-
- // Network
- private long updateInterval;
- private boolean allowMobileUpdate;
- private int parallelDownloads;
- private int episodeCacheSize;
- private boolean enableAutodownload;
- private boolean enableAutodownloadOnBattery;
- private boolean enableAutodownloadWifiFilter;
- private String[] autodownloadSelectedNetworks;
-
- // Services
- private boolean autoFlattr;
- private float autoFlattrPlayedDurationThreshold;
-
- // Settings somewhere in the GUI
- private String playbackSpeed;
- private int fastForwardSecs;
- private int rewindSecs;
- private boolean queueLocked;
-
-
- private UserPreferences(Context context) {
- this.context = context;
- loadPreferences();
- }
+ private static Context context;
+ private static SharedPreferences prefs;
/**
* Sets up the UserPreferences class.
*
* @throws IllegalArgumentException if context is null
*/
- public static void createInstance(Context context) {
+ public static void init(@NonNull Context context) {
Log.d(TAG, "Creating new instance of UserPreferences");
- Validate.notNull(context);
- instance = new UserPreferences(context);
+ UserPreferences.context = context.getApplicationContext();
+ UserPreferences.prefs = PreferenceManager.getDefaultSharedPreferences(context);
createImportDirectory();
createNoMediaFile();
- PreferenceManager.getDefaultSharedPreferences(context)
- .registerOnSharedPreferenceChangeListener(instance);
-
- }
-
- private void loadPreferences() {
- SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
-
- // User Interface
- theme = readThemeValue(sp.getString(PREF_THEME, "0"));
- if (sp.getBoolean(PREF_EXPANDED_NOTIFICATION, false)) {
- notifyPriority = NotificationCompat.PRIORITY_MAX;
- } else {
- notifyPriority = NotificationCompat.PRIORITY_DEFAULT;
- }
- hiddenDrawerItems = Arrays.asList(StringUtils.split(sp.getString(PREF_HIDDEN_DRAWER_ITEMS, ""), ','));
- persistNotify = sp.getBoolean(PREF_PERSISTENT_NOTIFICATION, false);
-
- // Queue
- enqueueAtFront = sp.getBoolean(PREF_QUEUE_ADD_TO_FRONT, false);
-
- // Playback
- pauseOnHeadsetDisconnect = sp.getBoolean(PREF_PAUSE_ON_HEADSET_DISCONNECT, true);
- unpauseOnHeadsetReconnect = sp.getBoolean(PREF_UNPAUSE_ON_HEADSET_RECONNECT, true);
- followQueue = sp.getBoolean(PREF_FOLLOW_QUEUE, false);
- autoDelete = sp.getBoolean(PREF_AUTO_DELETE, false);
- smartMarkAsPlayedSecs = Integer.valueOf(sp.getString(PREF_SMART_MARK_AS_PLAYED_SECS, "30"));
- playbackSpeedArray = readPlaybackSpeedArray(sp.getString(
- PREF_PLAYBACK_SPEED_ARRAY, null));
- pauseForFocusLoss = sp.getBoolean(PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS, false);
-
- // Network
- updateInterval = readUpdateInterval(sp.getString(PREF_UPDATE_INTERVAL, "0"));
- allowMobileUpdate = sp.getBoolean(PREF_MOBILE_UPDATE, false);
- parallelDownloads = Integer.valueOf(sp.getString(PREF_PARALLEL_DOWNLOADS, "6"));
- EPISODE_CACHE_SIZE_UNLIMITED = context.getResources().getInteger(
- R.integer.episode_cache_size_unlimited);
- episodeCacheSize = readEpisodeCacheSizeInternal(sp.getString(PREF_EPISODE_CACHE_SIZE, "20"));
- enableAutodownload = sp.getBoolean(PREF_ENABLE_AUTODL, false);
- enableAutodownloadOnBattery = sp.getBoolean(PREF_ENABLE_AUTODL_ON_BATTERY, true);
- enableAutodownloadWifiFilter = sp.getBoolean(PREF_ENABLE_AUTODL_WIFI_FILTER, false);
- autodownloadSelectedNetworks = StringUtils.split(
- sp.getString(PREF_AUTODL_SELECTED_NETWORKS, ""), ',');
-
- // Services
- autoFlattr = sp.getBoolean(PREF_AUTO_FLATTR, false);
- autoFlattrPlayedDurationThreshold = sp.getFloat(PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD,
- PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD_DEFAULT);
-
- // MediaPlayer
- playbackSpeed = sp.getString(PREF_PLAYBACK_SPEED, "1.0");
- fastForwardSecs = sp.getInt(PREF_FAST_FORWARD_SECS, 30);
- rewindSecs = sp.getInt(PREF_REWIND_SECS, 30);
- queueLocked = sp.getBoolean(PREF_QUEUE_LOCKED, false);
- }
-
- private int readThemeValue(String valueFromPrefs) {
- switch (Integer.parseInt(valueFromPrefs)) {
- case 0:
- return R.style.Theme_AntennaPod_Light;
- case 1:
- return R.style.Theme_AntennaPod_Dark;
- default:
- return R.style.Theme_AntennaPod_Light;
- }
- }
-
- private long readUpdateInterval(String valueFromPrefs) {
- int hours = Integer.parseInt(valueFromPrefs);
- return TimeUnit.HOURS.toMillis(hours);
- }
-
- private int readEpisodeCacheSizeInternal(String valueFromPrefs) {
- if (valueFromPrefs.equals(context
- .getString(R.string.pref_episode_cache_unlimited))) {
- return EPISODE_CACHE_SIZE_UNLIMITED;
- } else {
- return Integer.valueOf(valueFromPrefs);
- }
- }
-
- private String[] readPlaybackSpeedArray(String valueFromPrefs) {
- String[] selectedSpeeds = null;
- // If this preference hasn't been set yet, return the default options
- if (valueFromPrefs == null) {
- String[] allSpeeds = context.getResources().getStringArray(
- R.array.playback_speed_values);
- List<String> speedList = new LinkedList<String>();
- for (String speedStr : allSpeeds) {
- float speed = Float.parseFloat(speedStr);
- if (speed < 2.0001 && speed * 10 % 1 == 0) {
- speedList.add(speedStr);
- }
- }
- selectedSpeeds = speedList.toArray(new String[speedList.size()]);
- } else {
- try {
- JSONArray jsonArray = new JSONArray(valueFromPrefs);
- selectedSpeeds = new String[jsonArray.length()];
- for (int i = 0; i < jsonArray.length(); i++) {
- selectedSpeeds[i] = jsonArray.getString(i);
- }
- } catch (JSONException e) {
- Log.e(TAG, "Got JSON error when trying to get speeds from JSONArray");
- e.printStackTrace();
- }
- }
- return selectedSpeeds;
- }
-
- private static void instanceAvailable() {
- if (instance == null) {
- throw new IllegalStateException("UserPreferences was used before being set up");
- }
}
/**
@@ -269,8 +139,7 @@ public class UserPreferences implements
* @return R.style.Theme_AntennaPod_Light or R.style.Theme_AntennaPod_Dark
*/
public static int getTheme() {
- instanceAvailable();
- return instance.theme;
+ return readThemeValue(prefs.getString(PREF_THEME, "0"));
}
public static int getNoTitleTheme() {
@@ -283,8 +152,18 @@ public class UserPreferences implements
}
public static List<String> getHiddenDrawerItems() {
- instanceAvailable();
- return new ArrayList<String>(instance.hiddenDrawerItems);
+ String hiddenItems = prefs.getString(PREF_HIDDEN_DRAWER_ITEMS, "");
+ return new ArrayList<>(Arrays.asList(TextUtils.split(hiddenItems, ",")));
+ }
+
+ public static int getFeedOrder() {
+ String value = prefs.getString(PREF_DRAWER_FEED_ORDER, "0");
+ return Integer.valueOf(value);
+ }
+
+ public static int getFeedCounterSetting() {
+ String value = prefs.getString(PREF_DRAWER_FEED_COUNTER, "0");
+ return Integer.valueOf(value);
}
/**
@@ -293,8 +172,11 @@ public class UserPreferences implements
* @return NotificationCompat.PRIORITY_MAX or NotificationCompat.PRIORITY_DEFAULT
*/
public static int getNotifyPriority() {
- instanceAvailable();
- return instance.notifyPriority;
+ if (prefs.getBoolean(PREF_EXPANDED_NOTIFICATION, false)) {
+ return NotificationCompat.PRIORITY_MAX;
+ } else {
+ return NotificationCompat.PRIORITY_DEFAULT;
+ }
}
/**
@@ -303,8 +185,25 @@ public class UserPreferences implements
* @return {@code true} if notifications are persistent, {@code false} otherwise
*/
public static boolean isPersistNotify() {
- instanceAvailable();
- return instance.persistNotify;
+ return prefs.getBoolean(PREF_PERSISTENT_NOTIFICATION, true);
+ }
+
+ /**
+ * Returns true if notifications are persistent
+ *
+ * @return {@code true} if notifications are persistent, {@code false} otherwise
+ */
+ public static boolean setLockscreenBackground() {
+ return prefs.getBoolean(PREF_LOCKSCREEN_BACKGROUND, true);
+ }
+
+ /**
+ * Returns true if download reports are shown
+ *
+ * @return {@code true} if download reports are shown, {@code false} otherwise
+ */
+ public static boolean showDownloadReport() {
+ return prefs.getBoolean(PREF_SHOW_DOWNLOAD_REPORT, true);
}
/**
@@ -313,73 +212,107 @@ public class UserPreferences implements
* @return {@code true} if new queue elements are added to the front; {@code false} otherwise
*/
public static boolean enqueueAtFront() {
- instanceAvailable();
- return instance.enqueueAtFront;
+ return prefs.getBoolean(PREF_QUEUE_ADD_TO_FRONT, false);
}
public static boolean isPauseOnHeadsetDisconnect() {
- instanceAvailable();
- return instance.pauseOnHeadsetDisconnect;
+ return prefs.getBoolean(PREF_PAUSE_ON_HEADSET_DISCONNECT, true);
}
public static boolean isUnpauseOnHeadsetReconnect() {
- instanceAvailable();
- return instance.unpauseOnHeadsetReconnect;
+ return prefs.getBoolean(PREF_UNPAUSE_ON_HEADSET_RECONNECT, true);
+ }
+
+ public static boolean isUnpauseOnBluetoothReconnect() {
+ return prefs.getBoolean(PREF_UNPAUSE_ON_BLUETOOTH_RECONNECT, false);
+ }
+
+ public static boolean shouldHardwareButtonSkip() {
+ return prefs.getBoolean(PREF_HARDWARE_FOWARD_BUTTON_SKIPS, false);
}
public static boolean isFollowQueue() {
- instanceAvailable();
- return instance.followQueue;
+ return prefs.getBoolean(PREF_FOLLOW_QUEUE, true);
}
+ public static boolean shouldSkipKeepEpisode() { return prefs.getBoolean(PREF_SKIP_KEEPS_EPISODE, true); }
+
public static boolean isAutoDelete() {
- instanceAvailable();
- return instance.autoDelete;
+ return prefs.getBoolean(PREF_AUTO_DELETE, false);
}
public static int getSmartMarkAsPlayedSecs() {
- instanceAvailable();
- return instance.smartMarkAsPlayedSecs;
+ return Integer.valueOf(prefs.getString(PREF_SMART_MARK_AS_PLAYED_SECS, "30"));
}
public static boolean isAutoFlattr() {
- instanceAvailable();
- return instance.autoFlattr;
+ return prefs.getBoolean(PREF_AUTO_FLATTR, false);
}
public static String getPlaybackSpeed() {
- instanceAvailable();
- return instance.playbackSpeed;
+ return prefs.getString(PREF_PLAYBACK_SPEED, "1.00");
}
public static String[] getPlaybackSpeedArray() {
- instanceAvailable();
- return instance.playbackSpeedArray;
+ return readPlaybackSpeedArray(prefs.getString(PREF_PLAYBACK_SPEED_ARRAY, null));
+ }
+
+ public static float getLeftVolume() {
+ int volume = prefs.getInt(PREF_LEFT_VOLUME, 100);
+ if(volume == 100) {
+ return 1.0f;
+ } else {
+ return (float) (1 - (Math.log(100 - volume) / Math.log(100)));
+ }
+ }
+
+ public static float getRightVolume() {
+ int volume = prefs.getInt(PREF_RIGHT_VOLUME, 100);
+ if(volume == 100) {
+ return 1.0f;
+ } else {
+ return (float) (1 - (Math.log(100 - volume) / Math.log(100)));
+ }
}
public static boolean shouldPauseForFocusLoss() {
- instanceAvailable();
- return instance.pauseForFocusLoss;
+ return prefs.getBoolean(PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS, false);
}
+
+
public static long getUpdateInterval() {
- instanceAvailable();
- return instance.updateInterval;
+ String updateInterval = prefs.getString(PREF_UPDATE_INTERVAL, "0");
+ if(false == updateInterval.contains(":")) {
+ return readUpdateInterval(updateInterval);
+ } else {
+ return 0;
+ }
+ }
+
+ public static int[] getUpdateTimeOfDay() {
+ String datetime = prefs.getString(PREF_UPDATE_INTERVAL, "");
+ if(datetime.length() >= 3 && datetime.contains(":")) {
+ String[] parts = datetime.split(":");
+ int hourOfDay = Integer.valueOf(parts[0]);
+ int minute = Integer.valueOf(parts[1]);
+ return new int[] { hourOfDay, minute };
+ } else {
+ return new int[0];
+ }
}
public static boolean isAllowMobileUpdate() {
- instanceAvailable();
- return instance.allowMobileUpdate;
+ return prefs.getBoolean(PREF_MOBILE_UPDATE, false);
}
public static int getParallelDownloads() {
- instanceAvailable();
- return instance.parallelDownloads;
+ return Integer.valueOf(prefs.getString(PREF_PARALLEL_DOWNLOADS, "4"));
}
public static int getEpisodeCacheSizeUnlimited() {
- return EPISODE_CACHE_SIZE_UNLIMITED;
+ return context.getResources().getInteger(R.integer.episode_cache_size_unlimited);
}
/**
@@ -388,33 +321,40 @@ public class UserPreferences implements
* 'unlimited'.
*/
public static int getEpisodeCacheSize() {
- instanceAvailable();
- return instance.episodeCacheSize;
+ return readEpisodeCacheSizeInternal(prefs.getString(PREF_EPISODE_CACHE_SIZE, "20"));
}
public static boolean isEnableAutodownload() {
- instanceAvailable();
- return instance.enableAutodownload;
+ return prefs.getBoolean(PREF_ENABLE_AUTODL, false);
}
public static boolean isEnableAutodownloadOnBattery() {
- instanceAvailable();
- return instance.enableAutodownloadOnBattery;
+ return prefs.getBoolean(PREF_ENABLE_AUTODL_ON_BATTERY, true);
}
public static boolean isEnableAutodownloadWifiFilter() {
- instanceAvailable();
- return instance.enableAutodownloadWifiFilter;
+ return prefs.getBoolean(PREF_ENABLE_AUTODL_WIFI_FILTER, false);
+ }
+
+ public static int getImageCacheSize() {
+ String cacheSizeString = prefs.getString(PREF_IMAGE_CACHE_SIZE, IMAGE_CACHE_DEFAULT_VALUE);
+ int cacheSizeInt = Integer.valueOf(cacheSizeString);
+ // if the cache size is too small the user won't get any images at all
+ // that's bad, force it back to the default.
+ if (cacheSizeInt < IMAGE_CACHE_SIZE_MINIMUM) {
+ prefs.edit().putString(PREF_IMAGE_CACHE_SIZE, IMAGE_CACHE_DEFAULT_VALUE).apply();
+ cacheSizeInt = Integer.valueOf(IMAGE_CACHE_DEFAULT_VALUE);
+ }
+ int cacheSizeMB = cacheSizeInt * 1024 * 1024;
+ return cacheSizeMB;
}
public static int getFastFowardSecs() {
- instanceAvailable();
- return instance.fastForwardSecs;
+ return prefs.getInt(PREF_FAST_FORWARD_SECS, 30);
}
public static int getRewindSecs() {
- instanceAvailable();
- return instance.rewindSecs;
+ return prefs.getInt(PREF_REWIND_SECS, 30);
}
@@ -423,145 +363,38 @@ public class UserPreferences implements
* duration.
*/
public static float getAutoFlattrPlayedDurationThreshold() {
- instanceAvailable();
- return instance.autoFlattrPlayedDurationThreshold;
+ return prefs.getFloat(PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD, 0.8f);
}
public static String[] getAutodownloadSelectedNetworks() {
- instanceAvailable();
- return instance.autodownloadSelectedNetworks;
+ String selectedNetWorks = prefs.getString(PREF_AUTODL_SELECTED_NETWORKS, "");
+ return TextUtils.split(selectedNetWorks, ",");
}
public static boolean shouldResumeAfterCall() {
- instanceAvailable();
- return instance.resumeAfterCall;
+ return prefs.getBoolean(PREF_RESUME_AFTER_CALL, true);
}
public static boolean isQueueLocked() {
- instanceAvailable();
- return instance.queueLocked;
- }
-
- @Override
- public void onSharedPreferenceChanged(SharedPreferences sp, String key) {
- Log.d(TAG, "Registered change of user preferences. Key: " + key);
- switch(key) {
- // User Interface
- case PREF_THEME:
- theme = readThemeValue(sp.getString(PREF_THEME, ""));
- break;
- case PREF_HIDDEN_DRAWER_ITEMS:
- hiddenDrawerItems = Arrays.asList(StringUtils.split(sp.getString(PREF_HIDDEN_DRAWER_ITEMS, ""), ','));
- break;
- case PREF_EXPANDED_NOTIFICATION:
- if (sp.getBoolean(PREF_EXPANDED_NOTIFICATION, false)) {
- notifyPriority = NotificationCompat.PRIORITY_MAX;
- } else {
- notifyPriority = NotificationCompat.PRIORITY_DEFAULT;
- }
- break;
- case PREF_PERSISTENT_NOTIFICATION:
- persistNotify = sp.getBoolean(PREF_PERSISTENT_NOTIFICATION, false);
- break;
- // Queue
- case PREF_QUEUE_ADD_TO_FRONT:
- enqueueAtFront = sp.getBoolean(PREF_QUEUE_ADD_TO_FRONT, false);
- break;
- // Playback
- case PREF_PAUSE_ON_HEADSET_DISCONNECT:
- pauseOnHeadsetDisconnect = sp.getBoolean(PREF_PAUSE_ON_HEADSET_DISCONNECT, true);
- break;
- case PREF_UNPAUSE_ON_HEADSET_RECONNECT:
- unpauseOnHeadsetReconnect = sp.getBoolean(PREF_UNPAUSE_ON_HEADSET_RECONNECT, true);
- break;
- case PREF_FOLLOW_QUEUE:
- followQueue = sp.getBoolean(PREF_FOLLOW_QUEUE, false);
- break;
- case PREF_AUTO_DELETE:
- autoDelete = sp.getBoolean(PREF_AUTO_DELETE, false);
- break;
- case PREF_SMART_MARK_AS_PLAYED_SECS:
- smartMarkAsPlayedSecs = Integer.valueOf(sp.getString(PREF_SMART_MARK_AS_PLAYED_SECS, "30"));
- break;
- case PREF_PLAYBACK_SPEED_ARRAY:
- playbackSpeedArray = readPlaybackSpeedArray(sp.getString(PREF_PLAYBACK_SPEED_ARRAY, null));
- break;
- case PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS:
- pauseForFocusLoss = sp.getBoolean(PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS, false);
- break;
- case PREF_RESUME_AFTER_CALL:
- resumeAfterCall = sp.getBoolean(PREF_RESUME_AFTER_CALL, true);
- break;
- // Network
- case PREF_UPDATE_INTERVAL:
- updateInterval = readUpdateInterval(sp.getString(PREF_UPDATE_INTERVAL, "0"));
- ClientConfig.applicationCallbacks.setUpdateInterval(updateInterval);
- break;
- case PREF_MOBILE_UPDATE:
- allowMobileUpdate = sp.getBoolean(PREF_MOBILE_UPDATE, false);
- break;
- case PREF_PARALLEL_DOWNLOADS:
- parallelDownloads = Integer.valueOf(sp.getString(PREF_PARALLEL_DOWNLOADS, "6"));
- break;
- case PREF_EPISODE_CACHE_SIZE:
- episodeCacheSize = readEpisodeCacheSizeInternal(sp.getString(PREF_EPISODE_CACHE_SIZE, "20"));
- break;
- case PREF_ENABLE_AUTODL:
- enableAutodownload = sp.getBoolean(PREF_ENABLE_AUTODL, false);
- break;
- case PREF_ENABLE_AUTODL_ON_BATTERY:
- enableAutodownloadOnBattery = sp.getBoolean(PREF_ENABLE_AUTODL_ON_BATTERY, true);
- break;
- case PREF_ENABLE_AUTODL_WIFI_FILTER:
- enableAutodownloadWifiFilter = sp.getBoolean(PREF_ENABLE_AUTODL_WIFI_FILTER, false);
- break;
- case PREF_AUTODL_SELECTED_NETWORKS:
- autodownloadSelectedNetworks = StringUtils.split(
- sp.getString(PREF_AUTODL_SELECTED_NETWORKS, ""), ',');
- break;
- // Services
- case PREF_AUTO_FLATTR:
- autoFlattr = sp.getBoolean(PREF_AUTO_FLATTR, false);
- break;
- case PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD:
- autoFlattrPlayedDurationThreshold = sp.getFloat(PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD,
- PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD_DEFAULT);
- break;
- // Mediaplayer
- case PREF_PLAYBACK_SPEED:
- playbackSpeed = sp.getString(PREF_PLAYBACK_SPEED, "1.0");
- break;
- case PREF_FAST_FORWARD_SECS:
- fastForwardSecs = sp.getInt(PREF_FAST_FORWARD_SECS, 30);
- break;
- case PREF_REWIND_SECS:
- rewindSecs = sp.getInt(PREF_REWIND_SECS, 30);
- break;
- case PREF_QUEUE_LOCKED:
- queueLocked = sp.getBoolean(PREF_QUEUE_LOCKED, false);
- break;
- default:
- Log.w(TAG, "Unhandled key: " + key);
- }
+ return prefs.getBoolean(PREF_QUEUE_LOCKED, false);
}
public static void setPrefFastForwardSecs(int secs) {
- Log.d(TAG, "setPrefFastForwardSecs(" + secs +")");
- SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(instance.context).edit();
- editor.putInt(PREF_FAST_FORWARD_SECS, secs);
- editor.commit();
+ prefs.edit()
+ .putInt(PREF_FAST_FORWARD_SECS, secs)
+ .apply();
}
public static void setPrefRewindSecs(int secs) {
- Log.d(TAG, "setPrefRewindSecs(" + secs +")");
- SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(instance.context).edit();
- editor.putInt(PREF_REWIND_SECS, secs);
- editor.commit();
+ prefs.edit()
+ .putInt(PREF_REWIND_SECS, secs)
+ .apply();
}
public static void setPlaybackSpeed(String speed) {
- PreferenceManager.getDefaultSharedPreferences(instance.context).edit()
- .putString(PREF_PLAYBACK_SPEED, speed).apply();
+ prefs.edit()
+ .putString(PREF_PLAYBACK_SPEED, speed)
+ .apply();
}
public static void setPlaybackSpeedArray(String[] speeds) {
@@ -569,74 +402,161 @@ public class UserPreferences implements
for (String speed : speeds) {
jsonArray.put(speed);
}
- PreferenceManager.getDefaultSharedPreferences(instance.context).edit()
- .putString(PREF_PLAYBACK_SPEED_ARRAY, jsonArray.toString())
- .apply();
+ prefs.edit()
+ .putString(PREF_PLAYBACK_SPEED_ARRAY, jsonArray.toString())
+ .apply();
+ }
+
+ public static void setVolume(int leftVolume, int rightVolume) {
+ assert(0 <= leftVolume && leftVolume <= 100);
+ assert(0 <= rightVolume && rightVolume <= 100);
+ prefs.edit()
+ .putInt(PREF_LEFT_VOLUME, leftVolume)
+ .putInt(PREF_RIGHT_VOLUME, rightVolume)
+ .apply();
+ }
+
+ public static void setAutodownloadSelectedNetworks(String[] value) {
+ prefs.edit()
+ .putString(PREF_AUTODL_SELECTED_NETWORKS, TextUtils.join(",", value))
+ .apply();
}
- public static void setAutodownloadSelectedNetworks(Context context,
- String[] value) {
- SharedPreferences.Editor editor = PreferenceManager
- .getDefaultSharedPreferences(context.getApplicationContext())
- .edit();
- editor.putString(PREF_AUTODL_SELECTED_NETWORKS,
- StringUtils.join(value, ','));
- editor.commit();
+ /**
+ * Sets the update interval value.
+ */
+ public static void setUpdateInterval(long hours) {
+ prefs.edit()
+ .putString(PREF_UPDATE_INTERVAL, String.valueOf(hours))
+ .apply();
+ // when updating with an interval, we assume the user wants
+ // to update *now* and then every 'hours' interval thereafter.
+ restartUpdateAlarm(true);
}
/**
- * Sets the update interval value. Should only be used for testing purposes!
+ * Sets the update interval value.
*/
- public static void setUpdateInterval(Context context, long newValue) {
- instanceAvailable();
- SharedPreferences.Editor editor = PreferenceManager
- .getDefaultSharedPreferences(context.getApplicationContext())
- .edit();
- editor.putString(PREF_UPDATE_INTERVAL,
- String.valueOf(newValue));
- editor.commit();
- instance.updateInterval = newValue;
+ public static void setUpdateTimeOfDay(int hourOfDay, int minute) {
+ prefs.edit()
+ .putString(PREF_UPDATE_INTERVAL, hourOfDay + ":" + minute)
+ .apply();
+ restartUpdateAlarm(false);
}
/**
* Change the auto-flattr settings
*
- * @param context For accessing the shared preferences
* @param enabled Whether automatic flattring should be enabled at all
* @param autoFlattrThreshold The percentage of playback time after which an episode should be
* flattrd. Must be a value between 0 and 1 (inclusive)
* */
- public static void setAutoFlattrSettings(Context context, boolean enabled, float autoFlattrThreshold) {
- instanceAvailable();
- Validate.inclusiveBetween(0.0, 1.0, autoFlattrThreshold);
- PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext())
- .edit()
- .putBoolean(PREF_AUTO_FLATTR, enabled)
- .putFloat(PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD, autoFlattrThreshold)
- .commit();
- instance.autoFlattr = enabled;
- instance.autoFlattrPlayedDurationThreshold = autoFlattrThreshold;
- }
-
- public static void setHiddenDrawerItems(Context context, List<String> items) {
- instanceAvailable();
- instance.hiddenDrawerItems = items;
- String str = StringUtils.join(items, ',');
- PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext())
- .edit()
- .putString(PREF_HIDDEN_DRAWER_ITEMS, str)
- .commit();
+ public static void setAutoFlattrSettings( boolean enabled, float autoFlattrThreshold) {
+ if(autoFlattrThreshold < 0.0 || autoFlattrThreshold > 1.0) {
+ throw new IllegalArgumentException("Flattr threshold must be in range [0.0, 1.0]");
+ }
+ prefs.edit()
+ .putBoolean(PREF_AUTO_FLATTR, enabled)
+ .putFloat(PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD, autoFlattrThreshold)
+ .apply();
+ }
+
+ public static void setHiddenDrawerItems(List<String> items) {
+ String str = TextUtils.join(",", items);
+ prefs.edit()
+ .putString(PREF_HIDDEN_DRAWER_ITEMS, str)
+ .apply();
}
public static void setQueueLocked(boolean locked) {
- instanceAvailable();
- instance.queueLocked = locked;
- PreferenceManager.getDefaultSharedPreferences(instance.context)
- .edit()
- .putBoolean(PREF_QUEUE_LOCKED, locked)
- .commit();
+ prefs.edit()
+ .putBoolean(PREF_QUEUE_LOCKED, locked)
+ .apply();
+ }
+
+ private static int readThemeValue(String valueFromPrefs) {
+ switch (Integer.parseInt(valueFromPrefs)) {
+ case 0:
+ return R.style.Theme_AntennaPod_Light;
+ case 1:
+ return R.style.Theme_AntennaPod_Dark;
+ default:
+ return R.style.Theme_AntennaPod_Light;
+ }
}
+ private static long readUpdateInterval(String valueFromPrefs) {
+ int hours = Integer.parseInt(valueFromPrefs);
+ return TimeUnit.HOURS.toMillis(hours);
+ }
+
+ private static int readEpisodeCacheSizeInternal(String valueFromPrefs) {
+ if (valueFromPrefs.equals(context.getString(R.string.pref_episode_cache_unlimited))) {
+ return EPISODE_CACHE_SIZE_UNLIMITED;
+ } else {
+ return Integer.valueOf(valueFromPrefs);
+ }
+ }
+
+ private static String[] readPlaybackSpeedArray(String valueFromPrefs) {
+ String[] selectedSpeeds = null;
+ // If this preference hasn't been set yet, return the default options
+ if (valueFromPrefs == null) {
+ String[] allSpeeds = context.getResources().getStringArray(R.array.playback_speed_values);
+ List<String> speedList = new ArrayList<>();
+ for (String speedStr : allSpeeds) {
+ float speed = Float.parseFloat(speedStr);
+ if (speed < 2.0001 && speed * 10 % 1 == 0) {
+ speedList.add(speedStr);
+ }
+ }
+ selectedSpeeds = speedList.toArray(new String[speedList.size()]);
+ } else {
+ try {
+ JSONArray jsonArray = new JSONArray(valueFromPrefs);
+ selectedSpeeds = new String[jsonArray.length()];
+ for (int i = 0; i < jsonArray.length(); i++) {
+ selectedSpeeds[i] = jsonArray.getString(i);
+ }
+ } catch (JSONException e) {
+ Log.e(TAG, "Got JSON error when trying to get speeds from JSONArray");
+ e.printStackTrace();
+ }
+ }
+ return selectedSpeeds;
+ }
+
+ public static boolean useSonic() {
+ return prefs.getBoolean(PREF_SONIC, false);
+ }
+
+ public static void enableSonic(boolean enable) {
+ prefs.edit()
+ .putBoolean(PREF_SONIC, enable)
+ .apply();
+ }
+
+ public static boolean stereoToMono() {
+ return prefs.getBoolean(PREF_STEREO_TO_MONO, false);
+ }
+
+ public static void stereoToMono(boolean enable) {
+ prefs.edit()
+ .putBoolean(PREF_STEREO_TO_MONO, enable)
+ .apply();
+ }
+
+
+ public static EpisodeCleanupAlgorithm getEpisodeCleanupAlgorithm() {
+ int cleanupValue = Integer.valueOf(prefs.getString(PREF_EPISODE_CLEANUP, "-1"));
+ if (cleanupValue == EPISODE_CLEANUP_QUEUE) {
+ return new APQueueCleanupAlgorithm();
+ } else if (cleanupValue == EPISODE_CLEANUP_NULL) {
+ return new APNullCleanupAlgorithm();
+ } else {
+ return new APCleanupAlgorithm(cleanupValue);
+ }
+ }
/**
* Return the folder where the app stores all of its data. This method will
@@ -647,10 +567,7 @@ public class UserPreferences implements
* @return The data folder that has been requested or null if the folder
* could not be created.
*/
- public static File getDataFolder(Context context, String type) {
- instanceAvailable();
- SharedPreferences prefs = PreferenceManager
- .getDefaultSharedPreferences(context.getApplicationContext());
+ public static File getDataFolder(String type) {
String strDir = prefs.getString(PREF_DATA_FOLDER, null);
if (strDir == null) {
Log.d(TAG, "Using default data folder");
@@ -672,7 +589,7 @@ public class UserPreferences implements
for (int i = 0; i < dirs.length; i++) {
if (dirs.length > 0) {
if (i < dirs.length - 1) {
- dataDir = getDataFolder(context, dirs[i]);
+ dataDir = getDataFolder(dirs[i]);
if (dataDir == null) {
return null;
}
@@ -695,13 +612,10 @@ public class UserPreferences implements
}
public static void setDataFolder(String dir) {
- Log.d(TAG, "Result from DirectoryChooser: " + dir);
- instanceAvailable();
- SharedPreferences prefs = PreferenceManager
- .getDefaultSharedPreferences(instance.context);
- SharedPreferences.Editor editor = prefs.edit();
- editor.putString(PREF_DATA_FOLDER, dir);
- editor.commit();
+ Log.d(TAG, "setDataFolder(dir: " + dir + ")");
+ prefs.edit()
+ .putString(PREF_DATA_FOLDER, dir)
+ .apply();
createImportDirectory();
}
@@ -709,8 +623,7 @@ public class UserPreferences implements
* Create a .nomedia file to prevent scanning by the media scanner.
*/
private static void createNoMediaFile() {
- File f = new File(instance.context.getExternalFilesDir(null),
- ".nomedia");
+ File f = new File(context.getExternalFilesDir(null), ".nomedia");
if (!f.exists()) {
try {
f.createNewFile();
@@ -727,8 +640,7 @@ public class UserPreferences implements
* available
*/
private static void createImportDirectory() {
- File importDir = getDataFolder(instance.context,
- IMPORT_DIR);
+ File importDir = getDataFolder(IMPORT_DIR);
if (importDir != null) {
if (importDir.exists()) {
Log.d(TAG, "Import directory already exists");
@@ -741,32 +653,68 @@ public class UserPreferences implements
}
}
+ public static void restartUpdateAlarm(boolean now) {
+ int[] timeOfDay = getUpdateTimeOfDay();
+ Log.d(TAG, "timeOfDay: " + Arrays.toString(timeOfDay));
+ if (timeOfDay.length == 2) {
+ restartUpdateTimeOfDayAlarm(timeOfDay[0], timeOfDay[1]);
+ } else {
+ long hours = getUpdateInterval();
+ long startTrigger = hours;
+ if (now) {
+ startTrigger = TimeUnit.SECONDS.toMillis(10);
+ }
+ restartUpdateIntervalAlarm(startTrigger, hours);
+ }
+ }
+
/**
- * Updates alarm registered with the AlarmManager service or deactivates it.
+ * Sets the interval in which the feeds are refreshed automatically
*/
- public static void restartUpdateAlarm(long triggerAtMillis, long intervalMillis) {
- instanceAvailable();
+ public static void restartUpdateIntervalAlarm(long triggerAtMillis, long intervalMillis) {
Log.d(TAG, "Restarting update alarm.");
- AlarmManager alarmManager = (AlarmManager) instance.context
- .getSystemService(Context.ALARM_SERVICE);
- PendingIntent updateIntent = PendingIntent.getBroadcast(
- instance.context, 0, new Intent(ClientConfig.applicationCallbacks.getApplicationInstance(), FeedUpdateReceiver.class), 0);
+ 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.setRepeating(AlarmManager.RTC_WAKEUP, triggerAtMillis, intervalMillis,
+ if (intervalMillis > 0) {
+ alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
+ SystemClock.elapsedRealtime() + triggerAtMillis,
updateIntent);
- Log.d(TAG, "Changed alarm to new interval");
+ 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) {
- instanceAvailable();
- return instance.readEpisodeCacheSizeInternal(valueFromPrefs);
+ return readEpisodeCacheSizeInternal(valueFromPrefs);
}
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/receiver/AlarmUpdateReceiver.java b/core/src/main/java/de/danoeh/antennapod/core/receiver/AlarmUpdateReceiver.java
index 84277b6d5..ce5004a98 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/receiver/AlarmUpdateReceiver.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/receiver/AlarmUpdateReceiver.java
@@ -3,32 +3,28 @@ package de.danoeh.antennapod.core.receiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
+import android.text.TextUtils;
import android.util.Log;
-import org.apache.commons.lang3.StringUtils;
-
-import de.danoeh.antennapod.core.BuildConfig;
-import de.danoeh.antennapod.core.ClientConfig;
+import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
import de.danoeh.antennapod.core.preferences.UserPreferences;
/** Listens for events that make it necessary to reset the update alarm. */
public class AlarmUpdateReceiver extends BroadcastReceiver {
+
private static final String TAG = "AlarmUpdateReceiver";
@Override
public void onReceive(Context context, Intent intent) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Received intent");
- if (StringUtils.equals(intent.getAction(), Intent.ACTION_BOOT_COMPLETED)) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Resetting update alarm after reboot");
- } else if (StringUtils.equals(intent.getAction(), Intent.ACTION_PACKAGE_REPLACED)) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Resetting update alarm after app upgrade");
+ Log.d(TAG, "Received intent");
+ if (TextUtils.equals(intent.getAction(), Intent.ACTION_BOOT_COMPLETED)) {
+ Log.d(TAG, "Resetting update alarm after reboot");
+ } else if (TextUtils.equals(intent.getAction(), Intent.ACTION_PACKAGE_REPLACED)) {
+ Log.d(TAG, "Resetting update alarm after app upgrade");
}
-
- ClientConfig.applicationCallbacks.setUpdateInterval(UserPreferences.getUpdateInterval());
-
+ PlaybackPreferences.init(context);
+ UserPreferences.init(context);
+ UserPreferences.restartUpdateAlarm(false);
}
}
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 d37f97a5f..b959c7301 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
@@ -5,6 +5,7 @@ import android.content.Context;
import android.content.Intent;
import android.util.Log;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.storage.DBTasks;
import de.danoeh.antennapod.core.util.NetworkUtils;
@@ -18,11 +19,12 @@ public class FeedUpdateReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Log.d(TAG, "Received intent");
- if (NetworkUtils.isDownloadAllowed(context)) {
- DBTasks.refreshExpiredFeeds(context);
+ if (NetworkUtils.isDownloadAllowed()) {
+ DBTasks.refreshAllFeeds(context, null);
} else {
Log.d(TAG, "Blocking automatic update: no wifi available / no mobile updates allowed");
}
+ UserPreferences.restartUpdateAlarm(false);
}
}
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 3f2222f42..0b90cef6c 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
@@ -8,12 +8,12 @@ import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import android.support.v4.app.NotificationCompat;
+import android.support.v4.util.ArrayMap;
import android.util.Log;
import android.util.Pair;
import java.util.Collection;
import java.util.Date;
-import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -108,7 +108,7 @@ public class GpodnetSyncService extends Service {
private synchronized void sync() {
- if (GpodnetPreferences.loggedIn() == false || NetworkUtils.networkAvailable(this) == false) {
+ if (GpodnetPreferences.loggedIn() == false || NetworkUtils.networkAvailable() == false) {
stopSelf();
return;
}
@@ -126,7 +126,7 @@ public class GpodnetSyncService extends Service {
private synchronized void syncSubscriptionChanges() {
final long timestamp = GpodnetPreferences.getLastSubscriptionSyncTimestamp();
try {
- final List<String> localSubscriptions = DBReader.getFeedListDownloadUrls(this);
+ final List<String> localSubscriptions = DBReader.getFeedListDownloadUrls();
Collection<String> localAdded = GpodnetPreferences.getAddedFeedsCopy();
Collection<String> localRemoved = GpodnetPreferences.getRemovedFeedsCopy();
GpodnetService service = tryLogin();
@@ -226,11 +226,11 @@ public class GpodnetSyncService extends Service {
if(remoteActions.size() == 0) {
return;
}
- Map<Pair<String, String>, GpodnetEpisodeAction> localMostRecentPlayAction = new HashMap<Pair<String, String>, GpodnetEpisodeAction>();
+ Map<Pair<String, String>, GpodnetEpisodeAction> localMostRecentPlayAction = new ArrayMap<>();
for(GpodnetEpisodeAction action : localActions) {
Pair key = new Pair(action.getPodcast(), action.getEpisode());
GpodnetEpisodeAction mostRecent = localMostRecentPlayAction.get(key);
- if (mostRecent == null) {
+ if (mostRecent == null || mostRecent.getTimestamp() == null) {
localMostRecentPlayAction.put(key, action);
} else if (mostRecent.getTimestamp().before(action.getTimestamp())) {
localMostRecentPlayAction.put(key, action);
@@ -238,13 +238,13 @@ public class GpodnetSyncService extends Service {
}
// make sure more recent local actions are not overwritten by older remote actions
- Map<Pair<String, String>, GpodnetEpisodeAction> mostRecentPlayAction = new HashMap<Pair<String, String>, GpodnetEpisodeAction>();
+ Map<Pair<String, String>, GpodnetEpisodeAction> mostRecentPlayAction = new ArrayMap<>();
for (GpodnetEpisodeAction action : remoteActions) {
switch (action.getAction()) {
case NEW:
- FeedItem newItem = DBReader.getFeedItem(this, action.getPodcast(), action.getEpisode());
+ FeedItem newItem = DBReader.getFeedItem(action.getPodcast(), action.getEpisode());
if(newItem != null) {
- DBWriter.markItemRead(this, newItem, false, true);
+ DBWriter.markItemPlayed(newItem, FeedItem.UNPLAYED, true);
} else {
Log.i(TAG, "Unknown feed item: " + action);
}
@@ -255,12 +255,15 @@ public class GpodnetSyncService extends Service {
Pair key = new Pair(action.getPodcast(), action.getEpisode());
GpodnetEpisodeAction localMostRecent = localMostRecentPlayAction.get(key);
if(localMostRecent == null ||
+ localMostRecent.getTimestamp() == null ||
localMostRecent.getTimestamp().before(action.getTimestamp())) {
GpodnetEpisodeAction mostRecent = mostRecentPlayAction.get(key);
- if (mostRecent == null) {
+ if (mostRecent == null || mostRecent.getTimestamp() == null) {
mostRecentPlayAction.put(key, action);
- } else if (mostRecent.getTimestamp().before(action.getTimestamp())) {
+ } else if (action.getTimestamp() != null && mostRecent.getTimestamp().before(action.getTimestamp())) {
mostRecentPlayAction.put(key, action);
+ } else {
+ Log.d(TAG, "No date information in action, skipping it");
}
}
break;
@@ -270,14 +273,14 @@ public class GpodnetSyncService extends Service {
}
}
for (GpodnetEpisodeAction action : mostRecentPlayAction.values()) {
- FeedItem playItem = DBReader.getFeedItem(this, action.getPodcast(), action.getEpisode());
+ FeedItem playItem = DBReader.getFeedItem(action.getPodcast(), action.getEpisode());
if (playItem != null) {
FeedMedia media = playItem.getMedia();
media.setPosition(action.getPosition() * 1000);
- DBWriter.setFeedMedia(this, media);
+ DBWriter.setFeedMedia(media);
if(playItem.getMedia().hasAlmostEnded()) {
- DBWriter.markItemRead(this, playItem, true, true);
- DBWriter.addItemToPlaybackHistory(this, playItem.getMedia());
+ DBWriter.markItemPlayed(playItem, FeedItem.PLAYED, true);
+ DBWriter.addItemToPlaybackHistory(playItem.getMedia());
}
}
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/APRedirectHandler.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/APRedirectHandler.java
deleted file mode 100644
index 3efcf4da8..000000000
--- a/core/src/main/java/de/danoeh/antennapod/core/service/download/APRedirectHandler.java
+++ /dev/null
@@ -1,54 +0,0 @@
-package de.danoeh.antennapod.core.service.download;
-
-import android.util.Log;
-import de.danoeh.antennapod.core.BuildConfig;
-import org.apache.http.Header;
-import org.apache.http.HttpResponse;
-import org.apache.http.impl.client.DefaultRedirectHandler;
-import org.apache.http.protocol.HttpContext;
-
-import java.net.URI;
-
-public class APRedirectHandler extends DefaultRedirectHandler {
- // Identifier for logger
- private static final String TAG = "APRedirectHandler";
- // Header field, which has to be potentially fixed
- private static final String LOC = "Location";
- // Regular expressions for character strings, which should not appear in URLs
- private static final String CHi[] = { "\\{", "\\}", "\\|", "\\\\", "\\^", "~", "\\[", "\\]", "\\`"};
- private static final String CHo[] = { "%7B", "%7D", "%7C", "%5C", "%5E", "%7E", "%5B", "%5D", "%60"};
-
- /**
- * Workaround for broken URLs in redirection.
- * Proper solution involves LaxRedirectStrategy() which is not available in
- * current API yet.
- */
- @Override
- public URI getLocationURI(HttpResponse response, HttpContext context)
- throws org.apache.http.ProtocolException {
-
- Header h[] = response.getHeaders(LOC);
- if (h.length>0) {
- String s = h[0].getValue();
-
- // Fix broken URL
- for(int i=0; i<CHi.length;i++)
- s = s.replaceAll(CHi[i], CHo[i]);
-
- // If anything had to be fixed, then replace the header
- if (!s.equals(h[0].getValue()))
- {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Original URL: " + h[0].getValue());
-
- response.setHeader(LOC, s);
-
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Fixed URL: " + s);
- }
- }
-
- // call DefaultRedirectHandler with fixed URL
- return super.getLocationURI(response, context);
- }
-}
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 ec3d3e2fe..b23819ef7 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
@@ -1,14 +1,29 @@
package de.danoeh.antennapod.core.service.download;
+import android.os.Build;
+import android.support.annotation.NonNull;
import android.util.Log;
import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
+import com.squareup.okhttp.internal.http.StatusLine;
+import java.io.IOException;
import java.net.CookieManager;
import java.net.CookiePolicy;
+import java.net.HttpURLConnection;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.URL;
+import java.security.GeneralSecurityException;
import java.util.concurrent.TimeUnit;
-import de.danoeh.antennapod.core.BuildConfig;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+
+import de.danoeh.antennapod.core.storage.DBWriter;
/**
* Provides access to a HttpClient singleton.
@@ -30,32 +45,74 @@ public class AntennapodHttpClient {
public static synchronized OkHttpClient getHttpClient() {
if (httpClient == null) {
- if (BuildConfig.DEBUG) Log.d(TAG, "Creating new instance of HTTP client");
-
- System.setProperty("http.maxConnections", String.valueOf(MAX_CONNECTIONS));
-
- OkHttpClient client = new OkHttpClient();
-
- // set cookie handler
- CookieManager cm = new CookieManager();
- cm.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
- client.setCookieHandler(cm);
-
- // set timeouts
- client.setConnectTimeout(CONNECTION_TIMEOUT, TimeUnit.MILLISECONDS);
- client.setReadTimeout(READ_TIMEOUT, TimeUnit.MILLISECONDS);
- client.setWriteTimeout(READ_TIMEOUT, TimeUnit.MILLISECONDS);
-
- // configure redirects
- client.setFollowRedirects(true);
- client.setFollowSslRedirects(true);
-
- httpClient = client;
+ httpClient = newHttpClient();
}
return httpClient;
}
/**
+ * Creates a new HTTP client. Most users should just use
+ * getHttpClient() to get the standard AntennaPod client,
+ * but sometimes it's necessary for others to have their own
+ * copy so that the clients don't share state.
+ * @return http client
+ */
+ @NonNull
+ public static OkHttpClient newHttpClient() {
+ Log.d(TAG, "Creating new instance of HTTP client");
+
+ System.setProperty("http.maxConnections", String.valueOf(MAX_CONNECTIONS));
+
+ OkHttpClient client = new OkHttpClient();
+
+ // detect 301 Moved permanently and 308 Permanent Redirect
+ client.networkInterceptors().add(chain -> {
+ Request request = chain.request();
+ Response response = chain.proceed(request);
+ if(response.code() == HttpURLConnection.HTTP_MOVED_PERM ||
+ response.code() == StatusLine.HTTP_PERM_REDIRECT) {
+ String location = response.header("Location");
+ if(location.startsWith("/")) { // URL is not absolute, but relative
+ URL url = request.url();
+ location = url.getProtocol() + "://" + url.getHost() + location;
+ } else if(!location.toLowerCase().startsWith("http://") &&
+ !location.toLowerCase().startsWith("https://")) {
+ // Reference is relative to current path
+ URL url = request.url();
+ String path = url.getPath();
+ String newPath = path.substring(0, path.lastIndexOf("/") + 1) + location;
+ location = url.getProtocol() + "://" + url.getHost() + newPath;
+ }
+ try {
+ DBWriter.updateFeedDownloadURL(request.urlString(), location).get();
+ } catch (Exception e) {
+ Log.e(TAG, Log.getStackTraceString(e));
+ }
+ }
+ return response;
+ });
+
+ // set cookie handler
+ CookieManager cm = new CookieManager();
+ cm.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
+ client.setCookieHandler(cm);
+
+ // set timeouts
+ client.setConnectTimeout(CONNECTION_TIMEOUT, TimeUnit.MILLISECONDS);
+ client.setReadTimeout(READ_TIMEOUT, TimeUnit.MILLISECONDS);
+ client.setWriteTimeout(READ_TIMEOUT, TimeUnit.MILLISECONDS);
+
+ // configure redirects
+ client.setFollowRedirects(true);
+ client.setFollowSslRedirects(true);
+
+ if(16 <= Build.VERSION.SDK_INT && Build.VERSION.SDK_INT < 21) {
+ client.setSslSocketFactory(new CustomSslSocketFactory());
+ }
+ return client;
+ }
+
+ /**
* Closes expired connections. This method should be called by the using class once has finished its work with
* the HTTP client.
*/
@@ -64,4 +121,71 @@ public class AntennapodHttpClient {
// does nothing at the moment
}
}
+
+ private static class CustomSslSocketFactory extends SSLSocketFactory {
+
+ private SSLSocketFactory factory;
+
+ public CustomSslSocketFactory() {
+ try {
+ SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
+ sslContext.init(null, null, null);
+ factory= sslContext.getSocketFactory();
+ } catch(GeneralSecurityException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public String[] getDefaultCipherSuites() {
+ return factory.getDefaultCipherSuites();
+ }
+
+ @Override
+ public String[] getSupportedCipherSuites() {
+ return factory.getSupportedCipherSuites();
+ }
+
+ public Socket createSocket() throws IOException {
+ SSLSocket result = (SSLSocket) factory.createSocket();
+ configureSocket(result);
+ return result;
+ }
+
+ public Socket createSocket(String var1, int var2) throws IOException {
+ SSLSocket result = (SSLSocket) factory.createSocket(var1, var2);
+ configureSocket(result);
+ return result;
+ }
+
+ public Socket createSocket(Socket var1, String var2, int var3, boolean var4) throws IOException {
+ SSLSocket result = (SSLSocket) factory.createSocket(var1, var2, var3, var4);
+ configureSocket(result);
+ return result;
+ }
+
+ public Socket createSocket(InetAddress var1, int var2) throws IOException {
+ SSLSocket result = (SSLSocket) factory.createSocket(var1, var2);
+ configureSocket(result);
+ return result;
+ }
+
+ public Socket createSocket(String var1, int var2, InetAddress var3, int var4) throws IOException {
+ SSLSocket result = (SSLSocket) factory.createSocket(var1, var2, var3, var4);
+ configureSocket(result);
+ return result;
+ }
+
+ public Socket createSocket(InetAddress var1, int var2, InetAddress var3, int var4) throws IOException {
+ SSLSocket result = (SSLSocket) factory.createSocket(var1, var2, var3, var4);
+ configureSocket(result);
+ return result;
+ }
+
+ private void configureSocket(SSLSocket s) {
+ s.setEnabledProtocols(new String[] { "TLSv1.2", "TLSv1.1", "TLSv1" } );
+ }
+
+ }
+
}
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 41bbd5ba6..bc3006eea 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
@@ -3,8 +3,7 @@ package de.danoeh.antennapod.core.service.download;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
-
-import org.apache.commons.lang3.Validate;
+import android.support.annotation.NonNull;
import de.danoeh.antennapod.core.feed.FeedFile;
import de.danoeh.antennapod.core.util.URLChecker;
@@ -27,11 +26,15 @@ public class DownloadRequest implements Parcelable {
protected long size;
protected int statusMsg;
- public DownloadRequest(String destination, String source, String title,
- long feedfileId, int feedfileType, String username, String password, boolean deleteOnFailure, Bundle arguments) {
- Validate.notNull(destination);
- Validate.notNull(source);
- Validate.notNull(title);
+ public DownloadRequest(@NonNull String destination,
+ @NonNull String source,
+ @NonNull String title,
+ long feedfileId,
+ int feedfileType,
+ String username,
+ String password,
+ boolean deleteOnFailure,
+ Bundle arguments) {
this.destination = destination;
this.source = source;
@@ -260,7 +263,7 @@ public class DownloadRequest implements Parcelable {
private int feedfileType;
private Bundle arguments;
- public Builder(String destination, FeedFile item) {
+ public Builder(@NonNull String destination, @NonNull FeedFile item) {
this.destination = destination;
this.source = URLChecker.prepareURL(item.getDownload_url());
this.title = item.getHumanReadableIdentifier();
@@ -285,9 +288,6 @@ public class DownloadRequest implements Parcelable {
}
public DownloadRequest build() {
- Validate.notNull(destination);
- Validate.notNull(source);
- Validate.notNull(title);
return new DownloadRequest(this);
}
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 e7b226eca..d69228ceb 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
@@ -14,20 +14,19 @@ import android.media.MediaMetadataRetriever;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
+import android.support.annotation.NonNull;
import android.support.v4.app.NotificationCompat;
import android.support.v4.util.Pair;
+import android.text.TextUtils;
import android.util.Log;
import android.webkit.URLUtil;
import org.apache.commons.io.FileUtils;
-import org.apache.commons.lang3.ArrayUtils;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.commons.lang3.Validate;
-import org.apache.http.HttpStatus;
import org.xml.sax.SAXException;
import java.io.File;
import java.io.IOException;
+import java.net.HttpURLConnection;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
@@ -54,7 +53,8 @@ import javax.xml.parsers.ParserConfigurationException;
import de.danoeh.antennapod.core.ClientConfig;
import de.danoeh.antennapod.core.R;
-import de.danoeh.antennapod.core.feed.EventDistributor;
+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;
@@ -72,9 +72,9 @@ import de.danoeh.antennapod.core.storage.DownloadRequester;
import de.danoeh.antennapod.core.syndication.handler.FeedHandler;
import de.danoeh.antennapod.core.syndication.handler.FeedHandlerResult;
import de.danoeh.antennapod.core.syndication.handler.UnsupportedFeedtypeException;
-import de.danoeh.antennapod.core.util.ChapterUtils;
import de.danoeh.antennapod.core.util.DownloadError;
import de.danoeh.antennapod.core.util.InvalidFeedException;
+import de.greenrobot.event.EventBus;
/**
* Manages the download of feedfiles in the app. Downloads can be enqueued viathe startService intent.
@@ -103,22 +103,11 @@ public class DownloadService extends Service {
public static final String EXTRA_DOWNLOAD_URL = "downloadUrl";
/**
- * Sent by the DownloadService when the content of the downloads list
- * changes.
- */
- public static final String ACTION_DOWNLOADS_CONTENT_CHANGED = "action.de.danoeh.antennapod.core.service.downloadsContentChanged";
-
- /**
* Extra for ACTION_ENQUEUE_DOWNLOAD intent.
*/
public static final String EXTRA_REQUEST = "request";
/**
- * Stores new media files that will be queued for auto-download if possible.
- */
- private List<Long> newMediaFiles;
-
- /**
* Contains all completed downloads that have not been included in the report yet.
*/
private List<DownloadStatus> reportQueue;
@@ -136,7 +125,6 @@ public class DownloadService extends Service {
private NotificationCompat.Builder notificationCompatBuilder;
- private Notification.BigTextStyle notificationBuilder;
private int NOTIFICATION_ID = 2;
private int REPORT_ID = 3;
@@ -162,6 +150,8 @@ public class DownloadService extends Service {
private static final int SCHED_EX_POOL_SIZE = 1;
private ScheduledThreadPoolExecutor schedExecutor;
+ private Handler postHandler = new Handler();
+
private final IBinder mBinder = new LocalBinder();
public class LocalBinder extends Binder {
@@ -171,7 +161,7 @@ public class DownloadService extends Service {
}
private Thread downloadCompletionThread = new Thread() {
- private static final String TAG = "downloadCompletionThread";
+ private static final String TAG = "downloadCompletionThd";
@Override
public void run() {
@@ -187,10 +177,7 @@ public class DownloadService extends Service {
final int type = status.getFeedfileType();
if (successful) {
if (type == Feed.FEEDFILETYPE_FEED) {
- handleCompletedFeedDownload(downloader
- .getDownloadRequest());
- } else if (type == FeedImage.FEEDFILETYPE_FEEDIMAGE) {
- handleCompletedImageDownload(status, downloader.getDownloadRequest());
+ handleCompletedFeedDownload(downloader.getDownloadRequest());
} else if (type == FeedMedia.FEEDFILETYPE_FEEDMEDIA) {
handleCompletedFeedMediaDownload(status, downloader.getDownloadRequest());
}
@@ -200,7 +187,7 @@ public class DownloadService extends Service {
if (status.getReason() == DownloadError.ERROR_UNAUTHORIZED) {
postAuthenticationNotification(downloader.getDownloadRequest());
} else if (status.getReason() == DownloadError.ERROR_HTTP_DATA_ERROR
- && Integer.valueOf(status.getReasonDetailed()) == HttpStatus.SC_REQUESTED_RANGE_NOT_SATISFIABLE) {
+ && Integer.valueOf(status.getReasonDetailed()) == 416) {
Log.d(TAG, "Requested invalid range, restarting download from the beginning");
FileUtils.deleteQuietly(new File(downloader.getDownloadRequest().getDestination()));
@@ -209,9 +196,32 @@ public class DownloadService extends Service {
Log.e(TAG, "Download failed");
saveDownloadStatus(status);
handleFailedDownload(status, downloader.getDownloadRequest());
+
+ if(type == FeedMedia.FEEDFILETYPE_FEEDMEDIA) {
+ long id = status.getFeedfileId();
+ FeedMedia media = DBReader.getFeedMedia(id);
+ if(media == null || media.getItem() == null) {
+ return;
+ }
+ FeedItem item = media.getItem();
+ boolean httpNotFound = status.getReason() == DownloadError.ERROR_HTTP_DATA_ERROR
+ && String.valueOf(HttpURLConnection.HTTP_NOT_FOUND).equals(status.getReasonDetailed());
+ boolean notEnoughSpace = status.getReason() == DownloadError.ERROR_NOT_ENOUGH_SPACE;
+ if (httpNotFound || notEnoughSpace) {
+ DBWriter.saveFeedItemAutoDownloadFailed(item).get();
+ }
+ // to make lists reload the failed item, we fake an item update
+ EventBus.getDefault().post(FeedItemEvent.updated(item));
+ }
+ }
+ } else {
+ // if FeedMedia download has been canceled, fake FeedItem update
+ // so that lists reload that it
+ if(status.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) {
+ FeedMedia media = DBReader.getFeedMedia(status.getFeedfileId());
+ EventBus.getDefault().post(FeedItemEvent.updated(media.getItem()));
}
}
- sendDownloadHandledIntent();
queryDownloadsAsync();
}
} catch (InterruptedException e) {
@@ -241,9 +251,8 @@ public class DownloadService extends Service {
Log.d(TAG, "Service started");
isRunning = true;
handler = new Handler();
- newMediaFiles = Collections.synchronizedList(new ArrayList<Long>());
reportQueue = Collections.synchronizedList(new ArrayList<DownloadStatus>());
- downloads = new ArrayList<Downloader>();
+ downloads = Collections.synchronizedList(new ArrayList<Downloader>());
numberOfDownloads = new AtomicInteger(0);
IntentFilter cancelDownloadReceiverFilter = new IntentFilter();
@@ -309,10 +318,14 @@ public class DownloadService extends Service {
Log.d(TAG, "Service shutting down");
isRunning = false;
- if (ClientConfig.downloadServiceCallbacks.shouldCreateReport()) {
+ if (ClientConfig.downloadServiceCallbacks.shouldCreateReport() &&
+ UserPreferences.showDownloadReport()) {
updateReport();
}
+ postHandler.removeCallbacks(postDownloaderTask);
+ EventBus.getDefault().postSticky(DownloadEvent.refresh(Collections.emptyList()));
+
stopForeground(true);
NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
nm.cancel(NOTIFICATION_ID);
@@ -324,29 +337,20 @@ public class DownloadService extends Service {
cancelNotificationUpdater();
unregisterReceiver(cancelDownloadReceiver);
- if (!newMediaFiles.isEmpty()) {
- DBTasks.autodownloadUndownloadedItems(getApplicationContext(),
- ArrayUtils.toPrimitive(newMediaFiles.toArray(new Long[newMediaFiles.size()])));
- }
+ // start auto download in case anything new has shown up
+ DBTasks.autodownloadUndownloadedItems(getApplicationContext());
}
- @SuppressLint("NewApi")
private void setupNotificationBuilders() {
Bitmap icon = BitmapFactory.decodeResource(getResources(),
R.drawable.stat_notify_sync);
- if (android.os.Build.VERSION.SDK_INT >= 16) {
- notificationBuilder = new Notification.BigTextStyle(
- new Notification.Builder(this).setOngoing(true)
- .setContentIntent(ClientConfig.downloadServiceCallbacks.getNotificationContentIntent(this)).setLargeIcon(icon)
- .setSmallIcon(R.drawable.stat_notify_sync)
- );
- } else {
notificationCompatBuilder = new NotificationCompat.Builder(this)
- .setOngoing(true).setContentIntent(ClientConfig.downloadServiceCallbacks.getNotificationContentIntent(this))
+ .setOngoing(true)
+ .setContentIntent(ClientConfig.downloadServiceCallbacks.getNotificationContentIntent(this))
.setLargeIcon(icon)
.setSmallIcon(R.drawable.stat_notify_sync);
- }
+
Log.d(TAG, "Notification set up");
}
@@ -354,58 +358,48 @@ public class DownloadService extends Service {
* Updates the contents of the service's notifications. Should be called
* before setupNotificationBuilders.
*/
- @SuppressLint("NewApi")
private Notification updateNotifications() {
String contentTitle = getString(R.string.download_notification_title);
int numDownloads = requester.getNumberOfDownloads();
String downloadsLeft;
if (numDownloads > 0) {
- downloadsLeft = requester.getNumberOfDownloads()
- + getString(R.string.downloads_left);
+ downloadsLeft = getResources()
+ .getQuantityString(R.plurals.downloads_left, numDownloads, numDownloads);
} else {
downloadsLeft = getString(R.string.downloads_processing);
}
- if (android.os.Build.VERSION.SDK_INT >= 16) {
-
- if (notificationBuilder != null) {
-
- StringBuilder bigText = new StringBuilder("");
- for (int i = 0; i < downloads.size(); i++) {
- Downloader downloader = downloads.get(i);
- final DownloadRequest request = downloader
- .getDownloadRequest();
- if (request.getFeedfileType() == Feed.FEEDFILETYPE_FEED) {
- if (request.getTitle() != null) {
- if (i > 0) {
- bigText.append("\n");
- }
- bigText.append("\u2022 " + request.getTitle());
+ if (notificationCompatBuilder != null) {
+
+ StringBuilder bigText = new StringBuilder("");
+ for (int i = 0; i < downloads.size(); i++) {
+ Downloader downloader = downloads.get(i);
+ final DownloadRequest request = downloader
+ .getDownloadRequest();
+ if (request.getFeedfileType() == Feed.FEEDFILETYPE_FEED) {
+ if (request.getTitle() != null) {
+ if (i > 0) {
+ bigText.append("\n");
}
- } else if (request.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) {
- if (request.getTitle() != null) {
- if (i > 0) {
- bigText.append("\n");
- }
- bigText.append("\u2022 " + request.getTitle()
- + " (" + request.getProgressPercent()
- + "%)");
+ bigText.append("\u2022 " + request.getTitle());
+ }
+ } else if (request.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) {
+ if (request.getTitle() != null) {
+ if (i > 0) {
+ bigText.append("\n");
}
+ bigText.append("\u2022 " + request.getTitle()
+ + " (" + request.getProgressPercent()
+ + "%)");
}
-
- }
- notificationBuilder.setSummaryText(downloadsLeft);
- notificationBuilder.setBigContentTitle(contentTitle);
- if (bigText != null) {
- notificationBuilder.bigText(bigText.toString());
}
- return notificationBuilder.build();
+
}
- } else {
- if (notificationCompatBuilder != null) {
- notificationCompatBuilder.setContentTitle(contentTitle);
- notificationCompatBuilder.setContentText(downloadsLeft);
- return notificationCompatBuilder.build();
+ notificationCompatBuilder.setContentTitle(contentTitle);
+ notificationCompatBuilder.setContentText(downloadsLeft);
+ if (bigText != null) {
+ notificationCompatBuilder.setStyle(new NotificationCompat.BigTextStyle().bigText(bigText.toString()));
}
+ return notificationCompatBuilder.build();
}
return null;
}
@@ -423,9 +417,11 @@ public class DownloadService extends Service {
@Override
public void onReceive(Context context, Intent intent) {
- if (StringUtils.equals(intent.getAction(), ACTION_CANCEL_DOWNLOAD)) {
+ if (TextUtils.equals(intent.getAction(), ACTION_CANCEL_DOWNLOAD)) {
String url = intent.getStringExtra(EXTRA_DOWNLOAD_URL);
- Validate.notNull(url, "ACTION_CANCEL_DOWNLOAD intent needs download url extra");
+ if(url == null) {
+ throw new IllegalArgumentException("ACTION_CANCEL_DOWNLOAD intent needs download url extra");
+ }
Log.d(TAG, "Cancelling download with url " + url);
Downloader d = getDownloader(url);
@@ -434,14 +430,14 @@ public class DownloadService extends Service {
} else {
Log.e(TAG, "Could not cancel download with url " + url);
}
+ postDownloaders();
- } else if (StringUtils.equals(intent.getAction(), ACTION_CANCEL_ALL_DOWNLOADS)) {
+ } else if (TextUtils.equals(intent.getAction(), ACTION_CANCEL_ALL_DOWNLOADS)) {
for (Downloader d : downloads) {
d.cancel();
Log.d(TAG, "Cancelled all downloads");
}
- sendBroadcast(new Intent(ACTION_DOWNLOADS_CONTENT_CHANGED));
-
+ postDownloaders();
}
queryDownloads();
}
@@ -460,13 +456,14 @@ public class DownloadService extends Service {
if (downloader != null) {
numberOfDownloads.incrementAndGet();
// smaller rss feeds before bigger media files
- if(request.getFeedfileId() == Feed.FEEDFILETYPE_FEED) {
+ if(request.getFeedfileType() == Feed.FEEDFILETYPE_FEED) {
downloads.add(0, downloader);
} else {
downloads.add(downloader);
}
downloadExecutor.submit(downloader);
- sendBroadcast(new Intent(ACTION_DOWNLOADS_CONTENT_CHANGED));
+
+ postDownloaders();
}
queryDownloads();
@@ -497,7 +494,7 @@ public class DownloadService extends Service {
boolean rc = downloads.remove(d);
Log.d(TAG, "Result of downloads.remove: " + rc);
DownloadRequester.getInstance().removeDownload(d.getDownloadRequest());
- sendBroadcast(new Intent(ACTION_DOWNLOADS_CONTENT_CHANGED));
+ postDownloaders();
}
});
}
@@ -510,11 +507,7 @@ public class DownloadService extends Service {
*/
private void saveDownloadStatus(DownloadStatus status) {
reportQueue.add(status);
- DBWriter.addDownloadStatus(this, status);
- }
-
- private void sendDownloadHandledIntent() {
- EventDistributor.getInstance().sendDownloadHandledBroadcast();
+ DBWriter.addDownloadStatus(status);
}
/**
@@ -633,14 +626,6 @@ public class DownloadService extends Service {
}
/**
- * Is called whenever a Feed-Image is downloaded
- */
- private void handleCompletedImageDownload(DownloadStatus status, DownloadRequest request) {
- Log.d(TAG, "Handling completed Image Download");
- syncExecutor.execute(new ImageHandlerThread(status, request));
- }
-
- /**
* Is called whenever a FeedMedia is downloaded.
*/
private void handleCompletedFeedMediaDownload(DownloadStatus status, DownloadRequest request) {
@@ -774,52 +759,8 @@ public class DownloadService extends Service {
for (int i = 0; i < savedFeeds.length; i++) {
Feed savedFeed = savedFeeds[i];
- // Download Feed Image if provided and not downloaded
- if (savedFeed.getImage() != null
- && savedFeed.getImage().isDownloaded() == false) {
- Log.d(TAG, "Feed has image; Downloading....");
- savedFeed.getImage().setOwner(savedFeed);
- final Feed savedFeedRef = savedFeed;
- try {
- requester.downloadImage(DownloadService.this,
- savedFeedRef.getImage());
- } catch (DownloadRequestException e) {
- e.printStackTrace();
- DBWriter.addDownloadStatus(
- DownloadService.this,
- new DownloadStatus(
- savedFeedRef.getImage(),
- savedFeedRef
- .getImage()
- .getHumanReadableIdentifier(),
- DownloadError.ERROR_REQUEST_ERROR,
- false, e.getMessage()
- )
- );
- }
- }
-
- // queue new media files for automatic download
- for (FeedItem item : savedFeed.getItems()) {
- if(item.getPubDate() == null) {
- Log.d(TAG, item.toString());
- }
- if(item.getImage() != null && item.getImage().isDownloaded() == false) {
- item.getImage().setOwner(item);
- try {
- requester.downloadImage(DownloadService.this,
- item.getImage());
- } catch (DownloadRequestException e) {
- e.printStackTrace();
- }
- }
- if (!item.isRead() && item.hasMedia() && !item.getMedia().isDownloaded()) {
- newMediaFiles.add(item.getMedia().getId());
- }
- }
// If loadAllPages=true, check if another page is available and queue it for download
-
final boolean loadAllPages = results.get(i).first.getArguments().getBoolean(DownloadRequester.REQUEST_ARG_LOAD_ALL_PAGES);
final Feed feed = results.get(i).second.feed;
if (loadAllPages && feed.getNextPageLink() != null) {
@@ -837,8 +778,6 @@ public class DownloadService extends Service {
numberOfDownloads.decrementAndGet();
}
- sendDownloadHandledIntent();
-
queryDownloadsAsync();
}
});
@@ -889,7 +828,7 @@ public class DownloadService extends Service {
feed.setFile_url(request.getDestination());
feed.setId(request.getFeedfileId());
feed.setDownloaded(true);
- feed.setPreferences(new FeedPreferences(0, true,
+ feed.setPreferences(new FeedPreferences(0, true, FeedPreferences.AutoDeleteAction.GLOBAL,
request.getUsername(), request.getPassword()));
feed.setPageNr(request.getArguments().getInt(DownloadRequester.REQUEST_ARG_PAGE_NR, 0));
@@ -938,7 +877,7 @@ public class DownloadService extends Service {
if (successful) {
// we create a 'successful' download log if the feed's last refresh failed
- List<DownloadStatus> log = DBReader.getFeedDownloadLog(DownloadService.this, feed);
+ List<DownloadStatus> log = DBReader.getFeedDownloadLog(feed);
if(log.size() > 0 && log.get(0).isSuccessful() == false) {
saveDownloadStatus(new DownloadStatus(feed,
feed.getHumanReadableIdentifier(), DownloadError.SUCCESS, successful,
@@ -982,7 +921,7 @@ public class DownloadService extends Service {
FeedItem item1 = feed.getItems().get(x);
FeedItem item2 = feed.getItems().get(y);
if (item1.hasItemImage() && item2.hasItemImage()) {
- if (StringUtils.equals(item1.getImage().getDownload_url(), item2.getImage().getDownload_url())) {
+ if (TextUtils.equals(item1.getImage().getDownload_url(), item2.getImage().getDownload_url())) {
item2.setImage(null);
}
}
@@ -1061,17 +1000,17 @@ public class DownloadService extends Service {
@Override
public void run() {
if(request.getFeedfileType() == Feed.FEEDFILETYPE_FEED) {
- DBWriter.setFeedLastUpdateFailed(DownloadService.this, request.getFeedfileId(), true);
+ 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(DownloadService.this, request.getFeedfileId());
+ FeedMedia media = DBReader.getFeedMedia(request.getFeedfileId());
media.setFile_url(request.getDestination());
try {
- DBWriter.setFeedMedia(DownloadService.this, media).get();
+ DBWriter.setFeedMedia(media).get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
@@ -1083,40 +1022,6 @@ public class DownloadService extends Service {
}
/**
- * Handles a completed image download.
- */
- class ImageHandlerThread implements Runnable {
-
- private DownloadRequest request;
- private DownloadStatus status;
-
- public ImageHandlerThread(DownloadStatus status, DownloadRequest request) {
- Validate.notNull(status);
- Validate.notNull(request);
-
- this.status = status;
- this.request = request;
- }
-
- @Override
- public void run() {
- FeedImage image = DBReader.getFeedImage(DownloadService.this, request.getFeedfileId());
- if (image == null) {
- throw new IllegalStateException("Could not find downloaded image in database");
- }
-
- image.setFile_url(request.getDestination());
- image.setDownloaded(true);
-
- saveDownloadStatus(status);
- sendDownloadHandledIntent();
- DBWriter.setFeedImage(DownloadService.this, image);
- numberOfDownloads.decrementAndGet();
- queryDownloadsAsync();
- }
- }
-
- /**
* Handles a completed media download.
*/
class MediaHandlerThread implements Runnable {
@@ -1124,25 +1029,22 @@ public class DownloadService extends Service {
private DownloadRequest request;
private DownloadStatus status;
- public MediaHandlerThread(DownloadStatus status, DownloadRequest request) {
- Validate.notNull(status);
- Validate.notNull(request);
-
+ public MediaHandlerThread(@NonNull DownloadStatus status,
+ @NonNull DownloadRequest request) {
this.status = status;
this.request = request;
}
@Override
public void run() {
- FeedMedia media = DBReader.getFeedMedia(DownloadService.this,
- request.getFeedfileId());
+ FeedMedia media = DBReader.getFeedMedia(request.getFeedfileId());
if (media == null) {
throw new IllegalStateException(
"Could not find downloaded media object in database");
}
- boolean chaptersRead = false;
media.setDownloaded(true);
media.setFile_url(request.getDestination());
+ media.setHasEmbeddedPicture(null);
// Get duration
MediaMetadataRetriever mmr = null;
@@ -1162,20 +1064,19 @@ public class DownloadService extends Service {
}
}
- if (media.getItem().getChapters() == null) {
- ChapterUtils.loadChaptersFromFileUrl(media);
- if (media.getItem().getChapters() != null) {
- chaptersRead = true;
- }
- }
+ final FeedItem item = media.getItem();
try {
- if (chaptersRead) {
- DBWriter.setFeedItem(DownloadService.this, media.getItem()).get();
+ // we've received the media, we don't want to autodownload it again
+ if(item != null) {
+ item.setAutoDownload(false);
+ DBWriter.setFeedItem(item).get();
}
- DBWriter.setFeedMedia(DownloadService.this, media).get();
- if (!DBTasks.isInQueue(DownloadService.this, media.getItem().getId())) {
- DBWriter.addQueueItem(DownloadService.this, media.getItem().getId()).get();
+
+ DBWriter.setFeedMedia(media).get();
+
+ if (item != null && !DBTasks.isInQueue(DownloadService.this, item.getId())) {
+ DBWriter.addQueueItem(DownloadService.this, item).get();
}
} catch (ExecutionException e) {
e.printStackTrace();
@@ -1186,15 +1087,13 @@ public class DownloadService extends Service {
}
saveDownloadStatus(status);
- sendDownloadHandledIntent();
-
- if(GpodnetPreferences.loggedIn()) {
- FeedItem item = media.getItem();
- GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, Action.DOWNLOAD)
- .currentDeviceId()
- .currentTimestamp()
- .build();
- GpodnetPreferences.enqueueEpisodeAction(action);
+
+ if(GpodnetPreferences.loggedIn() && item != null) {
+ GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, Action.DOWNLOAD)
+ .currentDeviceId()
+ .currentTimestamp()
+ .build();
+ GpodnetPreferences.enqueueEpisodeAction(action);
}
numberOfDownloads.decrementAndGet();
@@ -1239,8 +1138,25 @@ public class DownloadService extends Service {
}
}
- public List<Downloader> getDownloads() {
- return downloads;
+
+ private long lastPost = 0;
+
+ final Runnable postDownloaderTask = new Runnable() {
+ @Override
+ public void run() {
+ List<Downloader> list = Collections.unmodifiableList(downloads);
+ EventBus.getDefault().postSticky(DownloadEvent.refresh(list));
+ postHandler.postDelayed(postDownloaderTask, 1500);
+ }
+ };
+
+ private void postDownloaders() {
+ long now = System.currentTimeMillis();
+ if(now - lastPost >= 250) {
+ postHandler.removeCallbacks(postDownloaderTask);
+ postDownloaderTask.run();
+ lastPost = now;
+ }
}
}
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 d05650d10..ed2b00dfe 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
@@ -1,12 +1,14 @@
package de.danoeh.antennapod.core.service.download;
-import org.apache.commons.lang3.Validate;
+import android.database.Cursor;
+import android.support.annotation.NonNull;
+
+import java.util.Date;
import de.danoeh.antennapod.core.feed.FeedFile;
+import de.danoeh.antennapod.core.storage.PodDBAdapter;
import de.danoeh.antennapod.core.util.DownloadError;
-import java.util.Date;
-
/** Contains status attributes for one download */
public class DownloadStatus {
/**
@@ -59,10 +61,8 @@ public class DownloadStatus {
this.feedfileType = feedfileType;
}
- public DownloadStatus(DownloadRequest request, DownloadError reason,
+ public DownloadStatus(@NonNull DownloadRequest request, DownloadError reason,
boolean successful, boolean cancelled, String reasonDetailed) {
- Validate.notNull(request);
-
this.title = request.getTitle();
this.feedfileId = request.getFeedfileId();
this.feedfileType = request.getFeedfileType();
@@ -74,10 +74,8 @@ public class DownloadStatus {
}
/** Constructor for creating new completed downloads. */
- public DownloadStatus(FeedFile feedfile, String title, DownloadError reason,
- boolean successful, String reasonDetailed) {
- Validate.notNull(feedfile);
-
+ public DownloadStatus(@NonNull FeedFile feedfile, String title, DownloadError reason,
+ boolean successful, String reasonDetailed) {
this.title = title;
this.done = true;
this.feedfileId = feedfile.getId();
@@ -101,6 +99,30 @@ public class DownloadStatus {
this.reasonDetailed = reasonDetailed;
}
+ public static DownloadStatus fromCursor(Cursor cursor) {
+ int indexId = cursor.getColumnIndex(PodDBAdapter.KEY_ID);
+ int indexTitle = cursor.getColumnIndex(PodDBAdapter.KEY_DOWNLOADSTATUS_TITLE);
+ int indexFeedFile = cursor.getColumnIndex(PodDBAdapter.KEY_FEEDFILE);
+ int indexFileFileType = cursor.getColumnIndex(PodDBAdapter.KEY_FEEDFILETYPE);
+ int indexSuccessful = cursor.getColumnIndex(PodDBAdapter.KEY_SUCCESSFUL);
+ int indexReason = cursor.getColumnIndex(PodDBAdapter.KEY_REASON);
+ int indexCompletionDate = cursor.getColumnIndex(PodDBAdapter.KEY_COMPLETION_DATE);
+ int indexReasonDetailed = cursor.getColumnIndex(PodDBAdapter.KEY_REASON_DETAILED);
+
+ long id = cursor.getLong(indexId);
+ String title = cursor.getString(indexTitle);
+ long feedfileId = cursor.getLong(indexFeedFile);
+ int feedfileType = cursor.getInt(indexFileFileType);
+ boolean successful = cursor.getInt(indexSuccessful) > 0;
+ int reason = cursor.getInt(indexReason);
+ Date completionDate = new Date(cursor.getLong(indexCompletionDate));
+ String reasonDetailed = cursor.getString(indexReasonDetailed);
+
+ return new DownloadStatus(id, title, feedfileId,
+ feedfileType, successful, DownloadError.fromCode(reason), completionDate,
+ reasonDetailed);
+ }
+
@Override
public String toString() {
return "DownloadStatus [id=" + id + ", title=" + title + ", reason="
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 ac0fe8036..0b9fba6f7 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
@@ -1,16 +1,16 @@
package de.danoeh.antennapod.core.service.download;
+import android.text.TextUtils;
import android.util.Log;
import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Protocol;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;
import com.squareup.okhttp.ResponseBody;
import com.squareup.okhttp.internal.http.HttpDate;
import org.apache.commons.io.IOUtils;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.http.HttpStatus;
import java.io.BufferedInputStream;
import java.io.File;
@@ -22,6 +22,7 @@ import java.net.HttpURLConnection;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.net.UnknownHostException;
+import java.util.Arrays;
import java.util.Date;
import de.danoeh.antennapod.core.ClientConfig;
@@ -84,7 +85,7 @@ public class HttpDownloader extends Downloader {
String credentials = encodeCredentials(parts[0], parts[1], "ISO-8859-1");
httpReq.header("Authorization", credentials);
}
- } else if (!StringUtils.isEmpty(request.getUsername()) && request.getPassword() != null) {
+ } else if (!TextUtils.isEmpty(request.getUsername()) && request.getPassword() != null) {
String credentials = encodeCredentials(request.getUsername(), request.getPassword(),
"ISO-8859-1");
httpReq.header("Authorization", credentials);
@@ -93,15 +94,29 @@ public class HttpDownloader extends Downloader {
// add range header if necessary
if (fileExists) {
request.setSoFar(destination.length());
- httpReq.addHeader("Range",
- "bytes=" + request.getSoFar() + "-");
+ httpReq.addHeader("Range", "bytes=" + request.getSoFar() + "-");
Log.d(TAG, "Adding range header: " + request.getSoFar());
}
- Response response = httpClient.newCall(httpReq.build()).execute();
+ Response response = null;
+ try {
+ response = httpClient.newCall(httpReq.build()).execute();
+ } catch(IOException e) {
+ Log.e(TAG, e.toString());
+ if(e.getMessage().contains("PROTOCOL_ERROR")) {
+ httpClient.setProtocols(Arrays.asList(Protocol.HTTP_1_1));
+ response = httpClient.newCall(httpReq.build()).execute();
+ }
+ else {
+ throw e;
+ }
+ }
responseBody = response.body();
String contentEncodingHeader = response.header("Content-Encoding");
- boolean isGzip = StringUtils.equalsIgnoreCase(contentEncodingHeader, "gzip");
+ boolean isGzip = false;
+ if(!TextUtils.isEmpty(contentEncodingHeader)) {
+ isGzip = TextUtils.equals(contentEncodingHeader.toLowerCase(), "gzip");
+ }
Log.d(TAG, "Response code is " + response.code());
@@ -113,7 +128,7 @@ public class HttpDownloader extends Downloader {
String credentials = encodeCredentials(parts[0], parts[1], "UTF-8");
httpReq.header("Authorization", credentials);
}
- } else if (!StringUtils.isEmpty(request.getUsername()) && request.getPassword() != null) {
+ } else if (!TextUtils.isEmpty(request.getUsername()) && request.getPassword() != null) {
String credentials = encodeCredentials(request.getUsername(), request.getPassword(),
"UTF-8");
httpReq.header("Authorization", credentials);
@@ -121,7 +136,9 @@ public class HttpDownloader extends Downloader {
response = httpClient.newCall(httpReq.build()).execute();
responseBody = response.body();
contentEncodingHeader = response.header("Content-Encoding");
- isGzip = StringUtils.equalsIgnoreCase(contentEncodingHeader, "gzip");
+ if(!TextUtils.isEmpty(contentEncodingHeader)) {
+ isGzip = TextUtils.equals(contentEncodingHeader.toLowerCase(), "gzip");
+ }
}
if(!response.isSuccessful() && response.code() == HttpURLConnection.HTTP_NOT_MODIFIED) {
@@ -144,7 +161,7 @@ public class HttpDownloader extends Downloader {
return;
}
- if (!StorageUtils.storageAvailable(ClientConfig.applicationCallbacks.getApplicationInstance())) {
+ if (!StorageUtils.storageAvailable()) {
onFail(DownloadError.ERROR_DEVICE_NOT_FOUND, null);
return;
}
@@ -153,8 +170,8 @@ public class HttpDownloader extends Downloader {
String contentRangeHeader = (fileExists) ? response.header("Content-Range") : null;
- if (fileExists && response.code() == HttpStatus.SC_PARTIAL_CONTENT
- && !StringUtils.isEmpty(contentRangeHeader)) {
+ if (fileExists && response.code() == HttpURLConnection.HTTP_PARTIAL
+ && !TextUtils.isEmpty(contentRangeHeader)) {
String start = contentRangeHeader.substring("bytes ".length(),
contentRangeHeader.indexOf("-"));
request.setSoFar(Long.valueOf(start));
@@ -188,13 +205,17 @@ public class HttpDownloader extends Downloader {
}
Log.d(TAG, "Starting download");
- while (!cancelled
- && (count = connection.read(buffer)) != -1) {
- out.write(buffer, 0, count);
- request.setSoFar(request.getSoFar() + count);
- request.setProgressPercent((int) (((double) request
- .getSoFar() / (double) request
- .getSize()) * 100));
+ try {
+ while (!cancelled
+ && (count = connection.read(buffer)) != -1) {
+ out.write(buffer, 0, count);
+ request.setSoFar(request.getSoFar() + count);
+ request.setProgressPercent((int) (((double) request
+ .getSoFar() / (double) request
+ .getSize()) * 100));
+ }
+ } catch(IOException e) {
+ Log.e(TAG, Log.getStackTraceString(e));
}
if (cancelled) {
onCancelled();
@@ -210,6 +231,9 @@ public class HttpDownloader extends Downloader {
request.getSize()
);
return;
+ } else if(request.getSize() > 0 && request.getSoFar() == 0){
+ onFail(DownloadError.ERROR_IO_ERROR, "Download completed, but nothing was read");
+ return;
}
onSuccess();
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/MediaButtonIntentReceiver.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/MediaButtonIntentReceiver.java
new file mode 100644
index 000000000..7d06390f2
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/MediaButtonIntentReceiver.java
@@ -0,0 +1,26 @@
+package de.danoeh.antennapod.core.service.playback;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+public class MediaButtonIntentReceiver extends BroadcastReceiver {
+
+ private static final String TAG = "MediaButtonIntentRcver";
+
+ private static PlaybackServiceMediaPlayer mMediaPlayer;
+
+ public static void setMediaPlayer(PlaybackServiceMediaPlayer mediaPlayer) {
+ mMediaPlayer = mediaPlayer;
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.d(TAG, "onReceive(Context, " + intent.toString() +")");
+ if (mMediaPlayer != null && Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())) {
+ mMediaPlayer.handleMediaKey(intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT));
+ }
+ }
+
+} \ No newline at end of file
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 3f6769ee4..2be075a92 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
@@ -1,11 +1,11 @@
package de.danoeh.antennapod.core.service.playback;
-import android.annotation.SuppressLint;
import android.app.Notification;
+import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
+import android.bluetooth.BluetoothA2dp;
import android.content.BroadcastReceiver;
-import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
@@ -13,27 +13,22 @@ import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.AudioManager;
-import android.media.MediaMetadataRetriever;
import android.media.MediaPlayer;
-import android.media.RemoteControlClient;
-import android.media.RemoteControlClient.MetadataEditor;
-import android.os.AsyncTask;
import android.os.Binder;
import android.os.Build;
import android.os.IBinder;
+import android.os.Vibrator;
import android.preference.PreferenceManager;
-import android.support.v4.app.NotificationCompat;
+import android.support.v7.app.NotificationCompat;
+import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
import android.view.KeyEvent;
import android.view.SurfaceHolder;
import android.widget.Toast;
-import com.squareup.picasso.Picasso;
+import com.bumptech.glide.Glide;
-import org.apache.commons.lang3.StringUtils;
-
-import java.io.IOException;
import java.util.List;
import de.danoeh.antennapod.core.ClientConfig;
@@ -42,6 +37,7 @@ import de.danoeh.antennapod.core.feed.Chapter;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.feed.MediaType;
+import de.danoeh.antennapod.core.glide.ApGlideSettings;
import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction;
import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction.Action;
import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
@@ -50,6 +46,7 @@ import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.receiver.MediaButtonReceiver;
import de.danoeh.antennapod.core.storage.DBTasks;
import de.danoeh.antennapod.core.storage.DBWriter;
+import de.danoeh.antennapod.core.util.IntList;
import de.danoeh.antennapod.core.util.QueueAccess;
import de.danoeh.antennapod.core.util.flattr.FlattrUtils;
import de.danoeh.antennapod.core.util.playback.Playable;
@@ -165,7 +162,6 @@ public class PlaybackService extends Service {
private static final int NOTIFICATION_ID = 1;
- private RemoteControlClient remoteControlClient;
private PlaybackServiceMediaPlayer mediaPlayer;
private PlaybackServiceTaskManager taskManager;
@@ -213,7 +209,6 @@ public class PlaybackService extends Service {
return ClientConfig.playbackServiceCallbacks.getPlayerActivityIntent(context, mt);
}
- @SuppressLint("NewApi")
@Override
public void onCreate() {
super.onCreate();
@@ -224,8 +219,10 @@ public class PlaybackService extends Service {
Intent.ACTION_HEADSET_PLUG));
registerReceiver(shutdownReceiver, new IntentFilter(
ACTION_SHUTDOWN_PLAYBACK_SERVICE));
- registerReceiver(bluetoothStateUpdated, new IntentFilter(
- AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED));
+ 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(
@@ -234,13 +231,11 @@ public class PlaybackService extends Service {
ACTION_PAUSE_PLAY_CURRENT_EPISODE));
registerReceiver(pauseResumeCurrentEpisodeReceiver, new IntentFilter(
ACTION_RESUME_PLAY_CURRENT_EPISODE));
- remoteControlClient = setupRemoteControlClient();
taskManager = new PlaybackServiceTaskManager(this, taskManagerCallback);
mediaPlayer = new PlaybackServiceMediaPlayer(this, mediaPlayerCallback);
}
- @SuppressLint("NewApi")
@Override
public void onDestroy() {
super.onDestroy();
@@ -251,7 +246,9 @@ public class PlaybackService extends Service {
unregisterReceiver(headsetDisconnected);
unregisterReceiver(shutdownReceiver);
- unregisterReceiver(bluetoothStateUpdated);
+ if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ unregisterReceiver(bluetoothStateUpdated);
+ }
unregisterReceiver(audioBecomingNoisy);
unregisterReceiver(skipCurrentEpisodeReceiver);
unregisterReceiver(pausePlayCurrentEpisodeReceiver);
@@ -345,6 +342,8 @@ public class PlaybackService extends Service {
break;
case KeyEvent.KEYCODE_MEDIA_NEXT:
+ mediaPlayer.endPlayback(true);
+ break;
case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
mediaPlayer.seekDelta(UserPreferences.getFastFowardSecs() * 1000);
break;
@@ -398,11 +397,27 @@ public class PlaybackService extends Service {
}
@Override
+ public void onSleepTimerAlmostExpired() {
+ float leftVolume = 0.1f * UserPreferences.getLeftVolume();
+ float rightVolume = 0.1f * UserPreferences.getRightVolume();
+ mediaPlayer.setVolume(leftVolume, rightVolume);
+ }
+
+ @Override
public void onSleepTimerExpired() {
mediaPlayer.pause(true, true);
+ float leftVolume = UserPreferences.getLeftVolume();
+ float rightVolume = UserPreferences.getRightVolume();
+ mediaPlayer.setVolume(leftVolume, rightVolume);
sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0);
}
+ @Override
+ public void onSleepTimerReset() {
+ float leftVolume = UserPreferences.getLeftVolume();
+ float rightVolume = UserPreferences.getRightVolume();
+ mediaPlayer.setVolume(leftVolume, rightVolume);
+ }
@Override
public void onWidgetUpdaterTick() {
@@ -442,7 +457,7 @@ public class PlaybackService extends Service {
}
writePlayerStatusPlaybackPreferences();
- final Playable playable = mediaPlayer.getPSMPInfo().playable;
+ final Playable playable = newInfo.playable;
// Gpodder: send play action
if(GpodnetPreferences.loggedIn() && playable instanceof FeedMedia) {
@@ -486,7 +501,6 @@ public class PlaybackService extends Service {
// statusUpdate.putExtra(EXTRA_NEW_PLAYER_STATUS, newInfo.playerStatus.ordinal());
sendBroadcast(statusUpdate);
updateWidget();
- refreshRemoteControlClientState(newInfo);
bluetoothNotifyChange(newInfo, AVRCP_ACTION_PLAYER_STATUS_CHANGED);
bluetoothNotifyChange(newInfo, AVRCP_ACTION_META_CHANGED);
}
@@ -523,9 +537,9 @@ public class PlaybackService extends Service {
@Override
public boolean onMediaPlayerError(Object inObj, int what, int extra) {
- final String TAG = "PlaybackService.onErrorListener";
+ final String TAG = "PlaybackSvc.onErrorLtsn";
Log.w(TAG, "An error has occured: " + what + " " + extra);
- if (mediaPlayer.getPSMPInfo().playerStatus == PlayerStatus.PLAYING) {
+ if (mediaPlayer.getPlayerStatus() == PlayerStatus.PLAYING) {
mediaPlayer.pause(true, false);
}
sendNotificationBroadcast(NOTIFICATION_TYPE_ERROR, what);
@@ -535,21 +549,16 @@ public class PlaybackService extends Service {
}
@Override
- public boolean endPlayback(boolean playNextEpisode) {
- PlaybackService.this.endPlayback(true);
+ public boolean endPlayback(boolean playNextEpisode, boolean wasSkipped) {
+ PlaybackService.this.endPlayback(playNextEpisode, wasSkipped);
return true;
}
-
- @Override
- public RemoteControlClient getRemoteControlClient() {
- return remoteControlClient;
- }
};
- private void endPlayback(boolean playNextEpisode) {
+ private void endPlayback(boolean playNextEpisode, boolean wasSkipped) {
Log.d(TAG, "Playback ended");
- final Playable playable = mediaPlayer.getPSMPInfo().playable;
+ final Playable playable = mediaPlayer.getPlayable();
if (playable == null) {
Log.e(TAG, "Cannot end playback: media was null");
return;
@@ -563,20 +572,28 @@ public class PlaybackService extends Service {
if (playable instanceof FeedMedia) {
FeedMedia media = (FeedMedia) playable;
FeedItem item = media.getItem();
- DBWriter.markItemRead(PlaybackService.this, item, true, true);
try {
final List<FeedItem> queue = taskManager.getQueue();
isInQueue = QueueAccess.ItemListAccess(queue).contains(item.getId());
- nextItem = DBTasks.getQueueSuccessorOfItem(this, item.getId(), queue);
+ nextItem = DBTasks.getQueueSuccessorOfItem(item.getId(), queue);
} catch (InterruptedException e) {
e.printStackTrace();
// isInQueue remains false
}
- if (isInQueue) {
- DBWriter.removeQueueItem(PlaybackService.this, item, true);
+
+ boolean shouldKeep = wasSkipped && UserPreferences.shouldSkipKeepEpisode();
+
+ if (!shouldKeep) {
+ // only mark the item as played if we're not keeping it anyways
+ DBWriter.markItemPlayed(item, FeedItem.PLAYED, true);
+
+ if (isInQueue) {
+ DBWriter.removeQueueItem(PlaybackService.this, item, true);
+ }
}
- DBWriter.addItemToPlaybackHistory(PlaybackService.this, media);
+
+ DBWriter.addItemToPlaybackHistory(media);
// auto-flattr if enabled
if (isAutoFlattrable(media) && UserPreferences.getAutoFlattrPlayedDurationThreshold() == 1.0f) {
@@ -584,7 +601,7 @@ public class PlaybackService extends Service {
}
// Delete episode if enabled
- if(UserPreferences.isAutoDelete()) {
+ if(item.getFeed().getPreferences().getCurrentAutoDelete() && !shouldKeep ) {
DBWriter.deleteFeedMediaOfItem(PlaybackService.this, media.getId());
Log.d(TAG, "Episode Deleted");
}
@@ -634,7 +651,7 @@ public class PlaybackService extends Service {
writePlaybackPreferencesNoMediaPlaying();
if (nextMedia != null) {
- stream = !playable.localFileAvailable();
+ stream = !nextMedia.localFileAvailable();
mediaPlayer.playMediaObject(nextMedia, stream, startWhenPrepared, prepareImmediately);
sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD,
(nextMedia.getMediaType() == MediaType.VIDEO) ? EXTRA_CODE_VIDEO : EXTRA_CODE_AUDIO);
@@ -645,10 +662,9 @@ public class PlaybackService extends Service {
}
}
- public void setSleepTimer(long waitingTime) {
- Log.d(TAG, "Setting sleep timer to " + Long.toString(waitingTime)
- + " milliseconds");
- taskManager.setSleepTimer(waitingTime);
+ public void setSleepTimer(long waitingTime, boolean shakeToReset, boolean vibrate) {
+ Log.d(TAG, "Setting sleep timer to " + Long.toString(waitingTime) + " milliseconds");
+ taskManager.setSleepTimer(waitingTime, shakeToReset, vibrate);
sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0);
}
@@ -744,8 +760,7 @@ public class PlaybackService extends Service {
SharedPreferences.Editor editor = PreferenceManager
.getDefaultSharedPreferences(getApplicationContext()).edit();
- PlaybackServiceMediaPlayer.PSMPInfo info = mediaPlayer.getPSMPInfo();
- int playerStatus = getCurrentPlayerStatusAsInt(info.playerStatus);
+ int playerStatus = getCurrentPlayerStatusAsInt(mediaPlayer.getPlayerStatus());
editor.putInt(
PlaybackPreferences.PREF_CURRENT_PLAYER_STATUS, playerStatus);
@@ -770,151 +785,158 @@ public class PlaybackService extends Service {
/**
* Used by setupNotification to load notification data in another thread.
*/
- private AsyncTask<Void, Void, Void> notificationSetupTask;
+ private Thread notificationSetupThread;
/**
* Prepares notification and starts the service in the foreground.
*/
- @SuppressLint("NewApi")
private void setupNotification(final PlaybackServiceMediaPlayer.PSMPInfo info) {
final PendingIntent pIntent = PendingIntent.getActivity(this, 0,
PlaybackService.getPlayerActivityIntent(this),
PendingIntent.FLAG_UPDATE_CURRENT);
- if (notificationSetupTask != null) {
- notificationSetupTask.cancel(true);
+ if (notificationSetupThread != null) {
+ notificationSetupThread.interrupt();
}
- notificationSetupTask = new AsyncTask<Void, Void, Void>() {
+ Runnable notificationSetupTask = new Runnable() {
Bitmap icon = null;
@Override
- protected Void doInBackground(Void... params) {
+ 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 {
- int iconSize = getResources().getDimensionPixelSize(
- android.R.dimen.notification_large_icon_width);
- icon = Picasso.with(PlaybackService.this)
+ icon = Glide.with(PlaybackService.this)
.load(info.playable.getImageUri())
- .resize(iconSize, iconSize)
+ .asBitmap()
+ .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY)
+ .centerCrop()
+ .into(iconSize, iconSize)
.get();
- } catch (IOException e) {
- e.printStackTrace();
+ } catch(Throwable tr) {
+ Log.e(TAG, Log.getStackTraceString(tr));
}
}
-
}
if (icon == null) {
icon = BitmapFactory.decodeResource(getApplicationContext().getResources(),
ClientConfig.playbackServiceCallbacks.getNotificationIconResource(getApplicationContext()));
}
- return null;
- }
-
- @Override
- protected void onPostExecute(Void result) {
- super.onPostExecute(result);
if (mediaPlayer == null) {
return;
}
- PlaybackServiceMediaPlayer.PSMPInfo newInfo = mediaPlayer.getPSMPInfo();
+ PlayerStatus playerStatus = mediaPlayer.getPlayerStatus();
final int smallIcon = ClientConfig.playbackServiceCallbacks.getNotificationIconResource(getApplicationContext());
- if (!isCancelled() &&
- started &&
- info.playable != null) {
- String contentText = info.playable.getFeedTitle();
- String contentTitle = info.playable.getEpisodeTitle();
+ if (!Thread.currentThread().isInterrupted() && started && info.playable != null) {
+ String contentText = info.playable.getEpisodeTitle();
+ String contentTitle = info.playable.getFeedTitle();
Notification notification = null;
- if (android.os.Build.VERSION.SDK_INT >= 16) {
- Intent pauseButtonIntent = new Intent( // pause button intent
- PlaybackService.this, PlaybackService.class);
- pauseButtonIntent.putExtra(
- MediaButtonReceiver.EXTRA_KEYCODE,
- KeyEvent.KEYCODE_MEDIA_PAUSE);
- PendingIntent pauseButtonPendingIntent = PendingIntent
- .getService(PlaybackService.this, 0,
- pauseButtonIntent,
- PendingIntent.FLAG_UPDATE_CURRENT);
- Intent playButtonIntent = new Intent( // play button intent
- PlaybackService.this, PlaybackService.class);
- playButtonIntent.putExtra(
- MediaButtonReceiver.EXTRA_KEYCODE,
- KeyEvent.KEYCODE_MEDIA_PLAY);
- PendingIntent playButtonPendingIntent = PendingIntent
- .getService(PlaybackService.this, 1,
- playButtonIntent,
- PendingIntent.FLAG_UPDATE_CURRENT);
- Intent stopButtonIntent = new Intent( // stop button intent
- PlaybackService.this, PlaybackService.class);
- stopButtonIntent.putExtra(
- MediaButtonReceiver.EXTRA_KEYCODE,
- KeyEvent.KEYCODE_MEDIA_STOP);
- PendingIntent stopButtonPendingIntent = PendingIntent
- .getService(PlaybackService.this, 2,
- stopButtonIntent,
- PendingIntent.FLAG_UPDATE_CURRENT);
- Notification.Builder notificationBuilder = new Notification.Builder(
- PlaybackService.this)
- .setContentTitle(contentTitle)
- .setContentText(contentText)
- .setOngoing(true)
- .setContentIntent(pIntent)
- .setLargeIcon(icon)
- .setSmallIcon(smallIcon)
- .setPriority(UserPreferences.getNotifyPriority()); // set notification priority
- if (newInfo.playerStatus == PlayerStatus.PLAYING) {
- notificationBuilder.addAction(android.R.drawable.ic_media_pause, //pause action
- getString(R.string.pause_label),
- pauseButtonPendingIntent);
- } else {
- notificationBuilder.addAction(android.R.drawable.ic_media_play, //play action
- getString(R.string.play_label),
- playButtonPendingIntent);
- }
- if (UserPreferences.isPersistNotify()) {
- notificationBuilder.addAction(android.R.drawable.ic_menu_close_clear_cancel, // stop action
- getString(R.string.stop_label),
- stopButtonPendingIntent);
- }
- if (Build.VERSION.SDK_INT >= 21) {
- notificationBuilder.setStyle(new Notification.MediaStyle()
- .setMediaSession((android.media.session.MediaSession.Token) mediaPlayer.getSessionToken().getToken())
- .setShowActionsInCompactView(0))
- .setVisibility(Notification.VISIBILITY_PUBLIC)
- .setColor(Notification.COLOR_DEFAULT);
- }
+ // 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)
+ .setContentText(contentText)
+ .setOngoing(false)
+ .setContentIntent(pIntent)
+ .setLargeIcon(icon)
+ .setSmallIcon(smallIcon)
+ .setWhen(0) // we don't need the time
+ .setPriority(UserPreferences.getNotifyPriority()); // set notification priority
+ IntList compactActionList = new IntList();
+
+
+ int numActions = 0; // we start and 0 and then increment by 1 for each call to addAction
+
+ // always let them rewind
+ PendingIntent rewindButtonPendingIntent = getPendingIntentForMediaAction(
+ KeyEvent.KEYCODE_MEDIA_REWIND, numActions);
+ notificationBuilder.addAction(android.R.drawable.ic_media_rew,
+ getString(R.string.rewind_label),
+ rewindButtonPendingIntent);
+ numActions++;
+
+ if (playerStatus == PlayerStatus.PLAYING) {
+ PendingIntent pauseButtonPendingIntent = getPendingIntentForMediaAction(
+ KeyEvent.KEYCODE_MEDIA_PAUSE, numActions);
+ notificationBuilder.addAction(android.R.drawable.ic_media_pause, //pause action
+ getString(R.string.pause_label),
+ pauseButtonPendingIntent);
+ compactActionList.add(numActions++);
+ } else {
+ PendingIntent playButtonPendingIntent = getPendingIntentForMediaAction(
+ KeyEvent.KEYCODE_MEDIA_PLAY, numActions);
+ notificationBuilder.addAction(android.R.drawable.ic_media_play, //play action
+ getString(R.string.play_label),
+ playButtonPendingIntent);
+ compactActionList.add(numActions++);
+ }
- notification = notificationBuilder.build();
+ // ff follows play, then we have skip (if it's present)
+ PendingIntent ffButtonPendingIntent = getPendingIntentForMediaAction(
+ KeyEvent.KEYCODE_MEDIA_FAST_FORWARD, numActions);
+ notificationBuilder.addAction(android.R.drawable.ic_media_ff,
+ getString(R.string.fast_forward_label),
+ ffButtonPendingIntent);
+ numActions++;
+
+ if (UserPreferences.isFollowQueue()) {
+ PendingIntent skipButtonPendingIntent = getPendingIntentForMediaAction(
+ KeyEvent.KEYCODE_MEDIA_NEXT, numActions);
+ notificationBuilder.addAction(android.R.drawable.ic_media_next,
+ getString(R.string.skip_episode_label),
+ skipButtonPendingIntent);
+ compactActionList.add(numActions++);
+ }
+
+ PendingIntent stopButtonPendingIntent = getPendingIntentForMediaAction(
+ KeyEvent.KEYCODE_MEDIA_STOP, numActions);
+ notificationBuilder.setStyle(new android.support.v7.app.NotificationCompat.MediaStyle()
+ .setMediaSession(mediaPlayer.getSessionToken())
+ .setShowActionsInCompactView(compactActionList.toArray())
+ .setShowCancelButton(true)
+ .setCancelButtonIntent(stopButtonPendingIntent))
+ .setVisibility(Notification.VISIBILITY_PUBLIC)
+ .setColor(Notification.COLOR_DEFAULT);
+
+ notification = notificationBuilder.build();
+
+ if (playerStatus == PlayerStatus.PLAYING ||
+ playerStatus == PlayerStatus.PREPARING ||
+ playerStatus == PlayerStatus.SEEKING) {
+ startForeground(NOTIFICATION_ID, notification);
} else {
- NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(
- PlaybackService.this)
- .setContentTitle(contentTitle)
- .setContentText(contentText).setOngoing(true)
- .setContentIntent(pIntent).setLargeIcon(icon)
- .setSmallIcon(smallIcon);
- notification = notificationBuilder.build();
+ stopForeground(false);
+ NotificationManager mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+ mNotificationManager.notify(NOTIFICATION_ID, notification);
}
- startForeground(NOTIFICATION_ID, notification);
Log.d(TAG, "Notification set up");
}
}
-
};
- if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) {
- notificationSetupTask
- .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
- } else {
- notificationSetupTask.execute();
- }
+ notificationSetupThread = new Thread(notificationSetupTask);
+ notificationSetupThread.start();
+ }
+ private PendingIntent getPendingIntentForMediaAction(int keycodeValue, int requestCode) {
+ Intent intent = new Intent(
+ PlaybackService.this, PlaybackService.class);
+ intent.putExtra(
+ MediaButtonReceiver.EXTRA_KEYCODE,
+ keycodeValue);
+ return PendingIntent
+ .getService(PlaybackService.this, requestCode,
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT);
}
/**
- * Saves the current position of the media file to the DB
+ * Persists the current position and last played time of the media file.
*
* @param updatePlayedDuration true if played_duration should be updated. This applies only to FeedMedia objects
* @param deltaPlayedDuration value by which played_duration should be increased.
@@ -923,7 +945,7 @@ public class PlaybackService extends Service {
int position = getCurrentPosition();
int duration = getDuration();
float playbackSpeed = getCurrentPlaybackSpeed();
- final Playable playable = mediaPlayer.getPSMPInfo().playable;
+ final Playable playable = mediaPlayer.getPlayable();
if (position != INVALID_TIME && duration != INVALID_TIME && playable != null) {
Log.d(TAG, "Saving current position to " + position);
if (updatePlayedDuration && playable instanceof FeedMedia) {
@@ -939,8 +961,9 @@ public class PlaybackService extends Service {
}
}
playable.saveCurrentPosition(PreferenceManager
- .getDefaultSharedPreferences(getApplicationContext()),
- position
+ .getDefaultSharedPreferences(getApplicationContext()),
+ position,
+ System.currentTimeMillis()
);
}
}
@@ -963,74 +986,6 @@ public class PlaybackService extends Service {
return taskManager.getSleepTimerTimeLeft();
}
- @SuppressLint("NewApi")
- private RemoteControlClient setupRemoteControlClient() {
- if (Build.VERSION.SDK_INT < 14) {
- return null;
- }
-
- Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
- mediaButtonIntent.setComponent(new ComponentName(getPackageName(),
- MediaButtonReceiver.class.getName()));
- PendingIntent mediaPendingIntent = PendingIntent.getBroadcast(
- getApplicationContext(), 0, mediaButtonIntent, 0);
- remoteControlClient = new RemoteControlClient(mediaPendingIntent);
- int controlFlags;
- if (android.os.Build.VERSION.SDK_INT < 16) {
- controlFlags = RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE
- | RemoteControlClient.FLAG_KEY_MEDIA_NEXT;
- } else {
- controlFlags = RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE;
- }
- remoteControlClient.setTransportControlFlags(controlFlags);
- return remoteControlClient;
- }
-
- /**
- * Refresh player status and metadata.
- */
- @SuppressLint("NewApi")
- private void refreshRemoteControlClientState(PlaybackServiceMediaPlayer.PSMPInfo info) {
- if (android.os.Build.VERSION.SDK_INT >= 14) {
- if (remoteControlClient != null) {
- switch (info.playerStatus) {
- case PLAYING:
- remoteControlClient
- .setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING);
- break;
- case PAUSED:
- case INITIALIZED:
- remoteControlClient
- .setPlaybackState(RemoteControlClient.PLAYSTATE_PAUSED);
- break;
- case STOPPED:
- remoteControlClient
- .setPlaybackState(RemoteControlClient.PLAYSTATE_STOPPED);
- break;
- case ERROR:
- remoteControlClient
- .setPlaybackState(RemoteControlClient.PLAYSTATE_ERROR);
- break;
- default:
- remoteControlClient
- .setPlaybackState(RemoteControlClient.PLAYSTATE_BUFFERING);
- }
- if (info.playable != null) {
- MetadataEditor editor = remoteControlClient
- .editMetadata(false);
- editor.putString(MediaMetadataRetriever.METADATA_KEY_TITLE,
- info.playable.getEpisodeTitle());
-
- editor.putString(MediaMetadataRetriever.METADATA_KEY_ALBUM,
- info.playable.getFeedTitle());
-
- editor.apply();
- }
- Log.d(TAG, "RemoteControlClient state was refreshed");
- }
- }
- }
-
private void bluetoothNotifyChange(PlaybackServiceMediaPlayer.PSMPInfo info, String whatChanged) {
boolean isPlaying = false;
@@ -1059,14 +1014,14 @@ public class PlaybackService extends Service {
* Pauses playback when the headset is disconnected and the preference is
* set
*/
- private BroadcastReceiver headsetDisconnected = new BroadcastReceiver() {
+ private final BroadcastReceiver headsetDisconnected = new BroadcastReceiver() {
private static final String TAG = "headsetDisconnected";
private static final int UNPLUGGED = 0;
private static final int PLUGGED = 1;
@Override
public void onReceive(Context context, Intent intent) {
- if (StringUtils.equals(intent.getAction(), Intent.ACTION_HEADSET_PLUG)) {
+ if (TextUtils.equals(intent.getAction(), Intent.ACTION_HEADSET_PLUG)) {
int state = intent.getIntExtra("state", -1);
if (state != -1) {
Log.d(TAG, "Headset plug event. State is " + state);
@@ -1075,7 +1030,7 @@ public class PlaybackService extends Service {
pauseIfPauseOnDisconnect();
} else if (state == PLUGGED) {
Log.d(TAG, "Headset was plugged in during playback.");
- unpauseIfPauseOnDisconnect();
+ unpauseIfPauseOnDisconnect(false);
}
} else {
Log.e(TAG, "Received invalid ACTION_HEADSET_PLUG intent");
@@ -1084,21 +1039,22 @@ public class PlaybackService extends Service {
}
};
- private BroadcastReceiver bluetoothStateUpdated = new BroadcastReceiver() {
+ private final BroadcastReceiver bluetoothStateUpdated = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
- if (StringUtils.equals(intent.getAction(), AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED)) {
- int state = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, -1);
- int prevState = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_PREVIOUS_STATE, -1);
- if (state == AudioManager.SCO_AUDIO_STATE_CONNECTED) {
- Log.d(TAG, "Received bluetooth connection intent");
- unpauseIfPauseOnDisconnect();
+ 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);
+ }
}
}
}
};
- private BroadcastReceiver audioBecomingNoisy = new BroadcastReceiver() {
+ private final BroadcastReceiver audioBecomingNoisy = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
@@ -1125,50 +1081,60 @@ public class PlaybackService extends Service {
}
}
- private void unpauseIfPauseOnDisconnect() {
+ /**
+ * @param bluetooth true if the event for unpausing came from bluetooth
+ */
+ private void unpauseIfPauseOnDisconnect(boolean bluetooth) {
if (transientPause) {
transientPause = false;
- if (UserPreferences.isPauseOnHeadsetDisconnect() && UserPreferences.isUnpauseOnHeadsetReconnect()) {
+ if (!bluetooth && UserPreferences.isUnpauseOnHeadsetReconnect()) {
+ mediaPlayer.resume();
+ } 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) {
+ v.vibrate(500);
+ }
mediaPlayer.resume();
}
}
}
- private BroadcastReceiver shutdownReceiver = new BroadcastReceiver() {
+ private final BroadcastReceiver shutdownReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
- if (StringUtils.equals(intent.getAction(), ACTION_SHUTDOWN_PLAYBACK_SERVICE)) {
+ if (TextUtils.equals(intent.getAction(), ACTION_SHUTDOWN_PLAYBACK_SERVICE)) {
stopSelf();
}
}
};
- private BroadcastReceiver skipCurrentEpisodeReceiver = new BroadcastReceiver() {
+ private final BroadcastReceiver skipCurrentEpisodeReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
- if (StringUtils.equals(intent.getAction(), ACTION_SKIP_CURRENT_EPISODE)) {
+ if (TextUtils.equals(intent.getAction(), ACTION_SKIP_CURRENT_EPISODE)) {
Log.d(TAG, "Received SKIP_CURRENT_EPISODE intent");
- mediaPlayer.endPlayback();
+ mediaPlayer.endPlayback(true);
}
}
};
- private BroadcastReceiver pauseResumeCurrentEpisodeReceiver = new BroadcastReceiver() {
+ private final BroadcastReceiver pauseResumeCurrentEpisodeReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
- if (StringUtils.equals(intent.getAction(), ACTION_RESUME_PLAY_CURRENT_EPISODE)) {
+ if (TextUtils.equals(intent.getAction(), ACTION_RESUME_PLAY_CURRENT_EPISODE)) {
Log.d(TAG, "Received RESUME_PLAY_CURRENT_EPISODE intent");
mediaPlayer.resume();
}
}
};
- private BroadcastReceiver pausePlayCurrentEpisodeReceiver = new BroadcastReceiver() {
+ private final BroadcastReceiver pausePlayCurrentEpisodeReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
- if (StringUtils.equals(intent.getAction(), ACTION_PAUSE_PLAY_CURRENT_EPISODE)) {
+ if (TextUtils.equals(intent.getAction(), ACTION_PAUSE_PLAY_CURRENT_EPISODE)) {
Log.d(TAG, "Received PAUSE_PLAY_CURRENT_EPISODE intent");
mediaPlayer.pause(false, false);
}
@@ -1200,25 +1166,35 @@ public class PlaybackService extends Service {
}
public PlayerStatus getStatus() {
- return mediaPlayer.getPSMPInfo().playerStatus;
+ return mediaPlayer.getPlayerStatus();
}
- public Playable getPlayable() {
- return mediaPlayer.getPSMPInfo().playable;
+ public Playable getPlayable() { return mediaPlayer.getPlayable(); }
+
+ public boolean canSetSpeed() {
+ return mediaPlayer.canSetSpeed();
}
public void setSpeed(float speed) {
mediaPlayer.setSpeed(speed);
}
- public boolean canSetSpeed() {
- return mediaPlayer.canSetSpeed();
+ public void setVolume(float leftVolume, float rightVolume) {
+ mediaPlayer.setVolume(leftVolume, rightVolume);
}
public float getCurrentPlaybackSpeed() {
return mediaPlayer.getPlaybackSpeed();
}
+ public boolean canDownmix() {
+ return mediaPlayer.canDownmix();
+ }
+
+ public void setDownmix(boolean enable) {
+ mediaPlayer.setDownmix(enable);
+ }
+
public boolean isStartWhenPrepared() {
return mediaPlayer.isStartWhenPrepared();
}
@@ -1231,7 +1207,7 @@ public class PlaybackService extends Service {
public void seekTo(final int t) {
if(mediaPlayer.getPlayerStatus() == PlayerStatus.PLAYING
&& GpodnetPreferences.loggedIn()) {
- final Playable playable = mediaPlayer.getPSMPInfo().playable;
+ final Playable playable = mediaPlayer.getPlayable();
if (playable instanceof FeedMedia) {
FeedMedia media = (FeedMedia) playable;
FeedItem item = media.getItem();
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 243ee78e4..a82e82506 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
@@ -1,22 +1,32 @@
package de.danoeh.antennapod.core.service.playback;
+import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.graphics.Bitmap;
import android.media.AudioManager;
-import android.media.RemoteControlClient;
import android.net.wifi.WifiManager;
import android.os.PowerManager;
+import android.preference.PreferenceManager;
+import android.support.annotation.NonNull;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.telephony.TelephonyManager;
import android.util.Log;
import android.util.Pair;
+import android.view.Display;
+import android.view.InputDevice;
+import android.view.KeyEvent;
import android.view.SurfaceHolder;
+import android.view.WindowManager;
-import org.apache.commons.lang3.Validate;
+import com.bumptech.glide.Glide;
import java.io.IOException;
+import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;
@@ -28,9 +38,10 @@ import de.danoeh.antennapod.core.feed.Chapter;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.feed.MediaType;
+import de.danoeh.antennapod.core.glide.ApGlideSettings;
import de.danoeh.antennapod.core.preferences.UserPreferences;
-import de.danoeh.antennapod.core.receiver.MediaButtonReceiver;
import de.danoeh.antennapod.core.storage.DBWriter;
+import de.danoeh.antennapod.core.util.RewindAfterPauseUtils;
import de.danoeh.antennapod.core.util.playback.AudioPlayer;
import de.danoeh.antennapod.core.util.playback.IPlayer;
import de.danoeh.antennapod.core.util.playback.Playable;
@@ -39,7 +50,7 @@ import de.danoeh.antennapod.core.util.playback.VideoPlayer;
/**
* Manages the MediaPlayer object of the PlaybackService.
*/
-public class PlaybackServiceMediaPlayer {
+public class PlaybackServiceMediaPlayer implements SharedPreferences.OnSharedPreferenceChangeListener {
public static final String TAG = "PlaybackSvcMediaPlayer";
/**
@@ -69,6 +80,7 @@ public class PlaybackServiceMediaPlayer {
* have to wait until these operations have finished.
*/
private final ReentrantLock playerLock;
+ private CountDownLatch seekLatch;
private final PSMPCallback callback;
private final Context context;
@@ -80,10 +92,8 @@ public class PlaybackServiceMediaPlayer {
*/
private WifiManager.WifiLock wifiLock;
- public PlaybackServiceMediaPlayer(Context context, PSMPCallback callback) {
- Validate.notNull(context);
- Validate.notNull(callback);
-
+ public PlaybackServiceMediaPlayer(@NonNull Context context,
+ @NonNull PSMPCallback callback) {
this.context = context;
this.callback = callback;
this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
@@ -98,9 +108,26 @@ public class PlaybackServiceMediaPlayer {
}
);
- mediaSession = new MediaSessionCompat(context, TAG);
- mediaSession.setCallback(sessionCallback);
- mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
+ MediaButtonIntentReceiver.setMediaPlayer(this);
+ ComponentName eventReceiver = new ComponentName(context.getPackageName(), MediaButtonIntentReceiver.class.getName());
+ Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
+ mediaButtonIntent.setComponent(eventReceiver);
+ PendingIntent buttonReceiverIntent = PendingIntent.getBroadcast(context, 0, mediaButtonIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+
+ mediaSession = new MediaSessionCompat(context, TAG, eventReceiver, buttonReceiverIntent);
+
+ try {
+ mediaSession.setCallback(sessionCallback);
+ mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
+ mediaSession.setActive(true);
+ } catch (NullPointerException npe) {
+ // on some devices (Huawei) setting active can cause a NullPointerException
+ // even with correct use of the api.
+ // See http://stackoverflow.com/questions/31556679/android-huawei-mediassessioncompat
+ // and https://plus.google.com/+IanLake/posts/YgdTkKFxz7d
+ Log.e(TAG, "NullPointerException while setting up MediaSession");
+ npe.printStackTrace();
+ }
mediaPlayer = null;
statusBeforeSeeking = null;
@@ -108,6 +135,16 @@ public class PlaybackServiceMediaPlayer {
mediaType = MediaType.UNKNOWN;
playerStatus = PlayerStatus.STOPPED;
videoSize = null;
+
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ prefs.registerOnSharedPreferenceChangeListener(this);
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
+ if(key.equals(UserPreferences.PREF_LOCKSCREEN_BACKGROUND)) {
+ updateMediaSessionMetadata();
+ }
}
/**
@@ -136,9 +173,7 @@ public class PlaybackServiceMediaPlayer {
* for playback immediately (see 'prepareImmediately' parameter for more details)
* @param prepareImmediately Set to true if the method should also prepare the episode for playback.
*/
- public void playMediaObject(final Playable playable, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) {
- Validate.notNull(playable);
-
+ public void playMediaObject(@NonNull final Playable playable, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) {
Log.d(TAG, "playMediaObject(...)");
executor.submit(new Runnable() {
@Override
@@ -164,8 +199,7 @@ public class PlaybackServiceMediaPlayer {
*
* @see #playMediaObject(de.danoeh.antennapod.core.util.playback.Playable, boolean, boolean, boolean)
*/
- private void playMediaObject(final Playable playable, final boolean forceReset, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) {
- Validate.notNull(playable);
+ private void playMediaObject(@NonNull final Playable playable, final boolean forceReset, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) {
if (!playerLock.isHeldByCurrentThread()) {
throw new IllegalStateException("method requires playerLock");
}
@@ -193,10 +227,10 @@ public class PlaybackServiceMediaPlayer {
if(oldMedia.hasAlmostEnded()) {
Log.d(TAG, "smart mark as read");
FeedItem item = oldMedia.getItem();
- DBWriter.markItemRead(context, item, true, false);
+ DBWriter.markItemPlayed(item, FeedItem.PLAYED, false);
DBWriter.removeQueueItem(context, item, false);
- DBWriter.addItemToPlaybackHistory(context, oldMedia);
- if (UserPreferences.isAutoDelete()) {
+ DBWriter.addItemToPlaybackHistory(oldMedia);
+ if (item.getFeed().getPreferences().getCurrentAutoDelete()) {
Log.d(TAG, "Delete " + oldMedia.toString());
DBWriter.deleteFeedMediaOfItem(context, oldMedia.getId());
}
@@ -216,7 +250,7 @@ public class PlaybackServiceMediaPlayer {
setPlayerStatus(PlayerStatus.INITIALIZING, media);
try {
media.loadMetadata();
- mediaSession.setMetadata(getMediaSessionMetadata(media));
+ updateMediaSessionMetadata();
if (stream) {
mediaPlayer.setDataSource(media.getStreamUrl());
} else {
@@ -247,11 +281,38 @@ public class PlaybackServiceMediaPlayer {
}
}
- private MediaMetadataCompat getMediaSessionMetadata(Playable p) {
- MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
- builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, p.getEpisodeTitle());
- builder.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, p.getFeedTitle());
- return builder.build();
+ private void updateMediaSessionMetadata() {
+ executor.execute(() -> {
+ final Playable p = this.media;
+ if (p == null) {
+ return;
+ }
+ MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
+ builder.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, p.getFeedTitle());
+ builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, p.getEpisodeTitle());
+ builder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, p.getDuration());
+ builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, p.getEpisodeTitle());
+ builder.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, p.getFeedTitle());
+
+ if (p.getImageUri() != null && UserPreferences.setLockscreenBackground()) {
+ builder.putString(MediaMetadataCompat.METADATA_KEY_ART_URI, p.getImageUri().toString());
+ try {
+ WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+ Display display = wm.getDefaultDisplay();
+ Bitmap art = Glide.with(context)
+ .load(p.getImageUri())
+ .asBitmap()
+ .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY)
+ .centerCrop()
+ .into(display.getWidth(), display.getHeight())
+ .get();
+ builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, art);
+ } catch (Throwable tr) {
+ Log.e(TAG, Log.getStackTraceString(tr));
+ }
+ }
+ mediaSession.setMetadata(builder.build());
+ });
}
@@ -262,13 +323,10 @@ public class PlaybackServiceMediaPlayer {
* This method is executed on an internal executor service.
*/
public void resume() {
- executor.submit(new Runnable() {
- @Override
- public void run() {
- playerLock.lock();
- resumeSync();
- playerLock.unlock();
- }
+ executor.submit(() -> {
+ playerLock.lock();
+ resumeSync();
+ playerLock.unlock();
});
}
@@ -279,24 +337,26 @@ public class PlaybackServiceMediaPlayer {
AudioManager.AUDIOFOCUS_GAIN);
if (focusGained == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
acquireWifiLockIfNecessary();
- setSpeed(Float.parseFloat(UserPreferences.getPlaybackSpeed()));
- mediaPlayer.start();
+ float speed = 1.0f;
+ try {
+ speed = Float.parseFloat(UserPreferences.getPlaybackSpeed());
+ } catch(NumberFormatException e) {
+ Log.e(TAG, Log.getStackTraceString(e));
+ UserPreferences.setPlaybackSpeed(String.valueOf(speed));
+ }
+ setSpeed(speed);
+ setVolume(UserPreferences.getLeftVolume(), UserPreferences.getRightVolume());
+
if (playerStatus == PlayerStatus.PREPARED && media.getPosition() > 0) {
- mediaPlayer.seekTo(media.getPosition());
+ int newPosition = RewindAfterPauseUtils.calculatePositionWithRewind(
+ media.getPosition(),
+ media.getLastPlayedTime());
+ seekToSync(newPosition);
}
+ mediaPlayer.start();
setPlayerStatus(PlayerStatus.PLAYING, media);
pausedBecauseOfTransientAudiofocusLoss = false;
- if (android.os.Build.VERSION.SDK_INT >= 14) {
- RemoteControlClient remoteControlClient = callback.getRemoteControlClient();
- if (remoteControlClient != null) {
- audioManager
- .registerRemoteControlClient(remoteControlClient);
- }
- }
- audioManager
- .registerMediaButtonEventReceiver(new ComponentName(context.getPackageName(),
- MediaButtonReceiver.class.getName()));
media.onPlaybackStart();
} else {
@@ -393,7 +453,7 @@ public class PlaybackServiceMediaPlayer {
}
if (media.getPosition() > 0) {
- mediaPlayer.seekTo(media.getPosition());
+ seekToSync(media.getPosition());
}
if (media.getDuration() == 0) {
@@ -453,8 +513,20 @@ public class PlaybackServiceMediaPlayer {
statusBeforeSeeking = playerStatus;
setPlayerStatus(PlayerStatus.SEEKING, media);
}
+ if(seekLatch != null && seekLatch.getCount() > 0) {
+ try {
+ seekLatch.await(3, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ Log.e(TAG, Log.getStackTraceString(e));
+ }
+ }
+ seekLatch = new CountDownLatch(1);
mediaPlayer.seekTo(t);
-
+ try {
+ seekLatch.await(3, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ Log.e(TAG, Log.getStackTraceString(e));
+ }
} else if (playerStatus == PlayerStatus.INITIALIZED) {
media.setPosition(t);
startWhenPrepared.set(false);
@@ -503,9 +575,7 @@ public class PlaybackServiceMediaPlayer {
/**
* Seek to the start of the specified chapter.
*/
- public void seekToChapter(Chapter c) {
- Validate.notNull(c);
-
+ public void seekToChapter(@NonNull Chapter c) {
seekTo((int) c.getStart());
}
@@ -534,7 +604,9 @@ public class PlaybackServiceMediaPlayer {
* Returns the position of the current media object or INVALID_TIME if the position could not be retrieved.
*/
public int getPosition() {
- playerLock.lock();
+ if (!playerLock.tryLock()) {
+ return INVALID_TIME;
+ }
int retVal = INVALID_TIME;
if (playerStatus == PlayerStatus.PLAYING
@@ -542,6 +614,9 @@ public class PlaybackServiceMediaPlayer {
|| playerStatus == PlayerStatus.PREPARED
|| playerStatus == PlayerStatus.SEEKING) {
retVal = mediaPlayer.getCurrentPosition();
+ if(retVal <= 0 && media != null && media.getPosition() > 0) {
+ retVal = media.getPosition();
+ }
} else if (media != null && media.getPosition() > 0) {
retVal = media.getPosition();
}
@@ -617,12 +692,49 @@ public class PlaybackServiceMediaPlayer {
return retVal;
}
- public MediaType getCurrentMediaType() {
- return mediaType;
+ /**
+ * Sets the playback speed.
+ * This method is executed on an internal executor service.
+ */
+ public void setVolume(final float volumeLeft, float volumeRight) {
+ executor.submit(() -> setVolumeSync(volumeLeft, volumeRight));
}
- public PlayerStatus getPlayerStatus() {
- return playerStatus;
+ /**
+ * Sets the playback speed.
+ * This method is executed on the caller's thread.
+ */
+ private void setVolumeSync(float volumeLeft, float volumeRight) {
+ playerLock.lock();
+ if (media != null && media.getMediaType() == MediaType.AUDIO) {
+ mediaPlayer.setVolume(volumeLeft, volumeRight);
+ Log.d(TAG, "Media player volume was set to " + volumeLeft + " " + volumeRight);
+ }
+ playerLock.unlock();
+ }
+
+ /**
+ * Returns true if the mediaplayer can mix stereo down to mono
+ */
+ public boolean canDownmix() {
+ boolean retVal = false;
+ if (mediaPlayer != null && media != null && media.getMediaType() == MediaType.AUDIO) {
+ retVal = mediaPlayer.canDownmix();
+ }
+ return retVal;
+ }
+
+ public void setDownmix(boolean enable) {
+ playerLock.lock();
+ if (media != null && media.getMediaType() == MediaType.AUDIO) {
+ mediaPlayer.setDownmix(enable);
+ Log.d(TAG, "Media player downmix was set to " + enable);
+ }
+ playerLock.unlock();
+ }
+
+ public MediaType getCurrentMediaType() {
+ return mediaType;
}
public boolean isStreaming() {
@@ -704,6 +816,26 @@ public class PlaybackServiceMediaPlayer {
}
/**
+ * Returns the current status, if you need the media and the player status together, you should
+ * use getPSMPInfo() to make sure they're properly synchronized. Otherwise a race condition
+ * could result in nonsensical results (like a status of PLAYING, but a null playable)
+ * @return the current player status
+ */
+ public PlayerStatus getPlayerStatus() {
+ return playerStatus;
+ }
+
+ /**
+ * Returns the current media, if you need the media and the player status together, you should
+ * use getPSMPInfo() to make sure they're properly synchronized. Otherwise a race condition
+ * could result in nonsensical results (like a status of PLAYING, but a null playable)
+ * @return the current media. May be null
+ */
+ public Playable getPlayable() {
+ return media;
+ }
+
+ /**
* Returns a token to this object's MediaSession. The MediaSession should only be used for notifications
* at the moment.
*
@@ -723,9 +855,7 @@ public class PlaybackServiceMediaPlayer {
* @param newStatus The new PlayerStatus. This must not be null.
* @param newMedia The new playable object of the PSMP object. This can be null.
*/
- private synchronized void setPlayerStatus(PlayerStatus newStatus, Playable newMedia) {
- Validate.notNull(newStatus);
-
+ private synchronized void setPlayerStatus(@NonNull PlayerStatus newStatus, Playable newMedia) {
Log.d(TAG, "Setting player status to " + newStatus);
this.playerStatus = newStatus;
@@ -768,7 +898,12 @@ public class PlaybackServiceMediaPlayer {
} else {
state = PlaybackStateCompat.STATE_NONE;
}
- sessionState.setState(state, PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, getPlaybackSpeed());
+ sessionState.setState(state, getPosition(), getPlaybackSpeed());
+ sessionState.setActions(PlaybackStateCompat.ACTION_PLAY_PAUSE
+ | PlaybackStateCompat.ACTION_REWIND
+ | PlaybackStateCompat.ACTION_FAST_FORWARD
+ | PlaybackStateCompat.ACTION_SKIP_TO_NEXT);
+ mediaSession.setPlaybackState(sessionState.build());
callback.statusChanged(new PSMPInfo(playerStatus, media));
}
@@ -841,25 +976,24 @@ public class PlaybackServiceMediaPlayer {
};
- public void endPlayback() {
- executor.submit(new Runnable() {
- @Override
- public void run() {
- playerLock.lock();
- releaseWifiLockIfNecessary();
+ public void endPlayback(final boolean wasSkipped) {
+ executor.submit(() -> {
+ playerLock.lock();
+ releaseWifiLockIfNecessary();
- if (playerStatus != PlayerStatus.INDETERMINATE) {
- setPlayerStatus(PlayerStatus.INDETERMINATE, media);
- }
- if (mediaPlayer != null) {
- mediaPlayer.reset();
+ boolean isPlaying = playerStatus == PlayerStatus.PLAYING;
- }
- audioManager.abandonAudioFocus(audioFocusChangeListener);
- callback.endPlayback(true);
+ if (playerStatus != PlayerStatus.INDETERMINATE) {
+ setPlayerStatus(PlayerStatus.INDETERMINATE, media);
+ }
+ if (mediaPlayer != null) {
+ mediaPlayer.reset();
- playerLock.unlock();
}
+ audioManager.abandonAudioFocus(audioFocusChangeListener);
+ callback.endPlayback(isPlaying, wasSkipped);
+
+ playerLock.unlock();
});
}
@@ -917,22 +1051,20 @@ public class PlaybackServiceMediaPlayer {
}
}
- public static interface PSMPCallback {
- public void statusChanged(PSMPInfo newInfo);
+ public interface PSMPCallback {
+ void statusChanged(PSMPInfo newInfo);
- public void shouldStop();
+ void shouldStop();
- public void playbackSpeedChanged(float s);
+ void playbackSpeedChanged(float s);
- public void onBufferingUpdate(int percent);
+ void onBufferingUpdate(int percent);
- public boolean onMediaPlayerInfo(int code);
+ boolean onMediaPlayerInfo(int code);
- public boolean onMediaPlayerError(Object inObj, int what, int extra);
+ boolean onMediaPlayerError(Object inObj, int what, int extra);
- public boolean endPlayback(boolean playNextEpisode);
-
- public RemoteControlClient getRemoteControlClient();
+ boolean endPlayback(boolean playNextEpisode, boolean wasSkipped);
}
private IPlayer setMediaPlayerListeners(IPlayer mp) {
@@ -960,9 +1092,9 @@ public class PlaybackServiceMediaPlayer {
return mp;
}
- private final com.aocate.media.MediaPlayer.OnCompletionListener audioCompletionListener = new com.aocate.media.MediaPlayer.OnCompletionListener() {
+ private final org.antennapod.audio.MediaPlayer.OnCompletionListener audioCompletionListener = new org.antennapod.audio.MediaPlayer.OnCompletionListener() {
@Override
- public void onCompletion(com.aocate.media.MediaPlayer mp) {
+ public void onCompletion(org.antennapod.audio.MediaPlayer mp) {
genericOnCompletion();
}
};
@@ -975,12 +1107,12 @@ public class PlaybackServiceMediaPlayer {
};
private void genericOnCompletion() {
- endPlayback();
+ endPlayback(false);
}
- private final com.aocate.media.MediaPlayer.OnBufferingUpdateListener audioBufferingUpdateListener = new com.aocate.media.MediaPlayer.OnBufferingUpdateListener() {
+ private final org.antennapod.audio.MediaPlayer.OnBufferingUpdateListener audioBufferingUpdateListener = new org.antennapod.audio.MediaPlayer.OnBufferingUpdateListener() {
@Override
- public void onBufferingUpdate(com.aocate.media.MediaPlayer mp,
+ public void onBufferingUpdate(org.antennapod.audio.MediaPlayer mp,
int percent) {
genericOnBufferingUpdate(percent);
}
@@ -997,9 +1129,9 @@ public class PlaybackServiceMediaPlayer {
callback.onBufferingUpdate(percent);
}
- private final com.aocate.media.MediaPlayer.OnInfoListener audioInfoListener = new com.aocate.media.MediaPlayer.OnInfoListener() {
+ private final org.antennapod.audio.MediaPlayer.OnInfoListener audioInfoListener = new org.antennapod.audio.MediaPlayer.OnInfoListener() {
@Override
- public boolean onInfo(com.aocate.media.MediaPlayer mp, int what,
+ public boolean onInfo(org.antennapod.audio.MediaPlayer mp, int what,
int extra) {
return genericInfoListener(what);
}
@@ -1016,11 +1148,15 @@ public class PlaybackServiceMediaPlayer {
return callback.onMediaPlayerInfo(what);
}
- private final com.aocate.media.MediaPlayer.OnErrorListener audioErrorListener = new com.aocate.media.MediaPlayer.OnErrorListener() {
+ private final org.antennapod.audio.MediaPlayer.OnErrorListener audioErrorListener = new org.antennapod.audio.MediaPlayer.OnErrorListener() {
@Override
- public boolean onError(com.aocate.media.MediaPlayer mp, int what,
- int extra) {
- return genericOnError(mp, what, extra);
+ public boolean onError(org.antennapod.audio.MediaPlayer mp, int what, int extra) {
+ if(mp.canFallback()) {
+ mp.fallback();
+ return true;
+ } else {
+ return genericOnError(mp, what, extra);
+ }
}
};
@@ -1035,9 +1171,9 @@ public class PlaybackServiceMediaPlayer {
return callback.onMediaPlayerError(inObj, what, extra);
}
- private final com.aocate.media.MediaPlayer.OnSeekCompleteListener audioSeekCompleteListener = new com.aocate.media.MediaPlayer.OnSeekCompleteListener() {
+ private final org.antennapod.audio.MediaPlayer.OnSeekCompleteListener audioSeekCompleteListener = new org.antennapod.audio.MediaPlayer.OnSeekCompleteListener() {
@Override
- public void onSeekComplete(com.aocate.media.MediaPlayer mp) {
+ public void onSeekComplete(org.antennapod.audio.MediaPlayer mp) {
genericSeekCompleteListener();
}
};
@@ -1050,65 +1186,116 @@ public class PlaybackServiceMediaPlayer {
};
private final void genericSeekCompleteListener() {
- executor.submit(new Runnable() {
- @Override
- public void run() {
- playerLock.lock();
- if (playerStatus == PlayerStatus.SEEKING) {
- setPlayerStatus(statusBeforeSeeking, media);
- }
- playerLock.unlock();
+ Thread t = new Thread(() -> {
+ Log.d(TAG, "genericSeekCompleteListener");
+ if(seekLatch != null) {
+ seekLatch.countDown();
+ }
+ playerLock.lock();
+ if (playerStatus == PlayerStatus.SEEKING) {
+ setPlayerStatus(statusBeforeSeeking, media);
}
+ playerLock.unlock();
});
+ t.start();
}
private final MediaSessionCompat.Callback sessionCallback = new MediaSessionCompat.Callback() {
- @Override
- public void onPlay() {
- if (playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PREPARED) {
- resume();
- } else if (playerStatus == PlayerStatus.INITIALIZED) {
- setStartWhenPrepared(true);
- prepare();
- }
- }
+ private static final String TAG = "MediaSessionCompat";
@Override
- public void onPause() {
- super.onPause();
- if (playerStatus == PlayerStatus.PLAYING) {
- pause(false, true);
+ public boolean onMediaButtonEvent(final Intent mediaButton) {
+ Log.d(TAG, "onMediaButtonEvent(" + mediaButton + ")");
+ if (mediaButton != null) {
+ KeyEvent keyEvent = (KeyEvent) mediaButton.getExtras().get(Intent.EXTRA_KEY_EVENT);
+ handleMediaKey(keyEvent);
}
- if (UserPreferences.isPersistNotify()) {
- pause(false, true);
- } else {
- pause(true, true);
- }
- }
-
- @Override
- public void onSkipToNext() {
- super.onSkipToNext();
- endPlayback();
- }
-
- @Override
- public void onFastForward() {
- super.onFastForward();
- seekDelta(UserPreferences.getFastFowardSecs() * 1000);
- }
-
- @Override
- public void onRewind() {
- super.onRewind();
- seekDelta(-UserPreferences.getRewindSecs() * 1000);
+ return false;
}
+ };
- @Override
- public void onSeekTo(long pos) {
- super.onSeekTo(pos);
- seekTo((int) pos);
+ public boolean handleMediaKey(KeyEvent event) {
+ Log.d(TAG, "handleMediaKey(" + event +")");
+ if (event != null
+ && event.getAction() == KeyEvent.ACTION_DOWN
+ && event.getRepeatCount() == 0) {
+ switch (event.getKeyCode()) {
+ case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
+ case KeyEvent.KEYCODE_HEADSETHOOK: {
+ Log.d(TAG, "Received Play/Pause event from RemoteControlClient");
+ if (playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PREPARED) {
+ resume();
+ } else if (playerStatus == PlayerStatus.INITIALIZED) {
+ setStartWhenPrepared(true);
+ prepare();
+ } else if (playerStatus == PlayerStatus.PLAYING) {
+ pause(false, true);
+ if (UserPreferences.isPersistNotify()) {
+ pause(false, true);
+ } else {
+ pause(true, true);
+ }
+ }
+ return true;
+ }
+ case KeyEvent.KEYCODE_MEDIA_PLAY: {
+ Log.d(TAG, "Received Play event from RemoteControlClient");
+ if (playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PREPARED) {
+ resume();
+ } else if (playerStatus == PlayerStatus.INITIALIZED) {
+ setStartWhenPrepared(true);
+ prepare();
+ }
+ return true;
+ }
+ case KeyEvent.KEYCODE_MEDIA_PAUSE: {
+ Log.d(TAG, "Received Pause event from RemoteControlClient");
+ if (playerStatus == PlayerStatus.PLAYING) {
+ pause(false, true);
+ }
+ if (UserPreferences.isPersistNotify()) {
+ pause(false, true);
+ } else {
+ pause(true, true);
+ }
+ return true;
+ }
+ case KeyEvent.KEYCODE_MEDIA_STOP: {
+ Log.d(TAG, "Received Stop event from RemoteControlClient");
+ stop();
+ return true;
+ }
+ case KeyEvent.KEYCODE_MEDIA_PREVIOUS: {
+ seekDelta(-UserPreferences.getRewindSecs() * 1000);
+ return true;
+ }
+ case KeyEvent.KEYCODE_MEDIA_REWIND: {
+ seekDelta(-UserPreferences.getRewindSecs() * 1000);
+ return true;
+ }
+ case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: {
+ seekDelta(UserPreferences.getFastFowardSecs() * 1000);
+ return true;
+ }
+ case KeyEvent.KEYCODE_MEDIA_NEXT: {
+ if(event.getSource() == InputDevice.SOURCE_CLASS_NONE ||
+ UserPreferences.shouldHardwareButtonSkip()) {
+ // assume the skip command comes from a notification or the lockscreen
+ // a >| skip button should actually skip
+ endPlayback(true);
+ } else {
+ // assume skip command comes from a (bluetooth) media button
+ // user actually wants to fast-forward
+ seekDelta(UserPreferences.getFastFowardSecs() * 1000);
+ }
+ return true;
+ }
+ default:
+ Log.d(TAG, "Unhandled key code: " + event.getKeyCode());
+ break;
+ }
}
- };
+ return false;
+ }
}
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 fc73c9446..680fb8777 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
@@ -1,10 +1,10 @@
package de.danoeh.antennapod.core.service.playback;
import android.content.Context;
+import android.os.Vibrator;
+import android.support.annotation.NonNull;
import android.util.Log;
-import org.apache.commons.lang3.Validate;
-
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
@@ -14,12 +14,10 @@ import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
-import de.danoeh.antennapod.core.BuildConfig;
+import de.danoeh.antennapod.core.event.QueueEvent;
import de.danoeh.antennapod.core.feed.FeedItem;
-import de.danoeh.antennapod.core.feed.QueueEvent;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.util.playback.Playable;
-
import de.greenrobot.event.EventBus;
@@ -32,7 +30,7 @@ import de.greenrobot.event.EventBus;
* to notify the PlaybackService about updates from the running tasks.
*/
public class PlaybackServiceTaskManager {
- private static final String TAG = "PlaybackServiceTaskManager";
+ private static final String TAG = "PlaybackServiceTaskMgr";
/**
* Update interval of position saver in milliseconds.
@@ -41,7 +39,7 @@ public class PlaybackServiceTaskManager {
/**
* Notification interval of widget updater in milliseconds.
*/
- public static final int WIDGET_UPDATER_NOTIFICATION_INTERVAL = 1500;
+ public static final int WIDGET_UPDATER_NOTIFICATION_INTERVAL = 1000;
private static final int SCHED_EX_POOL_SIZE = 2;
private final ScheduledThreadPoolExecutor schedExecutor;
@@ -63,10 +61,8 @@ public class PlaybackServiceTaskManager {
* @param context
* @param callback A PSTMCallback object for notifying the user about updates. Must not be null.
*/
- public PlaybackServiceTaskManager(Context context, PSTMCallback callback) {
- Validate.notNull(context);
- Validate.notNull(callback);
-
+ public PlaybackServiceTaskManager(@NonNull Context context,
+ @NonNull PSTMCallback callback) {
this.context = context;
this.callback = callback;
schedExecutor = new ScheduledThreadPoolExecutor(SCHED_EX_POOL_SIZE, new ThreadFactory() {
@@ -82,6 +78,7 @@ public class PlaybackServiceTaskManager {
}
public void onEvent(QueueEvent event) {
+ Log.d(TAG, "onEvent(QueueEvent " + event +")");
cancelQueueLoader();
loadQueue();
}
@@ -101,7 +98,7 @@ public class PlaybackServiceTaskManager {
queueFuture = schedExecutor.submit(new Callable<List<FeedItem>>() {
@Override
public List<FeedItem> call() throws Exception {
- return DBReader.getQueue(context);
+ return DBReader.getQueue();
}
});
}
@@ -168,7 +165,7 @@ public class PlaybackServiceTaskManager {
public synchronized void cancelPositionSaver() {
if (isPositionSaverActive()) {
positionSaverFuture.cancel(false);
- if (BuildConfig.DEBUG) Log.d(TAG, "Cancelled PositionSaver");
+ Log.d(TAG, "Cancelled PositionSaver");
}
}
@@ -186,9 +183,9 @@ public class PlaybackServiceTaskManager {
widgetUpdaterFuture = schedExecutor.scheduleWithFixedDelay(widgetUpdater, WIDGET_UPDATER_NOTIFICATION_INTERVAL,
WIDGET_UPDATER_NOTIFICATION_INTERVAL, TimeUnit.MILLISECONDS);
- if (BuildConfig.DEBUG) Log.d(TAG, "Started WidgetUpdater");
+ Log.d(TAG, "Started WidgetUpdater");
} else {
- if (BuildConfig.DEBUG) Log.d(TAG, "Call to startWidgetUpdater was ignored.");
+ Log.d(TAG, "Call to startWidgetUpdater was ignored.");
}
}
@@ -199,16 +196,16 @@ public class PlaybackServiceTaskManager {
*
* @throws java.lang.IllegalArgumentException if waitingTime <= 0
*/
- public synchronized void setSleepTimer(long waitingTime) {
- Validate.isTrue(waitingTime > 0, "Waiting time <= 0");
+ public synchronized void setSleepTimer(long waitingTime, boolean shakeToReset, boolean vibrate) {
+ if(waitingTime <= 0) {
+ throw new IllegalArgumentException("Waiting time <= 0");
+ }
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Setting sleep timer to " + Long.toString(waitingTime)
- + " milliseconds");
+ Log.d(TAG, "Setting sleep timer to " + Long.toString(waitingTime) + " milliseconds");
if (isSleepTimerActive()) {
sleepTimerFuture.cancel(true);
}
- sleepTimer = new SleepTimer(waitingTime);
+ sleepTimer = new SleepTimer(waitingTime, shakeToReset, vibrate);
sleepTimerFuture = schedExecutor.schedule(sleepTimer, 0, TimeUnit.MILLISECONDS);
}
@@ -216,7 +213,11 @@ public class PlaybackServiceTaskManager {
* Returns true if the sleep timer is currently active.
*/
public synchronized boolean isSleepTimerActive() {
- return sleepTimer != null && sleepTimerFuture != null && !sleepTimerFuture.isCancelled() && !sleepTimerFuture.isDone() && sleepTimer.isWaiting;
+ return sleepTimer != null
+ && sleepTimerFuture != null
+ && !sleepTimerFuture.isCancelled()
+ && !sleepTimerFuture.isDone()
+ && sleepTimer.getWaitingTime() > 0;
}
/**
@@ -224,8 +225,7 @@ public class PlaybackServiceTaskManager {
*/
public synchronized void disableSleepTimer() {
if (isSleepTimerActive()) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Disabling sleep timer");
+ Log.d(TAG, "Disabling sleep timer");
sleepTimerFuture.cancel(true);
}
}
@@ -255,7 +255,7 @@ public class PlaybackServiceTaskManager {
public synchronized void cancelWidgetUpdater() {
if (isWidgetUpdaterActive()) {
widgetUpdaterFuture.cancel(false);
- if (BuildConfig.DEBUG) Log.d(TAG, "Cancelled WidgetUpdater");
+ Log.d(TAG, "Cancelled WidgetUpdater");
}
}
@@ -274,9 +274,7 @@ public class PlaybackServiceTaskManager {
* it will be cancelled first.
* On completion, the callback's onChapterLoaded method will be called.
*/
- public synchronized void startChapterLoader(final Playable media) {
- Validate.notNull(media);
-
+ public synchronized void startChapterLoader(@NonNull final Playable media) {
if (isChapterLoaderActive()) {
cancelChapterLoader();
}
@@ -284,16 +282,14 @@ public class PlaybackServiceTaskManager {
Runnable chapterLoader = new Runnable() {
@Override
public void run() {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Chapter loader started");
+ Log.d(TAG, "Chapter loader started");
if (media.getChapters() == null) {
media.loadChapterMarks();
if (!Thread.currentThread().isInterrupted() && media.getChapters() != null) {
callback.onChapterLoaded(media);
}
}
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Chapter loader stopped");
+ Log.d(TAG, "Chapter loader stopped");
}
};
chapterLoaderFuture = schedExecutor.submit(chapterLoader);
@@ -324,63 +320,90 @@ public class PlaybackServiceTaskManager {
/**
* Sleeps for a given time and then pauses playback.
*/
- private class SleepTimer implements Runnable {
+ protected class SleepTimer implements Runnable {
private static final String TAG = "SleepTimer";
- private static final long UPDATE_INTERVALL = 1000L;
- private volatile long waitingTime;
- private volatile boolean isWaiting;
-
- public SleepTimer(long waitingTime) {
+ private static final long UPDATE_INTERVAL = 1000L;
+ private static final long NOTIFICATION_THRESHOLD = 10000;
+ private long waitingTime;
+ private final boolean shakeToReset;
+ private final boolean vibrate;
+ private ShakeListener shakeListener;
+
+ public SleepTimer(long waitingTime, boolean shakeToReset, boolean vibrate) {
super();
this.waitingTime = waitingTime;
- isWaiting = true;
+ this.shakeToReset = shakeToReset;
+ this.vibrate = vibrate;
}
@Override
public void run() {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Starting");
+ Log.d(TAG, "Starting");
+ boolean notifiedAlmostExpired = false;
+ long lastTick = System.currentTimeMillis();
while (waitingTime > 0) {
try {
- Thread.sleep(UPDATE_INTERVALL);
- waitingTime -= UPDATE_INTERVALL;
-
+ Thread.sleep(UPDATE_INTERVAL);
+ long now = System.currentTimeMillis();
+ waitingTime -= now - lastTick;
+ lastTick = now;
+
+ if(waitingTime < NOTIFICATION_THRESHOLD && !notifiedAlmostExpired) {
+ Log.d(TAG, "Sleep timer is about to expire");
+ if(vibrate) {
+ Vibrator v = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
+ if(v != null) {
+ v.vibrate(500);
+ }
+ }
+ if(shakeListener == null && shakeToReset) {
+ shakeListener = new ShakeListener(context, this);
+ }
+ callback.onSleepTimerAlmostExpired();
+ notifiedAlmostExpired = true;
+ }
if (waitingTime <= 0) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Waiting completed");
- postExecute();
+ Log.d(TAG, "Sleep timer expired");
+ if(shakeListener != null) {
+ shakeListener.pause();
+ shakeListener = null;
+ }
if (!Thread.currentThread().isInterrupted()) {
callback.onSleepTimerExpired();
+ } else {
+ Log.d(TAG, "Sleep timer interrupted");
}
-
}
} catch (InterruptedException e) {
Log.d(TAG, "Thread was interrupted while waiting");
+ e.printStackTrace();
break;
}
}
- postExecute();
- }
-
- protected void postExecute() {
- isWaiting = false;
}
public long getWaitingTime() {
return waitingTime;
}
- public boolean isWaiting() {
- return isWaiting;
+ public void onShake() {
+ setSleepTimer(15 * 60 * 1000, shakeToReset, vibrate);
+ callback.onSleepTimerReset();
+ shakeListener.pause();
+ shakeListener = null;
}
}
- public static interface PSTMCallback {
+ public interface PSTMCallback {
void positionSaverTick();
+ void onSleepTimerAlmostExpired();
+
void onSleepTimerExpired();
+ void onSleepTimerReset();
+
void onWidgetUpdaterTick();
void onChapterLoaded(Playable media);
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
new file mode 100644
index 000000000..fcd96826b
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/ShakeListener.java
@@ -0,0 +1,64 @@
+package de.danoeh.antennapod.core.service.playback;
+
+import android.content.Context;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.util.Log;
+
+public 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;
+
+ public ShakeListener(Context context, PlaybackServiceTaskManager.SleepTimer sleepTimer) {
+ mContext = context;
+ mSleepTimer = sleepTimer;
+ resume();
+ }
+
+ public 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);
+ if (mSensorMgr == null) {
+ throw new UnsupportedOperationException("Sensors not supported");
+ }
+ mAccelerometer = mSensorMgr.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
+ if (!mSensorMgr.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_UI)) { // if not supported
+ mSensorMgr.unregisterListener(this);
+ throw new UnsupportedOperationException("Accelerometer not supported");
+ }
+ }
+
+ public void pause() {
+ if (mSensorMgr != null) {
+ mSensorMgr.unregisterListener(this);
+ mSensorMgr = null;
+ }
+ }
+
+ @Override
+ public void onSensorChanged(SensorEvent event) {
+ float gX = event.values[0] / SensorManager.GRAVITY_EARTH;
+ float gY = event.values[1] / SensorManager.GRAVITY_EARTH;
+ float gZ = event.values[2] / SensorManager.GRAVITY_EARTH;
+
+ double gForce = Math.sqrt(gX*gX + gY*gY + gZ*gZ);
+ if (gForce > 2.25) {
+ Log.d(TAG, "Detected shake " + gForce);
+ mSleepTimer.onShake();
+ }
+ }
+
+ @Override
+ public void onAccuracyChanged(Sensor sensor, int accuracy) {
+ return;
+ }
+
+} \ No newline at end of file
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/APCleanupAlgorithm.java b/core/src/main/java/de/danoeh/antennapod/core/storage/APCleanupAlgorithm.java
index f647fd537..0dc54fb6e 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/storage/APCleanupAlgorithm.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/APCleanupAlgorithm.java
@@ -4,54 +4,70 @@ import android.content.Context;
import android.util.Log;
import java.util.ArrayList;
+import java.util.Calendar;
import java.util.Collections;
-import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ExecutionException;
import de.danoeh.antennapod.core.feed.FeedItem;
+import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.util.LongList;
/**
* Implementation of the EpisodeCleanupAlgorithm interface used by AntennaPod.
*/
-public class APCleanupAlgorithm implements EpisodeCleanupAlgorithm<Integer> {
+public class APCleanupAlgorithm extends EpisodeCleanupAlgorithm {
+
private static final String TAG = "APCleanupAlgorithm";
+ /** the number of days after playback to wait before an item is eligible to be cleaned up */
+ private final int numberOfDaysAfterPlayback;
+
+ public APCleanupAlgorithm(int numberOfDaysAfterPlayback) {
+ this.numberOfDaysAfterPlayback = numberOfDaysAfterPlayback;
+ }
@Override
- public int performCleanup(Context context, Integer episodeNumber) {
- List<FeedItem> candidates = new ArrayList<FeedItem>();
- List<FeedItem> downloadedItems = DBReader.getDownloadedItems(context);
- LongList queue = DBReader.getQueueIDList(context);
+ public int performCleanup(Context context, int numberOfEpisodesToDelete) {
+ List<FeedItem> candidates = new ArrayList<>();
+ List<FeedItem> downloadedItems = DBReader.getDownloadedItems();
List<FeedItem> delete;
+ Calendar cal = Calendar.getInstance();
+ cal.add(Calendar.DAY_OF_MONTH, -1 * numberOfDaysAfterPlayback);
+ Date mostRecentDateForDeletion = cal.getTime();
for (FeedItem item : downloadedItems) {
- if (item.hasMedia() && item.getMedia().isDownloaded()
- && !queue.contains(item.getId()) && item.isRead()) {
- candidates.add(item);
+ if (item.hasMedia()
+ && item.getMedia().isDownloaded()
+ && !item.isTagged(FeedItem.TAG_QUEUE)
+ && item.isPlayed()
+ && !item.isTagged(FeedItem.TAG_FAVORITE)) {
+ FeedMedia media = item.getMedia();
+ // make sure this candidate was played at least the proper amount of days prior
+ // to now
+ if (media != null
+ && media.getPlaybackCompletionDate() != null
+ && media.getPlaybackCompletionDate().before(mostRecentDateForDeletion)) {
+ candidates.add(item);
+ }
}
-
}
- Collections.sort(candidates, new Comparator<FeedItem>() {
- @Override
- public int compare(FeedItem lhs, FeedItem rhs) {
- Date l = lhs.getMedia().getPlaybackCompletionDate();
- Date r = rhs.getMedia().getPlaybackCompletionDate();
+ Collections.sort(candidates, (lhs, rhs) -> {
+ Date l = lhs.getMedia().getPlaybackCompletionDate();
+ Date r = rhs.getMedia().getPlaybackCompletionDate();
- if (l == null) {
- l = new Date();
- }
- if (r == null) {
- r = new Date();
- }
- return l.compareTo(r);
+ if (l == null) {
+ l = new Date();
}
+ if (r == null) {
+ r = new Date();
+ }
+ return l.compareTo(r);
});
- if (candidates.size() > episodeNumber) {
- delete = candidates.subList(0, episodeNumber);
+ if (candidates.size() > numberOfEpisodesToDelete) {
+ delete = candidates.subList(0, numberOfEpisodesToDelete);
} else {
delete = candidates;
}
@@ -69,35 +85,15 @@ public class APCleanupAlgorithm implements EpisodeCleanupAlgorithm<Integer> {
Log.i(TAG, String.format(
"Auto-delete deleted %d episodes (%d requested)", counter,
- episodeNumber));
+ numberOfEpisodesToDelete));
return counter;
}
@Override
- public Integer getDefaultCleanupParameter(Context context) {
- return getPerformAutoCleanupArgs(context, 0);
- }
-
- @Override
- public Integer getPerformCleanupParameter(Context context, List<FeedItem> items) {
- return getPerformAutoCleanupArgs(context, items.size());
+ public int getDefaultCleanupParameter() {
+ return getNumEpisodesToCleanup(0);
}
- static int getPerformAutoCleanupArgs(Context context,
- final int episodeNumber) {
- if (episodeNumber >= 0
- && UserPreferences.getEpisodeCacheSize() != UserPreferences
- .getEpisodeCacheSizeUnlimited()) {
- int downloadedEpisodes = DBReader
- .getNumberOfDownloadedEpisodes(context);
- if (downloadedEpisodes + episodeNumber >= UserPreferences
- .getEpisodeCacheSize()) {
-
- return downloadedEpisodes + episodeNumber
- - UserPreferences.getEpisodeCacheSize();
- }
- }
- return 0;
- }
+ public int getNumberOfDaysAfterPlayback() { return numberOfDaysAfterPlayback; }
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/APDownloadAlgorithm.java b/core/src/main/java/de/danoeh/antennapod/core/storage/APDownloadAlgorithm.java
index 92de1eee7..26dc027bf 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/storage/APDownloadAlgorithm.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/APDownloadAlgorithm.java
@@ -7,7 +7,9 @@ import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
+import de.danoeh.antennapod.core.feed.FeedFilter;
import de.danoeh.antennapod.core.feed.FeedItem;
+import de.danoeh.antennapod.core.feed.FeedPreferences;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.util.NetworkUtils;
import de.danoeh.antennapod.core.util.PowerUtils;
@@ -19,28 +21,24 @@ import de.danoeh.antennapod.core.util.PowerUtils;
public class APDownloadAlgorithm implements AutomaticDownloadAlgorithm {
private static final String TAG = "APDownloadAlgorithm";
- private final APCleanupAlgorithm cleanupAlgorithm = new APCleanupAlgorithm();
-
/**
- * Looks for undownloaded episodes in the queue or list of unread items and request a download if
+ * Looks for undownloaded episodes in the queue or list of new items and request a download if
* 1. Network is available
* 2. The device is charging or the user allows auto download on battery
* 3. There is free space in the episode cache
* This method is executed on an internal single thread executor.
*
* @param context Used for accessing the DB.
- * @param mediaIds If this list is not empty, the method will only download a candidate for automatic downloading if
- * its media ID is in the mediaIds list.
* @return A Runnable that will be submitted to an ExecutorService.
*/
@Override
- public Runnable autoDownloadUndownloadedItems(final Context context, final long... mediaIds) {
+ public Runnable autoDownloadUndownloadedItems(final Context context) {
return new Runnable() {
@Override
public void run() {
// true if we should auto download based on network status
- boolean networkShouldAutoDl = NetworkUtils.autodownloadNetworkAvailable(context)
+ boolean networkShouldAutoDl = NetworkUtils.autodownloadNetworkAvailable()
&& UserPreferences.isEnableAutodownload();
// true if we should auto download based on power status
@@ -53,17 +51,15 @@ public class APDownloadAlgorithm implements AutomaticDownloadAlgorithm {
Log.d(TAG, "Performing auto-dl of undownloaded episodes");
List<FeedItem> candidates;
- if(mediaIds.length > 0) {
- candidates = DBReader.getFeedItems(context, mediaIds);
- } else {
- final List<FeedItem> queue = DBReader.getQueue(context);
- final List<FeedItem> unreadItems = DBReader.getUnreadItemsList(context);
- candidates = new ArrayList<FeedItem>(queue.size() + unreadItems.size());
- candidates.addAll(queue);
- for(FeedItem unreadItem : unreadItems) {
- if(candidates.contains(unreadItem) == false) {
- candidates.add(unreadItem);
- }
+ final List<FeedItem> queue = DBReader.getQueue();
+ final List<FeedItem> newItems = DBReader.getNewItemsList();
+ candidates = new ArrayList<FeedItem>(queue.size() + newItems.size());
+ candidates.addAll(queue);
+ for(FeedItem newItem : newItems) {
+ FeedPreferences feedPrefs = newItem.getFeed().getPreferences();
+ FeedFilter feedFilter = feedPrefs.getFilter();
+ if(candidates.contains(newItem) == false && feedFilter.shouldAutoDownload(newItem)) {
+ candidates.add(newItem);
}
}
@@ -77,9 +73,9 @@ public class APDownloadAlgorithm implements AutomaticDownloadAlgorithm {
}
int autoDownloadableEpisodes = candidates.size();
- int downloadedEpisodes = DBReader.getNumberOfDownloadedEpisodes(context);
- int deletedEpisodes = cleanupAlgorithm.performCleanup(context,
- APCleanupAlgorithm.getPerformAutoCleanupArgs(context, autoDownloadableEpisodes));
+ int downloadedEpisodes = DBReader.getNumberOfDownloadedEpisodes();
+ int deletedEpisodes = UserPreferences.getEpisodeCleanupAlgorithm()
+ .makeRoomForEpisodes(context, autoDownloadableEpisodes);
boolean cacheIsUnlimited = UserPreferences.getEpisodeCacheSize() == UserPreferences
.getEpisodeCacheSizeUnlimited();
int episodeCacheSize = UserPreferences.getEpisodeCacheSize();
@@ -107,5 +103,4 @@ public class APDownloadAlgorithm implements AutomaticDownloadAlgorithm {
}
};
}
-
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/APNullCleanupAlgorithm.java b/core/src/main/java/de/danoeh/antennapod/core/storage/APNullCleanupAlgorithm.java
new file mode 100644
index 000000000..132b61853
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/APNullCleanupAlgorithm.java
@@ -0,0 +1,24 @@
+package de.danoeh.antennapod.core.storage;
+
+import android.content.Context;
+import android.util.Log;
+
+/**
+ * A cleanup algorithm that never removes anything
+ */
+public class APNullCleanupAlgorithm extends EpisodeCleanupAlgorithm {
+
+ private static final String TAG = "APNullCleanupAlgorithm";
+
+ @Override
+ public int performCleanup(Context context, int parameter) {
+ // never clean anything up
+ Log.i(TAG, "performCleanup: Not removing anything");
+ return 0;
+ }
+
+ @Override
+ public int getDefaultCleanupParameter() {
+ return 0;
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/APQueueCleanupAlgorithm.java b/core/src/main/java/de/danoeh/antennapod/core/storage/APQueueCleanupAlgorithm.java
new file mode 100644
index 000000000..234d6162c
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/APQueueCleanupAlgorithm.java
@@ -0,0 +1,81 @@
+package de.danoeh.antennapod.core.storage;
+
+import android.content.Context;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+
+import de.danoeh.antennapod.core.feed.FeedItem;
+import de.danoeh.antennapod.core.feed.FeedMedia;
+import de.danoeh.antennapod.core.util.LongList;
+
+/**
+ * A cleanup algorithm that removes any item that isn't in the queue and isn't a favorite
+ * but only if space is needed.
+ */
+public class APQueueCleanupAlgorithm extends EpisodeCleanupAlgorithm {
+
+ private static final String TAG = "APQueueCleanupAlgorithm";
+
+ @Override
+ public int performCleanup(Context context, int numberOfEpisodesToDelete) {
+ List<FeedItem> candidates = new ArrayList<>();
+ List<FeedItem> downloadedItems = DBReader.getDownloadedItems();
+ List<FeedItem> delete;
+ for (FeedItem item : downloadedItems) {
+ if (item.hasMedia()
+ && item.getMedia().isDownloaded()
+ && !item.isTagged(FeedItem.TAG_QUEUE)
+ && !item.isTagged(FeedItem.TAG_FAVORITE)) {
+ candidates.add(item);
+ }
+ }
+
+ // in the absence of better data, we'll sort by item publication date
+ Collections.sort(candidates, (lhs, rhs) -> {
+ Date l = lhs.getPubDate();
+ Date r = rhs.getPubDate();
+
+ if (l == null) {
+ l = new Date();
+ }
+ if (r == null) {
+ r = new Date();
+ }
+ return l.compareTo(r);
+ });
+
+ if (candidates.size() > numberOfEpisodesToDelete) {
+ delete = candidates.subList(0, numberOfEpisodesToDelete);
+ } else {
+ delete = candidates;
+ }
+
+ for (FeedItem item : delete) {
+ try {
+ DBWriter.deleteFeedMediaOfItem(context, item.getMedia().getId()).get();
+ } catch (InterruptedException | ExecutionException e) {
+ e.printStackTrace();
+ }
+ }
+
+ int counter = delete.size();
+
+
+ Log.i(TAG, String.format(
+ "Auto-delete deleted %d episodes (%d requested)", counter,
+ numberOfEpisodesToDelete));
+
+ return counter;
+ }
+
+ @Override
+ public int getDefaultCleanupParameter() {
+ return getNumEpisodesToCleanup(0);
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/APSPCleanupAlgorithm.java b/core/src/main/java/de/danoeh/antennapod/core/storage/APSPCleanupAlgorithm.java
deleted file mode 100644
index 420bbc09d..000000000
--- a/core/src/main/java/de/danoeh/antennapod/core/storage/APSPCleanupAlgorithm.java
+++ /dev/null
@@ -1,139 +0,0 @@
-package de.danoeh.antennapod.core.storage;
-
-import android.content.Context;
-import android.util.Log;
-
-import org.apache.commons.io.FileUtils;
-
-import java.io.File;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.Iterator;
-import java.util.List;
-import java.util.concurrent.ExecutionException;
-
-import de.danoeh.antennapod.core.feed.FeedItem;
-
-/**
- * Implementation of the EpisodeCleanupAlgorithm interface used by AntennaPodSP apps.
- */
-public class APSPCleanupAlgorithm implements EpisodeCleanupAlgorithm<Integer> {
- private static final String TAG = "APSPCleanupAlgorithm";
-
- final int numberOfNewAutomaticallyDownloadedEpisodes;
-
- public APSPCleanupAlgorithm(int numberOfNewAutomaticallyDownloadedEpisodes) {
- this.numberOfNewAutomaticallyDownloadedEpisodes = numberOfNewAutomaticallyDownloadedEpisodes;
- }
-
- /**
- * Performs an automatic cleanup. Episodes that have been downloaded first will also be deleted first.
- * The episode that is currently playing as well as the n most recent episodes (the exact value is determined
- * by AppPreferences.numberOfNewAutomaticallyDownloadedEpisodes) will never be deleted.
- *
- * @param context
- * @param episodeSize The maximum amount of space that should be freed by this method
- * @return The number of episodes that have been deleted
- */
- @Override
- public int performCleanup(Context context, Integer episodeSize) {
- Log.i(TAG, String.format("performAutoCleanup(%d)", episodeSize));
- if (episodeSize <= 0) {
- return 0;
- }
-
- List<FeedItem> candidates = getAutoCleanupCandidates(context);
- List<FeedItem> deleteList = new ArrayList<FeedItem>();
- long deletedEpisodesSize = 0;
- Collections.sort(candidates, new Comparator<FeedItem>() {
- @Override
- public int compare(FeedItem lhs, FeedItem rhs) {
- File lFile = new File(lhs.getMedia().getFile_url());
- File rFile = new File(rhs.getMedia().getFile_url());
- if (!lFile.exists() || !rFile.exists()) {
- return 0;
- }
- if (FileUtils.isFileOlder(lFile, rFile)) {
- return -1;
- } else {
- return 1;
- }
- }
- });
- // listened episodes will be deleted first
- Iterator<FeedItem> it = candidates.iterator();
- if (it.hasNext()) {
- for (FeedItem i = it.next(); it.hasNext() && deletedEpisodesSize <= episodeSize; i = it.next()) {
- if (!i.getMedia().isPlaying() && i.getMedia().getPlaybackCompletionDate() != null) {
- it.remove();
- deleteList.add(i);
- deletedEpisodesSize += i.getMedia().getSize();
- }
- }
- }
-
- // delete unlistened old episodes if necessary
- it = candidates.iterator();
- if (it.hasNext()) {
- for (FeedItem i = it.next(); it.hasNext() && deletedEpisodesSize <= episodeSize; i = it.next()) {
- if (!i.getMedia().isPlaying()) {
- it.remove();
- deleteList.add(i);
- deletedEpisodesSize += i.getMedia().getSize();
- }
- }
- }
- for (FeedItem item : deleteList) {
- try {
- DBWriter.deleteFeedMediaOfItem(context, item.getMedia().getId()).get();
- } catch (InterruptedException e) {
- e.printStackTrace();
- } catch (ExecutionException e) {
- e.printStackTrace();
- }
- }
- Log.i(TAG, String.format("performAutoCleanup(%d) deleted %d episodes and freed %d bytes of memory",
- episodeSize, deleteList.size(), deletedEpisodesSize));
- return deleteList.size();
- }
-
- @Override
- public Integer getDefaultCleanupParameter(Context context) {
- return 0;
- }
-
- @Override
- public Integer getPerformCleanupParameter(Context context, List<FeedItem> items) {
- int episodeSize = 0;
- for (FeedItem item : items) {
- if (item.hasMedia() && !item.getMedia().isDownloaded()) {
- episodeSize += item.getMedia().getSize();
- }
- }
- return episodeSize;
- }
-
- /**
- * Returns list of FeedItems that have been downloaded, but are not one of the
- * [numberOfNewAutomaticallyDownloadedEpisodes] most recent items.
- */
- private List<FeedItem> getAutoCleanupCandidates(Context context) {
- List<FeedItem> downloaded = new ArrayList<FeedItem>(DBReader.getDownloadedItems(context));
- List<FeedItem> recent = new ArrayList<FeedItem>(DBReader.getRecentlyPublishedEpisodes(context,
- numberOfNewAutomaticallyDownloadedEpisodes));
- for (FeedItem r : recent) {
- if (r.hasMedia() && r.getMedia().isDownloaded()) {
- for (int i = 0; i < downloaded.size(); i++) {
- if (downloaded.get(i).getId() == r.getId()) {
- downloaded.remove(i);
- break;
- }
- }
- }
- }
-
- return downloaded;
-
- }
-}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/APSPDownloadAlgorithm.java b/core/src/main/java/de/danoeh/antennapod/core/storage/APSPDownloadAlgorithm.java
deleted file mode 100644
index f760ec0ce..000000000
--- a/core/src/main/java/de/danoeh/antennapod/core/storage/APSPDownloadAlgorithm.java
+++ /dev/null
@@ -1,72 +0,0 @@
-package de.danoeh.antennapod.core.storage;
-
-import android.content.Context;
-import android.util.Log;
-
-import java.util.Arrays;
-import java.util.Iterator;
-import java.util.List;
-
-import de.danoeh.antennapod.core.BuildConfig;
-import de.danoeh.antennapod.core.feed.FeedItem;
-import de.danoeh.antennapod.core.preferences.UserPreferences;
-import de.danoeh.antennapod.core.util.NetworkUtils;
-
-/**
- * Implements the automatic download algorithm used by AntennaPodSP apps.
- */
-public class APSPDownloadAlgorithm implements AutomaticDownloadAlgorithm {
- private static final String TAG = "APSPDownloadAlgorithm";
-
- private final int numberOfNewAutomaticallyDownloadedEpisodes;
-
- public APSPDownloadAlgorithm(int numberOfNewAutomaticallyDownloadedEpisodes) {
- this.numberOfNewAutomaticallyDownloadedEpisodes = numberOfNewAutomaticallyDownloadedEpisodes;
- }
-
- /**
- * Downloads the most recent episodes automatically. The exact number of
- * episodes that will be downloaded can be set in the AppPreferences.
- *
- * @param context Used for accessing the DB.
- * @return A Runnable that will be submitted to an ExecutorService.
- */
- @Override
- public Runnable autoDownloadUndownloadedItems(final Context context, final long... mediaIds) {
- return new Runnable() {
- @Override
- public void run() {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Performing auto-dl of undownloaded episodes");
- if (NetworkUtils.autodownloadNetworkAvailable(context)
- && UserPreferences.isEnableAutodownload()) {
-
- Arrays.sort(mediaIds);
- List<FeedItem> itemsToDownload = DBReader.getRecentlyPublishedEpisodes(context,
- numberOfNewAutomaticallyDownloadedEpisodes);
- Iterator<FeedItem> it = itemsToDownload.iterator();
-
- for (FeedItem item = it.next(); it.hasNext(); item = it.next()) {
- if (!item.hasMedia()
- || item.getMedia().isDownloaded()
- || Arrays.binarySearch(mediaIds, item.getMedia().getId()) < 0) {
- it.remove();
- }
- }
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Enqueueing " + itemsToDownload.size()
- + " items for automatic download");
- if (!itemsToDownload.isEmpty()) {
- try {
- DBTasks.downloadFeedItems(false, context,
- itemsToDownload.toArray(new FeedItem[itemsToDownload
- .size()]));
- } catch (DownloadRequestException e) {
- e.printStackTrace();
- }
- }
- }
- }
- };
- }
-}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/AutomaticDownloadAlgorithm.java b/core/src/main/java/de/danoeh/antennapod/core/storage/AutomaticDownloadAlgorithm.java
index 9ca9620a7..72c68ddb6 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/storage/AutomaticDownloadAlgorithm.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/AutomaticDownloadAlgorithm.java
@@ -12,9 +12,7 @@ public interface AutomaticDownloadAlgorithm {
* This method is executed on an internal single thread executor.
*
* @param context Used for accessing the DB.
- * @param mediaIds If this list is not empty, the method will only download a candidate for automatic downloading if
- * its media ID is in the mediaIds list.
* @return A Runnable that will be submitted to an ExecutorService.
*/
- public Runnable autoDownloadUndownloadedItems(Context context, long... mediaIds);
+ public Runnable autoDownloadUndownloadedItems(Context context);
}
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 cc20b3d37..0563f878f 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
@@ -1,18 +1,16 @@
package de.danoeh.antennapod.core.storage;
-import android.content.Context;
import android.database.Cursor;
+import android.support.v4.util.ArrayMap;
import android.util.Log;
-import org.apache.commons.lang3.StringUtils;
-
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
+import java.util.Map;
-import de.danoeh.antennapod.core.BuildConfig;
import de.danoeh.antennapod.core.feed.Chapter;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.FeedImage;
@@ -22,14 +20,13 @@ 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.DownloadError;
import de.danoeh.antennapod.core.util.LongIntMap;
import de.danoeh.antennapod.core.util.LongList;
import de.danoeh.antennapod.core.util.comparator.DownloadStatusComparator;
import de.danoeh.antennapod.core.util.comparator.FeedItemPubdateComparator;
import de.danoeh.antennapod.core.util.comparator.PlaybackCompletionDateComparator;
-import de.danoeh.antennapod.core.util.flattr.FlattrStatus;
import de.danoeh.antennapod.core.util.flattr.FlattrThing;
/**
@@ -39,15 +36,16 @@ import de.danoeh.antennapod.core.util.flattr.FlattrThing;
* This class will use the {@link de.danoeh.antennapod.core.feed.EventDistributor} to notify listeners about changes in the database.
*/
public final class DBReader {
+
private static final String TAG = "DBReader";
/**
- * Maximum size of the list returned by {@link #getPlaybackHistory(android.content.Context)}.
+ * Maximum size of the list returned by {@link #getPlaybackHistory()}.
*/
public static final int PLAYBACK_HISTORY_SIZE = 50;
/**
- * Maximum size of the list returned by {@link #getDownloadLog(android.content.Context)}.
+ * Maximum size of the list returned by {@link #getDownloadLog()}.
*/
public static final int DOWNLOAD_LOG_SIZE = 200;
@@ -58,16 +56,14 @@ public final class DBReader {
/**
* Returns a list of Feeds, sorted alphabetically by their title.
*
- * @param context A context that is used for opening a database connection.
* @return A list of Feeds, sorted alphabetically by their title. A Feed-object
* of the returned list does NOT have its list of FeedItems yet. The FeedItem-list
- * can be loaded separately with {@link #getFeedItemList(android.content.Context, de.danoeh.antennapod.core.feed.Feed)}.
+ * can be loaded separately with {@link #getFeedItemList(Feed)}.
*/
- public static List<Feed> getFeedList(final Context context) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Extracting Feedlist");
+ public static List<Feed> getFeedList() {
+ Log.d(TAG, "Extracting Feedlist");
- PodDBAdapter adapter = new PodDBAdapter(context);
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
List<Feed> result = getFeedList(adapter);
adapter.close();
@@ -75,11 +71,8 @@ public final class DBReader {
}
private static List<Feed> getFeedList(PodDBAdapter adapter) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Extracting Feedlist");
-
Cursor feedlistCursor = adapter.getAllFeedsCursor();
- List<Feed> feeds = new ArrayList<Feed>(feedlistCursor.getCount());
+ List<Feed> feeds = new ArrayList<>(feedlistCursor.getCount());
if (feedlistCursor.moveToFirst()) {
do {
@@ -94,12 +87,11 @@ public final class DBReader {
/**
* Returns a list with the download URLs of all feeds.
*
- * @param context A context that is used for opening the database connection.
* @return A list of Strings with the download URLs of all feeds.
*/
- public static List<String> getFeedListDownloadUrls(final Context context) {
- PodDBAdapter adapter = new PodDBAdapter(context);
- List<String> result = new ArrayList<String>();
+ public static List<String> getFeedListDownloadUrls() {
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ List<String> result = new ArrayList<>();
adapter.open();
Cursor feeds = adapter.getFeedCursorDownloadUrls();
if (feeds.moveToFirst()) {
@@ -113,34 +105,28 @@ public final class DBReader {
return result;
}
+
/**
- * Returns a list of 'expired Feeds', i.e. Feeds that have not been updated for a certain amount of time.
- *
- * @param context A context that is used for opening a database connection.
- * @param expirationTime Time that is used for determining whether a feed is outdated or not.
- * A Feed is considered expired if 'lastUpdate < (currentTime - expirationTime)' evaluates to true.
- * @return A list of Feeds, sorted alphabetically by their title. A Feed-object
- * of the returned list does NOT have its list of FeedItems yet. The FeedItem-list
- * can be loaded separately with {@link #getFeedItemList(android.content.Context, de.danoeh.antennapod.core.feed.Feed)}.
+ * Loads additional data in to the feed items from other database queries
+ * @param items the FeedItems who should have other data loaded
*/
- public static List<Feed> getExpiredFeedsList(final Context context, final long expirationTime) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, String.format("getExpiredFeedsList(%d)", expirationTime));
-
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
+ public static void loadAdditionalFeedItemListData(List<FeedItem> items) {
+ loadTagsOfFeedItemList(items);
+ loadFeedDataOfFeedItemList(items);
+ }
- Cursor feedlistCursor = adapter.getExpiredFeedsCursor(expirationTime);
- List<Feed> feeds = new ArrayList<Feed>(feedlistCursor.getCount());
+ public static void loadTagsOfFeedItemList(List<FeedItem> items) {
+ LongList favoriteIds = getFavoriteIDList();
+ LongList queueIds = getQueueIDList();
- if (feedlistCursor.moveToFirst()) {
- do {
- Feed feed = extractFeedFromCursorRow(adapter, feedlistCursor);
- feeds.add(feed);
- } while (feedlistCursor.moveToNext());
+ for (FeedItem item : items) {
+ if (favoriteIds.contains(item.getId())) {
+ item.addTag(FeedItem.TAG_FAVORITE);
+ }
+ if (queueIds.contains(item.getId())) {
+ item.addTag(FeedItem.TAG_QUEUE);
+ }
}
- feedlistCursor.close();
- return feeds;
}
/**
@@ -148,12 +134,10 @@ public final class DBReader {
* The feedID-attribute of a FeedItem must be set to the ID of its feed or the method will
* not find the correct feed of an item.
*
- * @param context A context that is used for opening a database connection.
* @param items The FeedItems whose Feed-objects should be loaded.
*/
- public static void loadFeedDataOfFeedItemlist(Context context,
- List<FeedItem> items) {
- List<Feed> feeds = getFeedList(context);
+ public static void loadFeedDataOfFeedItemList(List<FeedItem> items) {
+ List<Feed> feeds = getFeedList();
for (FeedItem item : items) {
for (Feed feed : feeds) {
if (feed.getId() == item.getFeedId()) {
@@ -169,29 +153,26 @@ public final class DBReader {
/**
* Loads the list of FeedItems for a certain Feed-object. This method should NOT be used if the FeedItems are not
- * used. In order to get information ABOUT the list of FeedItems, consider using {@link #getFeedStatisticsList(android.content.Context)} instead.
+ * used. In order to get information ABOUT the list of FeedItems, consider using {@link #getFeedStatisticsList()} instead.
*
- * @param context A context that is used for opening a database connection.
* @param feed The Feed whose items should be loaded
* @return A list with the FeedItems of the Feed. The Feed-attribute of the FeedItems will already be set correctly.
* The method does NOT change the items-attribute of the feed.
*/
- public static List<FeedItem> getFeedItemList(Context context,
- final Feed feed) {
- Log.d(TAG, "Extracting Feeditems of feed " + feed.getTitle());
+ public static List<FeedItem> getFeedItemList(final Feed feed) {
+ Log.d(TAG, "getFeedItemList() called with: " + "feed = [" + feed + "]");
- PodDBAdapter adapter = new PodDBAdapter(context);
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
Cursor itemlistCursor = adapter.getAllItemsOfFeedCursor(feed);
List<FeedItem> items = extractItemlistFromCursor(adapter,
itemlistCursor);
itemlistCursor.close();
+ adapter.close();
Collections.sort(items, new FeedItemPubdateComparator());
- adapter.close();
-
for (FeedItem item : items) {
item.setFeed(feed);
}
@@ -199,200 +180,121 @@ public final class DBReader {
return items;
}
- static List<FeedItem> extractItemlistFromCursor(Context context, Cursor itemlistCursor) {
- PodDBAdapter adapter = new PodDBAdapter(context);
+ public static List<FeedItem> extractItemlistFromCursor(Cursor itemlistCursor) {
+ Log.d(TAG, "extractItemlistFromCursor() called with: " + "itemlistCursor = [" + itemlistCursor + "]");
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
List<FeedItem> result = extractItemlistFromCursor(adapter, itemlistCursor);
adapter.close();
return result;
}
- private static List<FeedItem> extractItemlistFromCursor(
- PodDBAdapter adapter, Cursor itemlistCursor) {
- ArrayList<String> itemIds = new ArrayList<String>();
- List<FeedItem> items = new ArrayList<FeedItem>(
- itemlistCursor.getCount());
+ private static List<FeedItem> extractItemlistFromCursor(PodDBAdapter adapter,
+ Cursor cursor) {
+ List<FeedItem> result = new ArrayList<>(cursor.getCount());
- if (itemlistCursor.moveToFirst()) {
+ LongList imageIds = new LongList(cursor.getCount());
+ LongList itemIds = new LongList(cursor.getCount());
+ if (cursor.moveToFirst()) {
do {
- long imageIndex = itemlistCursor.getLong(PodDBAdapter.IDX_FI_SMALL_IMAGE);
- FeedImage image = null;
- if (imageIndex != 0) {
- image = getFeedImage(adapter, imageIndex);
- }
+ int indexImage = cursor.getColumnIndex(PodDBAdapter.KEY_IMAGE);
+ long imageId = cursor.getLong(indexImage);
+ imageIds.add(imageId);
- FeedItem item = new FeedItem(itemlistCursor.getLong(PodDBAdapter.IDX_FI_SMALL_ID),
- itemlistCursor.getString(PodDBAdapter.IDX_FI_SMALL_TITLE),
- itemlistCursor.getString(PodDBAdapter.IDX_FI_SMALL_LINK),
- new Date(itemlistCursor.getLong(PodDBAdapter.IDX_FI_SMALL_PUBDATE)),
- itemlistCursor.getString(PodDBAdapter.IDX_FI_SMALL_PAYMENT_LINK),
- itemlistCursor.getLong(PodDBAdapter.IDX_FI_SMALL_FEED),
- new FlattrStatus(itemlistCursor.getLong(PodDBAdapter.IDX_FI_SMALL_FLATTR_STATUS)),
- itemlistCursor.getInt(PodDBAdapter.IDX_FI_SMALL_HAS_CHAPTERS) > 0,
- image,
- (itemlistCursor.getInt(PodDBAdapter.IDX_FI_SMALL_READ) > 0),
- itemlistCursor.getString(PodDBAdapter.IDX_FI_SMALL_ITEM_IDENTIFIER),
- itemlistCursor.getInt(itemlistCursor.getColumnIndex(PodDBAdapter.KEY_AUTO_DOWNLOAD)) > 0
- );
-
- itemIds.add(String.valueOf(item.getId()));
-
- items.add(item);
- } while (itemlistCursor.moveToNext());
+ 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.toArray());
+ 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);
+ FeedMedia media = medias.get(item.getId());
+ item.setMedia(media);
+ if(media != null) {
+ media.setItem(item);
+ }
+ }
}
-
- extractMediafromItemlist(adapter, items, itemIds);
- return items;
+ return result;
}
- private static void extractMediafromItemlist(PodDBAdapter adapter,
- List<FeedItem> items, ArrayList<String> itemIds) {
+ private static Map<Long,FeedMedia> getFeedMedia(PodDBAdapter adapter,
+ long... itemIds) {
- List<FeedItem> itemsCopy = new ArrayList<FeedItem>(items);
- Cursor cursor = adapter.getFeedMediaCursorByItemID(itemIds
- .toArray(new String[itemIds.size()]));
- if (cursor.moveToFirst()) {
- do {
- long itemId = cursor.getLong(PodDBAdapter.KEY_MEDIA_FEEDITEM_INDEX);
- // find matching feed item
- FeedItem item = getMatchingItemForMedia(itemId, itemsCopy);
- if (item != null) {
- item.setMedia(extractFeedMediaFromCursorRow(cursor));
- item.getMedia().setItem(item);
- }
- } while (cursor.moveToNext());
- cursor.close();
+ String[] ids = new String[itemIds.length];
+ for(int i=0, len=itemIds.length; i < len; i++) {
+ ids[i] = String.valueOf(itemIds[i]);
}
- }
-
- private static FeedMedia extractFeedMediaFromCursorRow(final Cursor cursor) {
- long mediaId = cursor.getLong(PodDBAdapter.KEY_ID_INDEX);
- Date playbackCompletionDate = null;
- long playbackCompletionTime = cursor
- .getLong(PodDBAdapter.KEY_PLAYBACK_COMPLETION_DATE_INDEX);
- if (playbackCompletionTime > 0) {
- playbackCompletionDate = new Date(
- playbackCompletionTime);
+ Map<Long,FeedMedia> result = new ArrayMap<>(itemIds.length);
+ Cursor cursor = adapter.getFeedMediaCursor(ids);
+ try {
+ if (cursor.moveToFirst()) {
+ do {
+ int index = cursor.getColumnIndex(PodDBAdapter.KEY_FEEDITEM);
+ long itemId = cursor.getLong(index);
+ FeedMedia media = FeedMedia.fromCursor(cursor);
+ result.put(itemId, media);
+ } while (cursor.moveToNext());
+ }
+ } finally {
+ cursor.close();
}
-
- return new FeedMedia(
- mediaId,
- null,
- cursor.getInt(PodDBAdapter.KEY_DURATION_INDEX),
- cursor.getInt(PodDBAdapter.KEY_POSITION_INDEX),
- cursor.getLong(PodDBAdapter.KEY_SIZE_INDEX),
- cursor.getString(PodDBAdapter.KEY_MIME_TYPE_INDEX),
- cursor.getString(PodDBAdapter.KEY_FILE_URL_INDEX),
- cursor.getString(PodDBAdapter.KEY_DOWNLOAD_URL_INDEX),
- cursor.getInt(PodDBAdapter.KEY_DOWNLOADED_INDEX) > 0,
- playbackCompletionDate,
- cursor.getInt(PodDBAdapter.KEY_PLAYED_DURATION_INDEX));
+ return result;
}
private static Feed extractFeedFromCursorRow(PodDBAdapter adapter,
Cursor cursor) {
- Date lastUpdate = new Date(
- cursor.getLong(PodDBAdapter.IDX_FEED_SEL_STD_LASTUPDATE));
-
final FeedImage image;
- long imageIndex = cursor.getLong(PodDBAdapter.IDX_FEED_SEL_STD_IMAGE);
- if (imageIndex != 0) {
- image = getFeedImage(adapter, imageIndex);
+ int indexImage = cursor.getColumnIndex(PodDBAdapter.KEY_IMAGE);
+ long imageId = cursor.getLong(indexImage);
+ if (imageId != 0) {
+ image = getFeedImage(adapter, imageId);
} else {
image = null;
}
- Feed feed = new Feed(cursor.getLong(PodDBAdapter.IDX_FEED_SEL_STD_ID),
- lastUpdate,
- cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_TITLE),
- cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_LINK),
- cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_DESCRIPTION),
- cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_PAYMENT_LINK),
- cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_AUTHOR),
- cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_LANGUAGE),
- cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_TYPE),
- cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_FEED_IDENTIFIER),
- image,
- cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_FILE_URL),
- cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_DOWNLOAD_URL),
- cursor.getInt(PodDBAdapter.IDX_FEED_SEL_STD_DOWNLOADED) > 0,
- new FlattrStatus(cursor.getLong(PodDBAdapter.IDX_FEED_SEL_STD_FLATTR_STATUS)),
- cursor.getInt(PodDBAdapter.IDX_FEED_SEL_STD_IS_PAGED) > 0,
- cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_NEXT_PAGE_LINK),
- cursor.getString(cursor.getColumnIndex(PodDBAdapter.KEY_HIDE)),
- cursor.getInt(cursor.getColumnIndex(PodDBAdapter.KEY_LAST_UPDATE_FAILED)) > 0
- );
+ Feed feed = Feed.fromCursor(cursor);
if (image != null) {
+ feed.setImage(image);
image.setOwner(feed);
}
- FeedPreferences preferences = new FeedPreferences(cursor.getLong(PodDBAdapter.IDX_FEED_SEL_STD_ID),
- cursor.getInt(PodDBAdapter.IDX_FEED_SEL_PREFERENCES_AUTO_DOWNLOAD) > 0,
- cursor.getString(PodDBAdapter.IDX_FEED_SEL_PREFERENCES_USERNAME),
- cursor.getString(PodDBAdapter.IDX_FEED_SEL_PREFERENCES_PASSWORD));
-
+ FeedPreferences preferences = FeedPreferences.fromCursor(cursor);
feed.setPreferences(preferences);
- return feed;
- }
-
- private static DownloadStatus extractDownloadStatusFromCursorRow(final Cursor cursor) {
- long id = cursor.getLong(PodDBAdapter.KEY_ID_INDEX);
- long feedfileId = cursor.getLong(PodDBAdapter.KEY_FEEDFILE_INDEX);
- int feedfileType = cursor.getInt(PodDBAdapter.KEY_FEEDFILETYPE_INDEX);
- boolean successful = cursor.getInt(PodDBAdapter.KEY_SUCCESSFUL_INDEX) > 0;
- int reason = cursor.getInt(PodDBAdapter.KEY_REASON_INDEX);
- String reasonDetailed = cursor.getString(PodDBAdapter.KEY_REASON_DETAILED_INDEX);
- String title = cursor.getString(PodDBAdapter.KEY_DOWNLOADSTATUS_TITLE_INDEX);
- Date completionDate = new Date(cursor.getLong(PodDBAdapter.KEY_COMPLETION_DATE_INDEX));
-
- return new DownloadStatus(id, title, feedfileId,
- feedfileType, successful, DownloadError.fromCode(reason), completionDate,
- reasonDetailed);
- }
-
- private static FeedItem getMatchingItemForMedia(long itemId,
- List<FeedItem> items) {
- for (FeedItem item : items) {
- if (item.getId() == itemId) {
- return item;
- }
- }
- return null;
+ return feed;
}
- static List<FeedItem> getQueue(Context context, PodDBAdapter adapter) {
+ static List<FeedItem> getQueue(PodDBAdapter adapter) {
Log.d(TAG, "getQueue()");
-
Cursor itemlistCursor = adapter.getQueueCursor();
- List<FeedItem> items = extractItemlistFromCursor(adapter,
- itemlistCursor);
+ List<FeedItem> items = extractItemlistFromCursor(adapter, itemlistCursor);
itemlistCursor.close();
- loadFeedDataOfFeedItemlist(context, items);
-
+ loadAdditionalFeedItemListData(items);
return items;
}
/**
* Loads the IDs of the FeedItems in the queue. This method should be preferred over
- * {@link #getQueue(android.content.Context)} if the FeedItems of the queue are not needed.
+ * {@link #getQueue()} if the FeedItems of the queue are not needed.
*
- * @param context A context that is used for opening a database connection.
* @return A list of IDs sorted by the same order as the queue. The caller can wrap the returned
* list in a {@link de.danoeh.antennapod.core.util.QueueAccess} object for easier access to the queue's properties.
*/
- public static LongList getQueueIDList(Context context) {
- PodDBAdapter adapter = new PodDBAdapter(context);
-
+ public static LongList getQueueIDList() {
+ Log.d(TAG, "getQueueIDList() called");
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
LongList result = getQueueIDList(adapter);
adapter.close();
-
return result;
}
static LongList getQueueIDList(PodDBAdapter adapter) {
- adapter.open();
Cursor queueCursor = adapter.getQueueIDCursor();
LongList queueIds = new LongList(queueCursor.getCount());
@@ -401,40 +303,23 @@ public final class DBReader {
queueIds.add(queueCursor.getLong(0));
} while (queueCursor.moveToNext());
}
+ queueCursor.close();
return queueIds;
}
-
- /**
- * Return the size of the queue.
- *
- * @param context A context that is used for opening a database connection.
- * @return Size of the queue.
- */
- public static int getQueueSize(Context context) {
- Log.d(TAG, "getQueueSize()");
-
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- int size = adapter.getQueueSize();
- adapter.close();
- return size;
- }
-
/**
* Loads a list of the FeedItems in the queue. If the FeedItems of the queue are not used directly, consider using
- * {@link #getQueueIDList(android.content.Context)} instead.
+ * {@link #getQueueIDList()} instead.
*
- * @param context A context that is used for opening a database connection.
* @return A list of FeedItems sorted by the same order as the queue. The caller can wrap the returned
* list in a {@link de.danoeh.antennapod.core.util.QueueAccess} object for easier access to the queue's properties.
*/
- public static List<FeedItem> getQueue(Context context) {
- Log.d(TAG, "getQueue()");
+ public static List<FeedItem> getQueue() {
+ Log.d(TAG, "getQueue() called with: " + "");
- PodDBAdapter adapter = new PodDBAdapter(context);
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
- List<FeedItem> items = getQueue(context, adapter);
+ List<FeedItem> items = getQueue(adapter);
adapter.close();
return items;
}
@@ -442,24 +327,23 @@ public final class DBReader {
/**
* Loads a list of FeedItems whose episode has been downloaded.
*
- * @param context A context that is used for opening a database connection.
* @return A list of FeedItems whose episdoe has been downloaded.
*/
- public static List<FeedItem> getDownloadedItems(Context context) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Extracting downloaded items");
+ public static List<FeedItem> getDownloadedItems() {
+ Log.d(TAG, "getDownloadedItems() called with: " + "");
- PodDBAdapter adapter = new PodDBAdapter(context);
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
Cursor itemlistCursor = adapter.getDownloadedItemsCursor();
List<FeedItem> items = extractItemlistFromCursor(adapter,
itemlistCursor);
itemlistCursor.close();
- loadFeedDataOfFeedItemlist(context, items);
+ loadAdditionalFeedItemListData(items);
+ adapter.close();
+
Collections.sort(items, new FeedItemPubdateComparator());
- adapter.close();
return items;
}
@@ -467,23 +351,18 @@ public final class DBReader {
/**
* Loads a list of FeedItems whose 'read'-attribute is set to false.
*
- * @param context A context that is used for opening a database connection.
- * @return A list of FeedItems whose 'read'-attribute it set to false. If the FeedItems in the list are not used,
- * consider using {@link #getUnreadItemIds(android.content.Context)} instead.
+ * @return A list of FeedItems whose 'read'-attribute it set to false.
*/
- public static List<FeedItem> getUnreadItemsList(Context context) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Extracting unread items list");
+ public static List<FeedItem> getUnreadItemsList() {
+ Log.d(TAG, "getUnreadItemsList() called");
- PodDBAdapter adapter = new PodDBAdapter(context);
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
-
Cursor itemlistCursor = adapter.getUnreadItemsCursor();
- List<FeedItem> items = extractItemlistFromCursor(adapter,
- itemlistCursor);
+ List<FeedItem> items = extractItemlistFromCursor(adapter, itemlistCursor);
itemlistCursor.close();
- loadFeedDataOfFeedItemlist(context, items);
+ loadAdditionalFeedItemListData(items);
adapter.close();
@@ -492,70 +371,76 @@ public final class DBReader {
/**
* Loads a list of FeedItems that are considered new.
- *
- * @param context A context that is used for opening a database connection.
+ * Excludes items from feeds that do not have keep updated enabled.
* @return A list of FeedItems that are considered new.
*/
- public static List<FeedItem> getNewItemsList(Context context) {
- Log.d(TAG, "getNewItemsList()");
+ public static List<FeedItem> getNewItemsList() {
+ Log.d(TAG, "getNewItemsList() called");
- PodDBAdapter adapter = new PodDBAdapter(context);
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
Cursor itemlistCursor = adapter.getNewItemsCursor();
List<FeedItem> items = extractItemlistFromCursor(adapter, itemlistCursor);
itemlistCursor.close();
- loadFeedDataOfFeedItemlist(context, items);
+ loadAdditionalFeedItemListData(items);
adapter.close();
return items;
}
- /**
- * Loads the IDs of the FeedItems whose 'read'-attribute is set to false.
- *
- * @param context A context that is used for opening a database connection.
- * @return A list of IDs of the FeedItems whose 'read'-attribute is set to false. This method should be preferred
- * over {@link #getUnreadItemsList(android.content.Context)} if the FeedItems in the UnreadItems list are not used.
- */
- public static LongList getNewItemIds(Context context) {
- PodDBAdapter adapter = new PodDBAdapter(context);
+ public static List<FeedItem> getFavoriteItemsList() {
+ Log.d(TAG, "getFavoriteItemsList() called");
+
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
- Cursor cursor = adapter.getNewItemIdsCursor();
- LongList itemIds = new LongList(cursor.getCount());
- int i = 0;
- if (cursor.moveToFirst()) {
+
+ Cursor itemlistCursor = adapter.getFavoritesCursor();
+ List<FeedItem> items = extractItemlistFromCursor(adapter, itemlistCursor);
+ itemlistCursor.close();
+
+ loadAdditionalFeedItemListData(items);
+
+ adapter.close();
+
+ return items;
+ }
+
+ public static LongList getFavoriteIDList() {
+ Log.d(TAG, "getFavoriteIDList() called");
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ Cursor favoritesCursor = adapter.getFavoritesCursor();
+
+ LongList favoriteIDs = new LongList(favoritesCursor.getCount());
+ if (favoritesCursor.moveToFirst()) {
do {
- long id = cursor.getLong(PodDBAdapter.KEY_ID_INDEX);
- itemIds.add(id);
- i++;
- } while (cursor.moveToNext());
+ favoriteIDs.add(favoritesCursor.getLong(0));
+ } while (favoritesCursor.moveToNext());
}
- return itemIds;
+ favoritesCursor.close();
+ adapter.close();
+ return favoriteIDs;
}
-
/**
* Loads a list of FeedItems sorted by pubDate in descending order.
*
- * @param context A context that is used for opening a database connection.
* @param limit The maximum number of episodes that should be loaded.
*/
- public static List<FeedItem> getRecentlyPublishedEpisodes(Context context, int limit) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Extracting recently published items list");
+ public static List<FeedItem> getRecentlyPublishedEpisodes(int limit) {
+ Log.d(TAG, "getRecentlyPublishedEpisodes() called with: " + "limit = [" + limit + "]");
- PodDBAdapter adapter = new PodDBAdapter(context);
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
Cursor itemlistCursor = adapter.getRecentlyPublishedItemsCursor(limit);
- List<FeedItem> items = extractItemlistFromCursor(adapter,
- itemlistCursor);
+ List<FeedItem> items = extractItemlistFromCursor(adapter, itemlistCursor);
itemlistCursor.close();
- loadFeedDataOfFeedItemlist(context, items);
+ loadAdditionalFeedItemListData(items);
adapter.close();
@@ -566,26 +451,25 @@ public final class DBReader {
* Loads the playback history from the database. A FeedItem is in the playback history if playback of the correpsonding episode
* has been completed at least once.
*
- * @param context A context that is used for opening a database connection.
* @return The playback history. The FeedItems are sorted by their media's playbackCompletionDate in descending order.
* The size of the returned list is limited by {@link #PLAYBACK_HISTORY_SIZE}.
*/
- public static List<FeedItem> getPlaybackHistory(final Context context) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Loading playback history");
+ public static List<FeedItem> getPlaybackHistory() {
+ Log.d(TAG, "getPlaybackHistory() called");
- PodDBAdapter adapter = new PodDBAdapter(context);
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
Cursor mediaCursor = adapter.getCompletedMediaCursor(PLAYBACK_HISTORY_SIZE);
String[] itemIds = new String[mediaCursor.getCount()];
for (int i = 0; i < itemIds.length && mediaCursor.moveToPosition(i); i++) {
- itemIds[i] = Long.toString(mediaCursor.getLong(PodDBAdapter.KEY_MEDIA_FEEDITEM_INDEX));
+ int index = mediaCursor.getColumnIndex(PodDBAdapter.KEY_FEEDITEM);
+ itemIds[i] = Long.toString(mediaCursor.getLong(index));
}
mediaCursor.close();
Cursor itemCursor = adapter.getFeedItemCursor(itemIds);
List<FeedItem> items = extractItemlistFromCursor(adapter, itemCursor);
- loadFeedDataOfFeedItemlist(context, items);
+ loadAdditionalFeedItemListData(items);
itemCursor.close();
adapter.close();
@@ -596,26 +480,25 @@ public final class DBReader {
/**
* Loads the download log from the database.
*
- * @param context A context that is used for opening a database connection.
* @return A list with DownloadStatus objects that represent the download log.
* The size of the returned list is limited by {@link #DOWNLOAD_LOG_SIZE}.
*/
- public static List<DownloadStatus> getDownloadLog(Context context) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Extracting DownloadLog");
+ public static List<DownloadStatus> getDownloadLog() {
+ Log.d(TAG, "getDownloadLog() called");
- PodDBAdapter adapter = new PodDBAdapter(context);
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
Cursor logCursor = adapter.getDownloadLogCursor(DOWNLOAD_LOG_SIZE);
- List<DownloadStatus> downloadLog = new ArrayList<DownloadStatus>(
- logCursor.getCount());
+ List<DownloadStatus> downloadLog = new ArrayList<>(logCursor.getCount());
if (logCursor.moveToFirst()) {
do {
- downloadLog.add(extractDownloadStatusFromCursorRow(logCursor));
+ DownloadStatus status = DownloadStatus.fromCursor(logCursor);
+ downloadLog.add(status);
} while (logCursor.moveToNext());
}
logCursor.close();
+ adapter.close();
Collections.sort(downloadLog, new DownloadStatusComparator());
return downloadLog;
}
@@ -623,77 +506,47 @@ public final class DBReader {
/**
* Loads the download log for a particular feed from the database.
*
- * @param context A context that is used for opening a database connection.
* @param feed Feed for which the download log is loaded
* @return A list with DownloadStatus objects that represent the feed's download log,
* newest events first.
*/
- public static List<DownloadStatus> getFeedDownloadLog(Context context, Feed feed) {
- Log.d(TAG, "getFeedDownloadLog(CONTEXT, " + feed.toString() + ")");
+ public static List<DownloadStatus> getFeedDownloadLog(Feed feed) {
+ Log.d(TAG, "getFeedDownloadLog() called with: " + "feed = [" + feed + "]");
- PodDBAdapter adapter = new PodDBAdapter(context);
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
Cursor cursor = adapter.getDownloadLog(Feed.FEEDFILETYPE_FEED, feed.getId());
- List<DownloadStatus> downloadLog = new ArrayList<DownloadStatus>(
- cursor.getCount());
-
- if (cursor.moveToFirst()) {
- do {
- downloadLog.add(extractDownloadStatusFromCursorRow(cursor));
- } while (cursor.moveToNext());
- }
- cursor.close();
- Collections.sort(downloadLog, new DownloadStatusComparator());
- return downloadLog;
- }
-
- /**
- * Loads the download log for a particular feed media from the database.
- *
- * @param context A context that is used for opening a database connection.
- * @param media Feed media for which the download log is loaded
- * @return A list with DownloadStatus objects that represent the feed media's download log,
- * newest events first.
- */
- public static List<DownloadStatus> getFeedMediaDownloadLog(Context context, FeedMedia media) {
- Log.d(TAG, "getFeedDownloadLog(CONTEXT, " + media.toString() + ")");
-
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- Cursor cursor = adapter.getDownloadLog(FeedMedia.FEEDFILETYPE_FEEDMEDIA, media.getId());
- List<DownloadStatus> downloadLog = new ArrayList<DownloadStatus>(
- cursor.getCount());
+ List<DownloadStatus> downloadLog = new ArrayList<>(cursor.getCount());
if (cursor.moveToFirst()) {
do {
- downloadLog.add(extractDownloadStatusFromCursorRow(cursor));
+ DownloadStatus status = DownloadStatus.fromCursor(cursor);
+ downloadLog.add(status);
} while (cursor.moveToNext());
}
cursor.close();
+ adapter.close();
Collections.sort(downloadLog, new DownloadStatusComparator());
return downloadLog;
}
/**
* Loads the FeedItemStatistics objects of all Feeds in the database. This method should be preferred over
- * {@link #getFeedItemList(android.content.Context, de.danoeh.antennapod.core.feed.Feed)} if only metadata about
+ * {@link #getFeedItemList(Feed)} if only metadata about
* the FeedItems is needed.
*
- * @param context A context that is used for opening a database connection.
* @return A list of FeedItemStatistics objects sorted alphabetically by their Feed's title.
*/
- public static List<FeedItemStatistics> getFeedStatisticsList(final Context context) {
- PodDBAdapter adapter = new PodDBAdapter(context);
+ public static List<FeedItemStatistics> getFeedStatisticsList() {
+ Log.d(TAG, "getFeedStatisticsList() called");
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
- List<FeedItemStatistics> result = new ArrayList<FeedItemStatistics>();
+ List<FeedItemStatistics> result = new ArrayList<>();
Cursor cursor = adapter.getFeedStatisticsCursor();
if (cursor.moveToFirst()) {
do {
- result.add(new FeedItemStatistics(cursor.getLong(PodDBAdapter.IDX_FEEDSTATISTICS_FEED),
- cursor.getInt(PodDBAdapter.IDX_FEEDSTATISTICS_NUM_ITEMS),
- cursor.getInt(PodDBAdapter.IDX_FEEDSTATISTICS_NEW_ITEMS),
- cursor.getInt(PodDBAdapter.IDX_FEEDSTATISTICS_IN_PROGRESS_EPISODES),
- new Date(cursor.getLong(PodDBAdapter.IDX_FEEDSTATISTICS_LATEST_EPISODE))));
+ FeedItemStatistics fis = FeedItemStatistics.fromCursor(cursor);
+ result.add(fis);
} while (cursor.moveToNext());
}
@@ -705,28 +558,26 @@ public final class DBReader {
/**
* Loads a specific Feed from the database.
*
- * @param context A context that is used for opening a database connection.
* @param feedId The ID of the Feed
* @return The Feed or null if the Feed could not be found. The Feeds FeedItems will also be loaded from the
* database and the items-attribute will be set correctly.
*/
- public static Feed getFeed(final Context context, final long feedId) {
- PodDBAdapter adapter = new PodDBAdapter(context);
+ public static Feed getFeed(final long feedId) {
+ Log.d(TAG, "getFeed() called with: " + "feedId = [" + feedId + "]");
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
- Feed result = getFeed(context, feedId, adapter);
+ Feed result = getFeed(feedId, adapter);
adapter.close();
return result;
}
- static Feed getFeed(final Context context, final long feedId, PodDBAdapter adapter) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Loading feed with id " + feedId);
+ static Feed getFeed(final long feedId, PodDBAdapter adapter) {
Feed feed = null;
Cursor feedCursor = adapter.getFeedCursor(feedId);
if (feedCursor.moveToFirst()) {
feed = extractFeedFromCursorRow(adapter, feedCursor);
- feed.setItems(getFeedItemList(context, feed));
+ feed.setItems(getFeedItemList(feed));
} else {
Log.e(TAG, "getFeed could not find feed with id " + feedId);
}
@@ -734,9 +585,8 @@ public final class DBReader {
return feed;
}
- static FeedItem getFeedItem(final Context context, final long itemId, PodDBAdapter adapter) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Loading feeditem with id " + itemId);
+ static FeedItem getFeedItem(final long itemId, PodDBAdapter adapter) {
+ Log.d(TAG, "Loading feeditem with id " + itemId);
FeedItem item = null;
Cursor itemCursor = adapter.getFeedItemCursor(Long.toString(itemId));
@@ -744,16 +594,17 @@ public final class DBReader {
List<FeedItem> list = extractItemlistFromCursor(adapter, itemCursor);
if (list.size() > 0) {
item = list.get(0);
- loadFeedDataOfFeedItemlist(context, list);
+ loadAdditionalFeedItemListData(list);
if (item.hasChapters()) {
loadChaptersOfFeedItem(adapter, item);
}
}
}
+ itemCursor.close();
return item;
}
- static List<FeedItem> getFeedItems(final Context context, PodDBAdapter adapter, final long... itemIds) {
+ static List<FeedItem> getFeedItems(PodDBAdapter adapter, final long... itemIds) {
String[] ids = new String[itemIds.length];
for(int i = 0; i < itemIds.length; i++) {
@@ -766,7 +617,7 @@ public final class DBReader {
Cursor itemCursor = adapter.getFeedItemCursor(ids);
if (itemCursor.moveToFirst()) {
result = extractItemlistFromCursor(adapter, itemCursor);
- loadFeedDataOfFeedItemlist(context, result);
+ loadAdditionalFeedItemListData(result);
for(FeedItem item : result) {
if (item.hasChapters()) {
loadChaptersOfFeedItem(adapter, item);
@@ -775,6 +626,7 @@ public final class DBReader {
} else {
result = Collections.emptyList();
}
+ itemCursor.close();
return result;
}
@@ -783,23 +635,21 @@ public final class DBReader {
* Loads a specific FeedItem from the database. This method should not be used for loading more
* than one FeedItem because this method might query the database several times for each item.
*
- * @param context A context that is used for opening a database connection.
* @param itemId The ID of the FeedItem
* @return The FeedItem or null if the FeedItem could not be found. All FeedComponent-attributes
* as well as chapter marks of the FeedItem will also be loaded from the database.
*/
- public static FeedItem getFeedItem(final Context context, final long itemId) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Loading feeditem with id " + itemId);
+ public static FeedItem getFeedItem(final long itemId) {
+ Log.d(TAG, "getFeedItem() called with: " + "itemId = [" + itemId + "]");
- PodDBAdapter adapter = new PodDBAdapter(context);
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
- FeedItem item = getFeedItem(context, itemId, adapter);
+ FeedItem item = getFeedItem(itemId, adapter);
adapter.close();
return item;
}
- static FeedItem getFeedItem(final Context context, final String podcastUrl, final String episodeUrl, PodDBAdapter adapter) {
+ static FeedItem getFeedItem(final String podcastUrl, final String episodeUrl, PodDBAdapter adapter) {
Log.d(TAG, "Loading feeditem with podcast url " + podcastUrl + " and episode url " + episodeUrl);
FeedItem item = null;
Cursor itemCursor = adapter.getFeedItemCursor(podcastUrl, episodeUrl);
@@ -807,12 +657,13 @@ public final class DBReader {
List<FeedItem> list = extractItemlistFromCursor(adapter, itemCursor);
if (list.size() > 0) {
item = list.get(0);
- loadFeedDataOfFeedItemlist(context, list);
+ loadAdditionalFeedItemListData(list);
if (item.hasChapters()) {
loadChaptersOfFeedItem(adapter, item);
}
}
}
+ itemCursor.close();
return item;
}
@@ -820,17 +671,15 @@ public final class DBReader {
* Loads specific FeedItems from the database. This method canbe used for loading more
* than one FeedItem
*
- * @param context A context that is used for opening a database connection.
* @param itemIds The IDs of the FeedItems
* @return The FeedItems or an empty list if none of the FeedItems could be found. All FeedComponent-attributes
* as well as chapter marks of the FeedItems will also be loaded from the database.
*/
- public static List<FeedItem> getFeedItems(final Context context, final long... itemIds) {
- Log.d(TAG, "Loading feeditem with ids: " + StringUtils.join(itemIds, ","));
-
- PodDBAdapter adapter = new PodDBAdapter(context);
+ public static List<FeedItem> getFeedItems(final long... itemIds) {
+ Log.d(TAG, "getFeedItems() called with: " + "itemIds = [" + itemIds + "]");
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
- List<FeedItem> items = getFeedItems(context, adapter, itemIds);
+ List<FeedItem> items = getFeedItems(adapter, itemIds);
adapter.close();
return items;
}
@@ -839,47 +688,55 @@ public final class DBReader {
/**
* Returns credentials based on image URL
*
- * @param context A context that is used for opening a database connection.
* @param imageUrl The URL of the image
* @return Credentials in format "<Username>:<Password>", empty String if no authorization given
*/
- public static String getImageAuthentication(final Context context, final String imageUrl) {
- Log.d(TAG, "Loading credentials for image with URL " + imageUrl);
+ public static String getImageAuthentication(final String imageUrl) {
+ Log.d(TAG, "getImageAuthentication() called with: " + "imageUrl = [" + imageUrl + "]");
- PodDBAdapter adapter = new PodDBAdapter(context);
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
- String credentials = getImageAuthentication(context, imageUrl, adapter);
+ String credentials = getImageAuthentication(imageUrl, adapter);
adapter.close();
return credentials;
}
- static String getImageAuthentication(final Context context, final String imageUrl, PodDBAdapter adapter) {
+ static String getImageAuthentication(final String imageUrl, PodDBAdapter adapter) {
String credentials = null;
Cursor cursor = adapter.getImageAuthenticationCursor(imageUrl);
- if (cursor.moveToFirst()) {
- String username = cursor.getString(0);
- String password = cursor.getString(1);
- return username + ":" + password;
+ try {
+ if (cursor.moveToFirst()) {
+ String username = cursor.getString(0);
+ String password = cursor.getString(1);
+ if(username != null && password != null) {
+ credentials = username + ":" + password;
+ } else {
+ credentials = "";
+ }
+ } else {
+ credentials = "";
+ }
+ } finally {
+ cursor.close();
}
- return "";
+ return credentials;
}
/**
* Loads a specific FeedItem from the database.
*
- * @param context A context that is used for opening a database connection.
* @param podcastUrl the corresponding feed's url
* @param episodeUrl the feed item's url
* @return The FeedItem or null if the FeedItem could not be found. All FeedComponent-attributes
* as well as chapter marks of the FeedItem will also be loaded from the database.
*/
- public static FeedItem getFeedItem(final Context context, final String podcastUrl, final String episodeUrl) {
- Log.d(TAG, "Loading feeditem with podcast url " + podcastUrl + " and episode url " + episodeUrl);
+ public static FeedItem getFeedItem(final String podcastUrl, final String episodeUrl) {
+ Log.d(TAG, "getFeedItem() called with: " + "podcastUrl = [" + podcastUrl + "], episodeUrl = [" + episodeUrl + "]");
- PodDBAdapter adapter = new PodDBAdapter(context);
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
- FeedItem item = getFeedItem(context, podcastUrl, episodeUrl, adapter);
+ FeedItem item = getFeedItem(podcastUrl, episodeUrl, adapter);
adapter.close();
return item;
}
@@ -887,21 +744,22 @@ public final class DBReader {
/**
* Loads additional information about a FeedItem, e.g. shownotes
*
- * @param context A context that is used for opening a database connection.
* @param item The FeedItem
*/
- public static void loadExtraInformationOfFeedItem(final Context context, final FeedItem item) {
- PodDBAdapter adapter = new PodDBAdapter(context);
+ public static void loadExtraInformationOfFeedItem(final FeedItem item) {
+ Log.d(TAG, "loadExtraInformationOfFeedItem() called with: " + "item = [" + item + "]");
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
Cursor extraCursor = adapter.getExtraInformationOfItem(item);
if (extraCursor.moveToFirst()) {
- String description = extraCursor
- .getString(PodDBAdapter.IDX_FI_EXTRA_DESCRIPTION);
- String contentEncoded = extraCursor
- .getString(PodDBAdapter.IDX_FI_EXTRA_CONTENT_ENCODED);
+ int indexDescription = extraCursor.getColumnIndex(PodDBAdapter.KEY_DESCRIPTION);
+ String description = extraCursor.getString(indexDescription);
+ int indexContentEncoded = extraCursor.getColumnIndex(PodDBAdapter.KEY_CONTENT_ENCODED);
+ String contentEncoded = extraCursor.getString(indexContentEncoded);
item.setDescription(description);
item.setContentEncoded(contentEncoded);
}
+ extraCursor.close();
adapter.close();
}
@@ -910,31 +768,31 @@ public final class DBReader {
* any chapters that this FeedItem has. If no chapters were found in the database, the chapters
* reference of the FeedItem will be set to null.
*
- * @param context A context that is used for opening a database connection.
* @param item The FeedItem
*/
- public static void loadChaptersOfFeedItem(final Context context, final FeedItem item) {
- PodDBAdapter adapter = new PodDBAdapter(context);
+ public static void loadChaptersOfFeedItem(final FeedItem item) {
+ Log.d(TAG, "loadChaptersOfFeedItem() called with: " + "item = [" + item + "]");
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
loadChaptersOfFeedItem(adapter, item);
adapter.close();
}
static void loadChaptersOfFeedItem(PodDBAdapter adapter, FeedItem item) {
- Cursor chapterCursor = adapter
- .getSimpleChaptersOfFeedItemCursor(item);
+ Cursor chapterCursor = adapter.getSimpleChaptersOfFeedItemCursor(item);
if (chapterCursor.moveToFirst()) {
- item.setChapters(new ArrayList<Chapter>());
+ item.setChapters(new ArrayList<>());
do {
- int chapterType = chapterCursor
- .getInt(PodDBAdapter.KEY_CHAPTER_TYPE_INDEX);
+ int indexType = chapterCursor.getColumnIndex(PodDBAdapter.KEY_CHAPTER_TYPE);
+ int indexStart = chapterCursor.getColumnIndex(PodDBAdapter.KEY_START);
+ int indexTitle = chapterCursor.getColumnIndex(PodDBAdapter.KEY_TITLE);
+ int indexLink = chapterCursor.getColumnIndex(PodDBAdapter.KEY_LINK);
+
+ int chapterType = chapterCursor.getInt(indexType);
Chapter chapter = null;
- long start = chapterCursor
- .getLong(PodDBAdapter.KEY_CHAPTER_START_INDEX);
- String title = chapterCursor
- .getString(PodDBAdapter.KEY_TITLE_INDEX);
- String link = chapterCursor
- .getString(PodDBAdapter.KEY_CHAPTER_LINK_INDEX);
+ long start = chapterCursor.getLong(indexStart);
+ String title = chapterCursor.getString(indexTitle);
+ String link = chapterCursor.getString(indexLink);
switch (chapterType) {
case SimpleChapter.CHAPTERTYPE_SIMPLECHAPTER:
@@ -951,8 +809,8 @@ public final class DBReader {
break;
}
if (chapter != null) {
- chapter.setId(chapterCursor
- .getLong(PodDBAdapter.KEY_ID_INDEX));
+ int indexId = chapterCursor.getColumnIndex(PodDBAdapter.KEY_ID);
+ chapter.setId(chapterCursor.getLong(indexId));
item.getChapters().add(chapter);
}
} while (chapterCursor.moveToNext());
@@ -965,11 +823,11 @@ public final class DBReader {
/**
* Returns the number of downloaded episodes.
*
- * @param context A context that is used for opening a database connection.
* @return The number of downloaded episodes.
*/
- public static int getNumberOfDownloadedEpisodes(final Context context) {
- PodDBAdapter adapter = new PodDBAdapter(context);
+ public static int getNumberOfDownloadedEpisodes() {
+ Log.d(TAG, "getNumberOfDownloadedEpisodes() called with: " + "");
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
final int result = adapter.getNumberOfDownloadedEpisodes();
adapter.close();
@@ -977,29 +835,16 @@ public final class DBReader {
}
/**
- * Returns the number of unread items.
- *
- * @param context A context that is used for opening a database connection.
- * @return The number of unread items.
- */
- public static int getNumberOfNewItems(final Context context) {
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- final int result = adapter.getNumberOfNewItems();
- adapter.close();
- return result;
- }
-
- /**
- * Returns a map containing the number of unread items per feed
+ * Searches the DB for a FeedImage of the given id.
*
- * @param context A context that is used for opening a database connection.
- * @return The number of unread items per feed.
+ * @param imageId The id of the object
+ * @return The found object
*/
- public static LongIntMap getNumberOfUnreadFeedItems(final Context context, long... feedIds) {
- PodDBAdapter adapter = new PodDBAdapter(context);
+ public static FeedImage getFeedImage(final long imageId) {
+ Log.d(TAG, "getFeedImage() called with: " + "imageId = [" + imageId + "]");
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
- final LongIntMap result = adapter.getNumberOfUnreadFeedItems(feedIds);
+ FeedImage result = getFeedImage(adapter, imageId);
adapter.close();
return result;
}
@@ -1007,60 +852,58 @@ public final class DBReader {
/**
* Searches the DB for a FeedImage of the given id.
*
- * @param context A context that is used for opening a database connection.
* @param imageId The id of the object
* @return The found object
*/
- public static FeedImage getFeedImage(final Context context, final long imageId) {
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- FeedImage result = getFeedImage(adapter, imageId);
- adapter.close();
- return result;
+ 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 id The id of the object
- * @return The found object
+ * @param imageIds The ids of the images
+ * @return Map that associates the id of an image with the image itself
*/
- static FeedImage getFeedImage(PodDBAdapter adapter, final long id) {
- Cursor cursor = adapter.getImageCursor(id);
- if ((cursor.getCount() == 0) || !cursor.moveToFirst()) {
- return null;
+ 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]);
}
- FeedImage image = new FeedImage(id, cursor.getString(cursor
- .getColumnIndex(PodDBAdapter.KEY_TITLE)),
- cursor.getString(cursor
- .getColumnIndex(PodDBAdapter.KEY_FILE_URL)),
- cursor.getString(cursor
- .getColumnIndex(PodDBAdapter.KEY_DOWNLOAD_URL)),
- cursor.getInt(cursor
- .getColumnIndex(PodDBAdapter.KEY_DOWNLOADED)) > 0
- );
- cursor.close();
- return image;
+ Cursor cursor = adapter.getImageCursor(ids);
+ Map<Long, FeedImage> result = new ArrayMap<>(cursor.getCount());
+ try {
+ if ((cursor.getCount() == 0) || !cursor.moveToFirst()) {
+ return Collections.emptyMap();
+ }
+ do {
+ FeedImage image = FeedImage.fromCursor(cursor);
+ result.put(image.getId(), image);
+ } while(cursor.moveToNext());
+ } finally {
+ cursor.close();
+ }
+ return result;
}
/**
* Searches the DB for a FeedMedia of the given id.
*
- * @param context A context that is used for opening a database connection.
* @param mediaId The id of the object
* @return The found object
*/
- public static FeedMedia getFeedMedia(final Context context, final long mediaId) {
- PodDBAdapter adapter = new PodDBAdapter(context);
+ public static FeedMedia getFeedMedia(final long mediaId) {
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
Cursor mediaCursor = adapter.getSingleFeedMediaCursor(mediaId);
FeedMedia media = null;
if (mediaCursor.moveToFirst()) {
- final long itemId = mediaCursor.getLong(PodDBAdapter.KEY_MEDIA_FEEDITEM_INDEX);
- media = extractFeedMediaFromCursorRow(mediaCursor);
- FeedItem item = getFeedItem(context, itemId);
+ int indexFeedItem = mediaCursor.getColumnIndex(PodDBAdapter.KEY_FEEDITEM);
+ final long itemId = mediaCursor.getLong(indexFeedItem);
+ media = FeedMedia.fromCursor(mediaCursor);
+ FeedItem item = getFeedItem(itemId);
if (media != null && item != null) {
media.setItem(item);
item.setMedia(media);
@@ -1076,13 +919,13 @@ public final class DBReader {
/**
* Returns the flattr queue as a List of FlattrThings. The list consists of Feeds and FeedItems.
*
- * @param context A context that is used for opening a database connection.
* @return The flattr queue as a List.
*/
- public static List<FlattrThing> getFlattrQueue(Context context) {
- PodDBAdapter adapter = new PodDBAdapter(context);
+ public static List<FlattrThing> getFlattrQueue() {
+ Log.d(TAG, "getFlattrQueue() called with: " + "");
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
- List<FlattrThing> result = new ArrayList<FlattrThing>();
+ List<FlattrThing> result = new ArrayList<>();
// load feeds
Cursor feedCursor = adapter.getFeedsInFlattrQueueCursor();
@@ -1103,56 +946,78 @@ public final class DBReader {
return result;
}
-
- /**
- * Returns true if the flattr queue is empty.
- *
- * @param context A context that is used for opening a database connection.
- */
- public static boolean getFlattrQueueEmpty(Context context) {
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- boolean empty = adapter.getFlattrQueueSize() == 0;
- adapter.close();
- return empty;
- }
-
/**
* Returns data necessary for displaying the navigation drawer. This includes
* the list of subscriptions, the number of items in the queue and the number of unread
* items.
*
- * @param context A context that is used for opening a database connection.
*/
- public static NavDrawerData getNavDrawerData(Context context) {
- PodDBAdapter adapter = new PodDBAdapter(context);
+ public static NavDrawerData getNavDrawerData() {
+ Log.d(TAG, "getNavDrawerData() called with: " + "");
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
List<Feed> feeds = getFeedList(adapter);
long[] feedIds = new long[feeds.size()];
for(int i=0; i < feeds.size(); i++) {
feedIds[i] = feeds.get(i).getId();
}
- final LongIntMap numUnreadFeedItems = adapter.getNumberOfUnreadFeedItems(feedIds);
- Collections.sort(feeds, new Comparator<Feed>() {
- @Override
- public int compare(Feed lhs, Feed rhs) {
- long numUnreadLhs = numUnreadFeedItems.get(lhs.getId());
- Log.d(TAG, "feed with id " + lhs.getId() + " has " + numUnreadLhs + " unread items");
- long numUnreadRhs = numUnreadFeedItems.get(rhs.getId());
- Log.d(TAG, "feed with id " + rhs.getId() + " has " + numUnreadRhs + " unread items");
- if(numUnreadLhs > numUnreadRhs) {
+ final LongIntMap feedCounters = adapter.getFeedCounters(feedIds);
+
+ Comparator<Feed> comparator;
+ int feedOrder = UserPreferences.getFeedOrder();
+ if(feedOrder == UserPreferences.FEED_ORDER_COUNTER) {
+ comparator = (lhs, rhs) -> {
+ long counterLhs = feedCounters.get(lhs.getId());
+ long counterRhs = feedCounters.get(rhs.getId());
+ if(counterLhs > counterRhs) {
// reverse natural order: podcast with most unplayed episodes first
return -1;
- } else if(numUnreadLhs == numUnreadRhs) {
+ } else if(counterLhs == counterRhs) {
return lhs.getTitle().compareTo(rhs.getTitle());
} else {
return 1;
}
- }
- });
+ };
+ } else if(feedOrder == UserPreferences.FEED_ORDER_ALPHABETICAL) {
+ comparator = (lhs, rhs) -> {
+ String t1 = lhs.getTitle();
+ String t2 = rhs.getTitle();
+ if(t1 == null) {
+ return 1;
+ } else if(t2 == null) {
+ return -1;
+ } else {
+ return t1.toLowerCase().compareTo(t2.toLowerCase());
+ }
+ };
+ } else {
+ comparator = (lhs, rhs) -> {
+ if(lhs.getItems() == null || lhs.getItems().size() == 0) {
+ List<FeedItem> items = DBReader.getFeedItemList(lhs);
+ lhs.setItems(items);
+ }
+ if(rhs.getItems() == null || rhs.getItems().size() == 0) {
+ List<FeedItem> items = DBReader.getFeedItemList(rhs);
+ rhs.setItems(items);
+ }
+ if(lhs.getMostRecentItem() == null) {
+ return 1;
+ } else if(rhs.getMostRecentItem() == null) {
+ return -1;
+ } else {
+ Date d1 = lhs.getMostRecentItem().getPubDate();
+ Date d2 = rhs.getMostRecentItem().getPubDate();
+ return d2.compareTo(d1);
+ }
+ };
+ }
+
+ Collections.sort(feeds, comparator);
int queueSize = adapter.getQueueSize();
int numNewItems = adapter.getNumberOfNewItems();
- NavDrawerData result = new NavDrawerData(feeds, queueSize, numNewItems, numUnreadFeedItems);
+ int numDownloadedItems = adapter.getNumberOfDownloadedEpisodes();
+
+ NavDrawerData result = new NavDrawerData(feeds, queueSize, numNewItems, numDownloadedItems, feedCounters);
adapter.close();
return result;
}
@@ -1161,14 +1026,19 @@ public final class DBReader {
public List<Feed> feeds;
public int queueSize;
public int numNewItems;
- public LongIntMap numUnreadFeedItems;
-
- public NavDrawerData(List<Feed> feeds, int queueSize, int numNewItems,
- LongIntMap numUnreadFeedItems) {
+ public int numDownloadedItems;
+ public LongIntMap feedCounters;
+
+ public NavDrawerData(List<Feed> feeds,
+ int queueSize,
+ int numNewItems,
+ int numDownloadedItems,
+ LongIntMap feedIndicatorValues) {
this.feeds = feeds;
this.queueSize = queueSize;
this.numNewItems = numNewItems;
- this.numUnreadFeedItems = numUnreadFeedItems;
+ this.numDownloadedItems = numDownloadedItems;
+ this.feedCounters = feedIndicatorValues;
}
}
}
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 e570ee709..efc60bfc2 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
@@ -6,7 +6,6 @@ import android.database.Cursor;
import android.util.Log;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
@@ -25,10 +24,9 @@ import de.danoeh.antennapod.core.asynctask.FlattrClickWorker;
import de.danoeh.antennapod.core.asynctask.FlattrStatusFetcher;
import de.danoeh.antennapod.core.feed.EventDistributor;
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.preferences.UserPreferences;
+import de.danoeh.antennapod.core.feed.FeedPreferences;
import de.danoeh.antennapod.core.service.GpodnetSyncService;
import de.danoeh.antennapod.core.service.download.DownloadStatus;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
@@ -70,7 +68,7 @@ public final class DBTasks {
* @param downloadUrl URL of the feed.
*/
public static void removeFeedWithDownloadUrl(Context context, String downloadUrl) {
- PodDBAdapter adapter = new PodDBAdapter(context);
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
Cursor cursor = adapter.getFeedCursorDownloadUrls();
long feedID = 0;
@@ -165,7 +163,7 @@ public final class DBTasks {
if (feeds != null) {
refreshFeeds(context, feeds);
} else {
- refreshFeeds(context, DBReader.getFeedList(context));
+ refreshFeeds(context, DBReader.getFeedList());
}
isRefreshing.set(false);
@@ -180,6 +178,7 @@ public final class DBTasks {
if (ClientConfig.gpodnetCallbacks.gpodnetEnabled()) {
GpodnetSyncService.sendSyncIntent(context);
}
+ Log.d(TAG, "refreshAllFeeds autodownload");
autodownloadUndownloadedItems(context);
}
}.start();
@@ -189,62 +188,29 @@ public final class DBTasks {
}
/**
- * Used by refreshExpiredFeeds to determine which feeds should be refreshed.
- * This method will use the value specified in the UserPreferences as the
- * expiration time.
- *
- * @param context Used for DB access.
- * @return A list of expired feeds. An empty list will be returned if there
- * are no expired feeds.
- */
- public static List<Feed> getExpiredFeeds(final Context context) {
- long millis = UserPreferences.getUpdateInterval();
-
- if (millis > 0) {
-
- List<Feed> feedList = DBReader.getExpiredFeedsList(context,
- millis);
- if (feedList.size() > 0) {
- refreshFeeds(context, feedList);
- }
- return feedList;
- } else {
- return new ArrayList<Feed>();
- }
- }
-
- /**
- * Refreshes expired Feeds in the list returned by the getExpiredFeedsList(Context, long) method in DBReader.
- * The expiration date parameter is determined by the update interval specified in {@link UserPreferences}.
- *
- * @param context Used for DB access.
+ * @param context
+ * @param feedList the list of feeds to refresh
*/
- public static void refreshExpiredFeeds(final Context context) {
- Log.d(TAG, "Refreshing expired feeds");
-
- new Thread() {
- public void run() {
- refreshFeeds(context, getExpiredFeeds(context));
- }
- }.start();
- }
-
private static void refreshFeeds(final Context context,
final List<Feed> feedList) {
for (Feed feed : feedList) {
- try {
- refreshFeed(context, feed);
- } catch (DownloadRequestException e) {
- e.printStackTrace();
- DBWriter.addDownloadStatus(
- context,
- new DownloadStatus(feed, feed
- .getHumanReadableIdentifier(),
- DownloadError.ERROR_REQUEST_ERROR, false, e
- .getMessage()
- )
- );
+ FeedPreferences prefs = feed.getPreferences();
+ // feeds with !getKeepUpdated can only be refreshed
+ // directly from the FeedActivity
+ if (prefs.getKeepUpdated()) {
+ try {
+ refreshFeed(context, feed);
+ } catch (DownloadRequestException e) {
+ e.printStackTrace();
+ DBWriter.addDownloadStatus(
+ new DownloadStatus(feed, feed
+ .getHumanReadableIdentifier(),
+ DownloadError.ERROR_REQUEST_ERROR, false, e
+ .getMessage()
+ )
+ );
+ }
}
}
@@ -262,7 +228,6 @@ public final class DBTasks {
} catch (DownloadRequestException e) {
e.printStackTrace();
DBWriter.addDownloadStatus(
- context,
new DownloadStatus(feed, feed
.getHumanReadableIdentifier(),
DownloadError.ERROR_REQUEST_ERROR, false, e
@@ -302,16 +267,17 @@ public final class DBTasks {
*/
public static void refreshFeed(Context context, Feed feed)
throws DownloadRequestException {
- Log.d(TAG, "id " + feed.getId());
+ Log.d(TAG, "refreshFeed(feed.id: " + feed.getId() +")");
refreshFeed(context, feed, false);
}
private static void refreshFeed(Context context, Feed feed, boolean loadAllPages) throws DownloadRequestException {
Feed f;
+ Date lastUpdate = feed.hasLastUpdateFailed() ? new Date(0) : feed.getLastUpdate();
if (feed.getPreferences() == null) {
- f = new Feed(feed.getDownload_url(), feed.getLastUpdate(), feed.getTitle());
+ f = new Feed(feed.getDownload_url(), lastUpdate, feed.getTitle());
} else {
- f = new Feed(feed.getDownload_url(), feed.getLastUpdate(), feed.getTitle(),
+ f = new Feed(feed.getDownload_url(), lastUpdate, feed.getTitle(),
feed.getPreferences().getUsername(), feed.getPreferences().getPassword());
}
f.setId(feed.getId());
@@ -319,24 +285,6 @@ public final class DBTasks {
}
/**
- * Notifies the database about a missing FeedImage file. This method will attempt to re-download the file.
- *
- * @param context Used for requesting the download.
- * @param image The FeedImage object.
- */
- public static void notifyInvalidImageFile(final Context context,
- final FeedImage image) {
- Log.i(TAG,
- "The DB was notified about an invalid image download. It will now try to re-download the image file");
- try {
- DownloadRequester.getInstance().downloadImage(context, image);
- } catch (DownloadRequestException e) {
- e.printStackTrace();
- Log.w(TAG, "Failed to download invalid feed image");
- }
- }
-
- /**
* Notifies the database about a missing FeedMedia file. This method will correct the FeedMedia object's values in the
* DB and send a FeedUpdateBroadcast.
*/
@@ -346,7 +294,7 @@ public final class DBTasks {
"The feedmanager was notified about a missing episode. It will update its database now.");
media.setDownloaded(false);
media.setFile_url(null);
- DBWriter.setFeedMedia(context, media);
+ DBWriter.setFeedMedia(media);
EventDistributor.getInstance().sendFeedUpdateBroadcast();
}
@@ -358,7 +306,7 @@ public final class DBTasks {
public static void downloadAllItemsInQueue(final Context context) {
new Thread() {
public void run() {
- List<FeedItem> queue = DBReader.getQueue(context);
+ List<FeedItem> queue = DBReader.getQueue();
if (!queue.isEmpty()) {
try {
downloadFeedItems(context,
@@ -393,9 +341,7 @@ public final class DBTasks {
@Override
public void run() {
ClientConfig.dbTasksCallbacks.getEpisodeCacheCleanupAlgorithm()
- .performCleanup(context,
- ClientConfig.dbTasksCallbacks.getEpisodeCacheCleanupAlgorithm()
- .getPerformCleanupParameter(context, Arrays.asList(items)));
+ .makeRoomForEpisodes(context, items.length);
}
}.start();
@@ -409,7 +355,7 @@ public final class DBTasks {
requester.downloadMedia(context, item.getMedia());
} catch (DownloadRequestException e) {
e.printStackTrace();
- DBWriter.addDownloadStatus(context,
+ DBWriter.addDownloadStatus(
new DownloadStatus(item.getMedia(), item
.getMedia()
.getHumanReadableIdentifier(),
@@ -433,13 +379,12 @@ public final class DBTasks {
* This method is executed on an internal single thread executor.
*
* @param context Used for accessing the DB.
- * @param mediaIds If this list is not empty, the method will only download a candidate for automatic downloading if
- * its media ID is in the mediaIds list.
* @return A Future that can be used for waiting for the methods completion.
*/
- public static Future<?> autodownloadUndownloadedItems(final Context context, final long... mediaIds) {
+ public static Future<?> autodownloadUndownloadedItems(final Context context) {
+ Log.d(TAG, "autodownloadUndownloadedItems");
return autodownloadExec.submit(ClientConfig.dbTasksCallbacks.getAutomaticDownloadAlgorithm()
- .autoDownloadUndownloadedItems(context, mediaIds));
+ .autoDownloadUndownloadedItems(context));
}
@@ -452,24 +397,21 @@ public final class DBTasks {
* @param context Used for accessing the DB.
*/
public static void performAutoCleanup(final Context context) {
- ClientConfig.dbTasksCallbacks.getEpisodeCacheCleanupAlgorithm().performCleanup(context,
- ClientConfig.dbTasksCallbacks.getEpisodeCacheCleanupAlgorithm().getDefaultCleanupParameter(context));
+ ClientConfig.dbTasksCallbacks.getEpisodeCacheCleanupAlgorithm().performCleanup(context);
}
/**
* Returns the successor of a FeedItem in the queue.
*
- * @param context Used for accessing the DB.
* @param itemId ID of the FeedItem
* @param queue Used for determining the successor of the item. If this parameter is null, the method will load
* the queue from the database in the same thread.
* @return Successor of the FeedItem or null if the FeedItem is not in the queue or has no successor.
*/
- public static FeedItem getQueueSuccessorOfItem(Context context,
- final long itemId, List<FeedItem> queue) {
+ public static FeedItem getQueueSuccessorOfItem(final long itemId, List<FeedItem> queue) {
FeedItem result = null;
if (queue == null) {
- queue = DBReader.getQueue(context);
+ queue = DBReader.getQueue();
}
if (queue != null) {
Iterator<FeedItem> iterator = queue.iterator();
@@ -494,19 +436,19 @@ public final class DBTasks {
* @param feedItemId ID of the FeedItem
*/
public static boolean isInQueue(Context context, final long feedItemId) {
- LongList queue = DBReader.getQueueIDList(context);
+ LongList queue = DBReader.getQueueIDList();
return queue.contains(feedItemId);
}
- private static Feed searchFeedByIdentifyingValueOrID(Context context, PodDBAdapter adapter,
+ private static Feed searchFeedByIdentifyingValueOrID(PodDBAdapter adapter,
Feed feed) {
if (feed.getId() != 0) {
- return DBReader.getFeed(context, feed.getId(), adapter);
+ return DBReader.getFeed(feed.getId(), adapter);
} else {
- List<Feed> feeds = DBReader.getFeedList(context);
+ List<Feed> feeds = DBReader.getFeedList();
for (Feed f : feeds) {
if (f.getIdentifyingValue().equals(feed.getIdentifyingValue())) {
- f.setItems(DBReader.getFeedItemList(context, f));
+ f.setItems(DBReader.getFeedItemList(f));
return f;
}
}
@@ -545,7 +487,7 @@ public final class DBTasks {
List<Feed> newFeedsList = new ArrayList<Feed>();
List<Feed> updatedFeedsList = new ArrayList<Feed>();
Feed[] resultFeeds = new Feed[newFeeds.length];
- PodDBAdapter adapter = new PodDBAdapter(context);
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
for (int feedIdx = 0; feedIdx < newFeeds.length; feedIdx++) {
@@ -553,7 +495,7 @@ public final class DBTasks {
final Feed newFeed = newFeeds[feedIdx];
// Look up feed in the feedslist
- final Feed savedFeed = searchFeedByIdentifyingValueOrID(context, adapter,
+ final Feed savedFeed = searchFeedByIdentifyingValueOrID(adapter,
newFeed);
if (savedFeed == null) {
Log.d(TAG, "Found no existing Feed with title "
@@ -563,7 +505,7 @@ public final class DBTasks {
// all new feeds will have the most recent item marked as unplayed
FeedItem mostRecent = newFeed.getMostRecentItem();
if (mostRecent != null) {
- mostRecent.setRead(false);
+ mostRecent.setNew();
}
newFeedsList.add(newFeed);
@@ -574,22 +516,27 @@ public final class DBTasks {
Collections.sort(newFeed.getItems(), new FeedItemPubdateComparator());
- final boolean markNewItemsAsUnread;
if (newFeed.getPageNr() == savedFeed.getPageNr()) {
if (savedFeed.compareWithOther(newFeed)) {
Log.d(TAG, "Feed has updated attribute values. Updating old feed's attributes");
savedFeed.updateFromOther(newFeed);
}
- markNewItemsAsUnread = true;
} else {
- Log.d(TAG, "New feed has a higher page number. Merging without marking as unread");
- markNewItemsAsUnread = false;
+ Log.d(TAG, "New feed has a higher page number.");
savedFeed.setNextPageLink(newFeed.getNextPageLink());
}
if (savedFeed.getPreferences().compareWithOther(newFeed.getPreferences())) {
Log.d(TAG, "Feed has updated preferences. Updating old feed's preferences");
savedFeed.getPreferences().updateFromOther(newFeed.getPreferences());
}
+
+ // get the most recent date now, before we start changing the list
+ FeedItem priorMostRecent = savedFeed.getMostRecentItem();
+ Date priorMostRecentDate = null;
+ if (priorMostRecent != null) {
+ priorMostRecentDate = priorMostRecent.getPubDate();
+ }
+
// Look for new or updated Items
for (int idx = 0; idx < newFeed.getItems().size(); idx++) {
final FeedItem item = newFeed.getItems().get(idx);
@@ -597,12 +544,19 @@ public final class DBTasks {
item.getIdentifyingValue());
if (oldItem == null) {
// item is new
- final int i = idx;
item.setFeed(savedFeed);
item.setAutoDownload(savedFeed.getPreferences().getAutoDownload());
- savedFeed.getItems().add(i, item);
- if (markNewItemsAsUnread) {
- item.setRead(false);
+ savedFeed.getItems().add(idx, item);
+
+ // only mark the item new if it actually occurs
+ // before the most recent item (before we started adding things)
+ // (if the most recent date is null then we can assume there are no items
+ // and this is the first, hence 'new')
+ if (priorMostRecentDate == null ||
+ priorMostRecentDate.before(item.getPubDate())) {
+ Log.d(TAG, "Marking item published on " + item.getPubDate() +
+ " new, prior most recent date = " + priorMostRecentDate);
+ item.setNew();
}
} else {
oldItem.updateFromOther(item);
@@ -622,7 +576,7 @@ public final class DBTasks {
try {
DBWriter.addNewFeed(context, newFeedsList.toArray(new Feed[newFeedsList.size()])).get();
- DBWriter.setCompleteFeed(context, updatedFeedsList.toArray(new Feed[updatedFeedsList.size()])).get();
+ DBWriter.setCompleteFeed(updatedFeedsList.toArray(new Feed[updatedFeedsList.size()])).get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
@@ -650,8 +604,8 @@ public final class DBTasks {
public void execute(PodDBAdapter adapter) {
Cursor searchResult = adapter.searchItemTitles(feedID,
query);
- List<FeedItem> items = DBReader.extractItemlistFromCursor(context, searchResult);
- DBReader.loadFeedDataOfFeedItemlist(context, items);
+ List<FeedItem> items = DBReader.extractItemlistFromCursor(searchResult);
+ DBReader.loadAdditionalFeedItemListData(items);
setResult(items);
searchResult.close();
}
@@ -674,8 +628,8 @@ public final class DBTasks {
public void execute(PodDBAdapter adapter) {
Cursor searchResult = adapter.searchItemDescriptions(feedID,
query);
- List<FeedItem> items = DBReader.extractItemlistFromCursor(context, searchResult);
- DBReader.loadFeedDataOfFeedItemlist(context, items);
+ List<FeedItem> items = DBReader.extractItemlistFromCursor(searchResult);
+ DBReader.loadAdditionalFeedItemListData(items);
setResult(items);
searchResult.close();
}
@@ -698,8 +652,8 @@ public final class DBTasks {
public void execute(PodDBAdapter adapter) {
Cursor searchResult = adapter.searchItemContentEncoded(feedID,
query);
- List<FeedItem> items = DBReader.extractItemlistFromCursor(context, searchResult);
- DBReader.loadFeedDataOfFeedItemlist(context, items);
+ List<FeedItem> items = DBReader.extractItemlistFromCursor(searchResult);
+ DBReader.loadAdditionalFeedItemListData(items);
setResult(items);
searchResult.close();
}
@@ -721,8 +675,8 @@ public final class DBTasks {
public void execute(PodDBAdapter adapter) {
Cursor searchResult = adapter.searchItemChapters(feedID,
query);
- List<FeedItem> items = DBReader.extractItemlistFromCursor(context, searchResult);
- DBReader.loadFeedDataOfFeedItemlist(context, items);
+ List<FeedItem> items = DBReader.extractItemlistFromCursor(searchResult);
+ DBReader.loadAdditionalFeedItemListData(items);
setResult(items);
searchResult.close();
}
@@ -745,7 +699,7 @@ public final class DBTasks {
@Override
public T call() throws Exception {
- PodDBAdapter adapter = new PodDBAdapter(context);
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
execute(adapter);
adapter.close();
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 fe5d0dfd3..e728abc3b 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
@@ -13,20 +13,22 @@ import org.shredzone.flattr4j.model.Flattr;
import java.io.File;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
-import java.util.LinkedList;
import java.util.List;
-import java.util.Map;
+import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
-import java.util.concurrent.ThreadFactory;
-import de.danoeh.antennapod.core.BuildConfig;
import de.danoeh.antennapod.core.ClientConfig;
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.QueueEvent;
import de.danoeh.antennapod.core.feed.EventDistributor;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.FeedEvent;
@@ -34,7 +36,6 @@ import de.danoeh.antennapod.core.feed.FeedImage;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.feed.FeedPreferences;
-import de.danoeh.antennapod.core.feed.QueueEvent;
import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction;
import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
@@ -56,19 +57,16 @@ import de.greenrobot.event.EventBus;
* This class will use the {@link EventDistributor} to notify listeners about changes in the database.
*/
public class DBWriter {
+
private static final String TAG = "DBWriter";
private static final ExecutorService dbExec;
static {
- dbExec = Executors.newSingleThreadExecutor(new ThreadFactory() {
-
- @Override
- public Thread newThread(Runnable r) {
- Thread t = new Thread(r);
- t.setPriority(Thread.MIN_PRIORITY);
- return t;
- }
+ dbExec = Executors.newSingleThreadExecutor(r -> {
+ Thread t = new Thread(r);
+ t.setPriority(Thread.MIN_PRIORITY);
+ return t;
});
}
@@ -83,62 +81,59 @@ public class DBWriter {
*/
public static Future<?> deleteFeedMediaOfItem(final Context context,
final long mediaId) {
- return dbExec.submit(new Runnable() {
- @Override
- public void run() {
-
- final FeedMedia media = DBReader.getFeedMedia(context, mediaId);
- if (media != null) {
- Log.i(TAG, String.format("Requested to delete FeedMedia [id=%d, title=%s, downloaded=%s",
- media.getId(), media.getEpisodeTitle(), String.valueOf(media.isDownloaded())));
- boolean result = false;
- if (media.isDownloaded()) {
- // delete downloaded media file
- File mediaFile = new File(media.getFile_url());
- if (mediaFile.exists()) {
- result = mediaFile.delete();
- }
- media.setDownloaded(false);
- media.setFile_url(null);
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- adapter.setMedia(media);
- adapter.close();
-
- // If media is currently being played, change playback
- // type to 'stream' and shutdown playback service
- SharedPreferences prefs = PreferenceManager
- .getDefaultSharedPreferences(context);
- if (PlaybackPreferences.getCurrentlyPlayingMedia() == FeedMedia.PLAYABLE_TYPE_FEEDMEDIA) {
- if (media.getId() == PlaybackPreferences
- .getCurrentlyPlayingFeedMediaId()) {
- SharedPreferences.Editor editor = prefs.edit();
- editor.putBoolean(
- PlaybackPreferences.PREF_CURRENT_EPISODE_IS_STREAM,
- true);
- editor.commit();
- }
- if (PlaybackPreferences
- .getCurrentlyPlayingFeedMediaId() == media
- .getId()) {
- context.sendBroadcast(new Intent(
- PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE));
- }
+ return dbExec.submit(() -> {
+ final FeedMedia media = DBReader.getFeedMedia(mediaId);
+ if (media != null) {
+ Log.i(TAG, String.format("Requested to delete FeedMedia [id=%d, title=%s, downloaded=%s",
+ media.getId(), media.getEpisodeTitle(), String.valueOf(media.isDownloaded())));
+ boolean result = false;
+ if (media.isDownloaded()) {
+ // delete downloaded media file
+ File mediaFile = new File(media.getFile_url());
+ if (mediaFile.exists()) {
+ result = mediaFile.delete();
+ }
+ media.setDownloaded(false);
+ media.setFile_url(null);
+ media.setHasEmbeddedPicture(false);
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.setMedia(media);
+ adapter.close();
+
+ // If media is currently being played, change playback
+ // type to 'stream' and shutdown playback service
+ SharedPreferences prefs = PreferenceManager
+ .getDefaultSharedPreferences(context);
+ if (PlaybackPreferences.getCurrentlyPlayingMedia() == FeedMedia.PLAYABLE_TYPE_FEEDMEDIA) {
+ if (media.getId() == PlaybackPreferences
+ .getCurrentlyPlayingFeedMediaId()) {
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putBoolean(
+ PlaybackPreferences.PREF_CURRENT_EPISODE_IS_STREAM,
+ true);
+ editor.commit();
}
- // Gpodder: queue delete action for synchronization
- if(GpodnetPreferences.loggedIn()) {
- FeedItem item = media.getItem();
- GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, GpodnetEpisodeAction.Action.DELETE)
- .currentDeviceId()
- .currentTimestamp()
- .build();
- GpodnetPreferences.enqueueEpisodeAction(action);
+ if (PlaybackPreferences
+ .getCurrentlyPlayingFeedMediaId() == media
+ .getId()) {
+ context.sendBroadcast(new Intent(
+ PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE));
}
}
- Log.d(TAG, "Deleting File. Result: " + result);
- EventBus.getDefault().post(new QueueEvent(QueueEvent.Action.DELETED_MEDIA, media.getItem()));
- EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast();
+ // Gpodder: queue delete action for synchronization
+ if(GpodnetPreferences.loggedIn()) {
+ FeedItem item = media.getItem();
+ GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, GpodnetEpisodeAction.Action.DELETE)
+ .currentDeviceId()
+ .currentTimestamp()
+ .build();
+ GpodnetPreferences.enqueueEpisodeAction(action);
+ }
}
+ Log.d(TAG, "Deleting File. Result: " + result);
+ EventBus.getDefault().post(FeedItemEvent.deletedMedia(Arrays.asList(media.getItem())));
+ EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast();
}
});
}
@@ -150,83 +145,91 @@ public class DBWriter {
* @param feedId ID of the Feed that should be deleted.
*/
public static Future<?> deleteFeed(final Context context, final long feedId) {
- return dbExec.submit(new Runnable() {
- @Override
- public void run() {
- DownloadRequester requester = DownloadRequester.getInstance();
- SharedPreferences prefs = PreferenceManager
- .getDefaultSharedPreferences(context
- .getApplicationContext());
- final Feed feed = DBReader.getFeed(context, feedId);
- if (feed != null) {
- if (PlaybackPreferences.getCurrentlyPlayingMedia() == FeedMedia.PLAYABLE_TYPE_FEEDMEDIA
- && PlaybackPreferences.getLastPlayedFeedId() == feed
- .getId()) {
- context.sendBroadcast(new Intent(
- PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE));
- SharedPreferences.Editor editor = prefs.edit();
- editor.putLong(
- PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID,
- -1);
- editor.commit();
+ return dbExec.submit(() -> {
+ DownloadRequester requester = DownloadRequester.getInstance();
+ SharedPreferences prefs = PreferenceManager
+ .getDefaultSharedPreferences(context
+ .getApplicationContext());
+ final Feed feed = DBReader.getFeed(feedId);
+
+ if (feed != null) {
+ if (PlaybackPreferences.getCurrentlyPlayingMedia() == FeedMedia.PLAYABLE_TYPE_FEEDMEDIA
+ && PlaybackPreferences.getLastPlayedFeedId() == feed
+ .getId()) {
+ context.sendBroadcast(new Intent(
+ PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE));
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putLong(
+ PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID,
+ -1);
+ 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<>();
+ if (feed.getItems() == null) {
+ DBReader.getFeedItemList(feed);
+ }
- // 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());
- }
+ for (FeedItem item : feed.getItems()) {
+ if(queue.remove(item)) {
+ removed.add(item);
}
- // delete stored media files and mark them as read
- List<FeedItem> queue = DBReader.getQueue(context);
- boolean queueWasModified = false;
- if (feed.getItems() == null) {
- DBReader.getFeedItemList(context, feed);
+ if (item.getMedia() != null
+ && item.getMedia().isDownloaded()) {
+ File mediaFile = new File(item.getMedia()
+ .getFile_url());
+ mediaFile.delete();
+ } else if (item.getMedia() != null
+ && requester.isDownloadingFile(item.getMedia())) {
+ requester.cancelDownload(context, item.getMedia());
}
- for (FeedItem item : feed.getItems()) {
- queueWasModified |= queue.remove(item);
- if (item.getMedia() != null
- && item.getMedia().isDownloaded()) {
- File mediaFile = new File(item.getMedia()
- .getFile_url());
- mediaFile.delete();
- } else if (item.getMedia() != null
- && 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());
}
-
- 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 = new PodDBAdapter(context);
- adapter.open();
- if (queueWasModified) {
- adapter.setQueue(queue);
}
- adapter.removeFeed(feed);
- adapter.close();
-
- if (ClientConfig.gpodnetCallbacks.gpodnetEnabled()) {
- GpodnetPreferences.addRemovedFeed(feed.getDownload_url());
+ }
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ if (removed.size() > 0) {
+ adapter.setQueue(queue);
+ for(FeedItem item : removed) {
+ EventBus.getDefault().post(QueueEvent.irreversibleRemoved(item));
}
- EventDistributor.getInstance().sendFeedUpdateBroadcast();
+ }
+ adapter.removeFeed(feed);
+ adapter.close();
- BackupManager backupManager = new BackupManager(context);
- backupManager.dataChanged();
+ if (ClientConfig.gpodnetCallbacks.gpodnetEnabled()) {
+ GpodnetPreferences.addRemovedFeed(feed.getDownload_url());
}
+ EventDistributor.getInstance().sendFeedUpdateBroadcast();
+
+ // we assume we also removed download log entries for the feed or its media files.
+ // especially important if download or refresh failed, as the user should not be able
+ // to retry these
+ EventDistributor.getInstance().sendDownloadLogUpdateBroadcast();
+
+ BackupManager backupManager = new BackupManager(context);
+ backupManager.dataChanged();
}
});
}
@@ -234,39 +237,27 @@ public class DBWriter {
/**
* Deletes the entire playback history.
*
- * @param context A context that is used for opening a database connection.
*/
- public static Future<?> clearPlaybackHistory(final Context context) {
- return dbExec.submit(new Runnable() {
-
- @Override
- public void run() {
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- adapter.clearPlaybackHistory();
- adapter.close();
- EventDistributor.getInstance()
- .sendPlaybackHistoryUpdateBroadcast();
- }
+ public static Future<?> clearPlaybackHistory() {
+ return dbExec.submit(() -> {
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.clearPlaybackHistory();
+ adapter.close();
+ EventDistributor.getInstance().sendPlaybackHistoryUpdateBroadcast();
});
}
/**
* Deletes the entire download log.
- *
- * @param context A context that is used for opening a database connection.
*/
- public static Future<?> clearDownloadLog(final Context context) {
- return dbExec.submit(new Runnable() {
- @Override
- public void run() {
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- adapter.clearDownloadLog();
- adapter.close();
- EventDistributor.getInstance()
- .sendDownloadLogUpdateBroadcast();
- }
+ public static Future<?> clearDownloadLog() {
+ return dbExec.submit(() -> {
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.clearDownloadLog();
+ adapter.close();
+ EventDistributor.getInstance().sendDownloadLogUpdateBroadcast();
});
}
@@ -276,58 +267,36 @@ public class DBWriter {
* its playback completion date is set to a non-null value. This method will set the playback completion date to the
* current date regardless of the current value.
*
- * @param context A context that is used for opening a database connection.
* @param media FeedMedia that should be added to the playback history.
*/
- public static Future<?> addItemToPlaybackHistory(final Context context,
- final FeedMedia media) {
- return dbExec.submit(new Runnable() {
- @Override
- public void run() {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Adding new item to playback history");
- media.setPlaybackCompletionDate(new Date());
- // reset played_duration to 0 so that it behaves correctly when the episode is played again
- media.setPlayedDuration(0);
-
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- adapter.setFeedMediaPlaybackCompletionDate(media);
- adapter.close();
- EventDistributor.getInstance().sendPlaybackHistoryUpdateBroadcast();
+ public static Future<?> addItemToPlaybackHistory(final FeedMedia media) {
+ return dbExec.submit(() -> {
+ Log.d(TAG, "Adding new item to playback history");
+ media.setPlaybackCompletionDate(new Date());
+ // reset played_duration to 0 so that it behaves correctly when the episode is played again
+ media.setPlayedDuration(0);
+
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.setFeedMediaPlaybackCompletionDate(media);
+ adapter.close();
+ EventDistributor.getInstance().sendPlaybackHistoryUpdateBroadcast();
- }
});
}
- private static void cleanupDownloadLog(final PodDBAdapter adapter) {
- final long logSize = adapter.getDownloadLogSize();
- if (logSize > DBReader.DOWNLOAD_LOG_SIZE) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Cleaning up download log");
- adapter.removeDownloadLogItems(logSize - DBReader.DOWNLOAD_LOG_SIZE);
- }
- }
-
/**
* Adds a Download status object to the download log.
*
- * @param context A context that is used for opening a database connection.
* @param status The DownloadStatus object.
*/
- public static Future<?> addDownloadStatus(final Context context,
- final DownloadStatus status) {
- return dbExec.submit(new Runnable() {
-
- @Override
- public void run() {
-
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- adapter.setDownloadStatus(status);
- adapter.close();
- EventDistributor.getInstance().sendDownloadLogUpdateBroadcast();
- }
+ public static Future<?> addDownloadStatus(final DownloadStatus status) {
+ return dbExec.submit(() -> {
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.setDownloadStatus(status);
+ adapter.close();
+ EventDistributor.getInstance().sendDownloadLogUpdateBroadcast();
});
}
@@ -344,108 +313,118 @@ public class DBWriter {
*/
public static Future<?> addQueueItemAt(final Context context, final long itemId,
final int index, final boolean performAutoDownload) {
- return dbExec.submit(new Runnable() {
-
- @Override
- public void run() {
- final PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- final List<FeedItem> queue = DBReader.getQueue(context, adapter);
- FeedItem item = null;
-
- if (queue != null) {
- if (!itemListContains(queue, itemId)) {
- item = DBReader.getFeedItem(context, itemId);
- if (item != null) {
- queue.add(index, item);
- adapter.setQueue(queue);
- EventBus.getDefault().post(new QueueEvent(QueueEvent.Action.ADDED, item, index));
+ return dbExec.submit(() -> {
+ final PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ final List<FeedItem> queue = DBReader.getQueue(adapter);
+ FeedItem item;
+
+ if (queue != null) {
+ if (!itemListContains(queue, itemId)) {
+ item = DBReader.getFeedItem(itemId);
+ if (item != null) {
+ queue.add(index, item);
+ adapter.setQueue(queue);
+ item.addTag(FeedItem.TAG_QUEUE);
+ EventBus.getDefault().post(QueueEvent.added(item, index));
+ if (item.isNew()) {
+ DBWriter.markItemPlayed(FeedItem.UNPLAYED, item.getId());
}
}
}
+ }
- adapter.close();
- if (performAutoDownload) {
- DBTasks.autodownloadUndownloadedItems(context);
- }
-
+ adapter.close();
+ if (performAutoDownload) {
+ DBTasks.autodownloadUndownloadedItems(context);
}
+
});
}
+ public static Future<?> addQueueItem(final Context context,
+ final FeedItem... items) {
+ LongList itemIds = new LongList(items.length);
+ for (FeedItem item : items) {
+ itemIds.add(item.getId());
+ item.addTag(FeedItem.TAG_QUEUE);
+ }
+ return addQueueItem(context, false, itemIds.toArray());
+ }
+
/**
* Appends FeedItem objects to the end of the queue. The 'read'-attribute of all items will be set to true.
* If a FeedItem is already in the queue, the FeedItem will not change its position in the queue.
*
* @param context A context that is used for opening a database connection.
+ * @param performAutoDownload true if an auto-download process should be started after the operation.
* @param itemIds IDs of the FeedItem objects that should be added to the queue.
*/
- public static Future<?> addQueueItem(final Context context,
+ public static Future<?> addQueueItem(final Context context, final boolean performAutoDownload,
final long... itemIds) {
- return dbExec.submit(new Runnable() {
+ return dbExec.submit(() -> {
+ if (itemIds.length > 0) {
+ final PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ final List<FeedItem> queue = DBReader.getQueue(adapter);
- @Override
- public void run() {
- if (itemIds.length > 0) {
- final PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- final List<FeedItem> queue = DBReader.getQueue(context,
- adapter);
-
- if (queue != null) {
- boolean queueModified = false;
- boolean unreadItemsModified = false;
- List<FeedItem> itemsToSave = new LinkedList<FeedItem>();
- for (int i = 0; i < itemIds.length; i++) {
- if (!itemListContains(queue, itemIds[i])) {
- final FeedItem item = DBReader.getFeedItem(
- context, itemIds[i]);
-
- if (item != null) {
- // add item to either front ot back of queue
- boolean addToFront = UserPreferences.enqueueAtFront();
-
- if(addToFront){
- queue.add(0, item);
- } else {
- queue.add(item);
- }
-
- queueModified = true;
+ if (queue != null) {
+ boolean queueModified = false;
+ LongList markAsUnplayedIds = new LongList();
+ List<QueueEvent> events = new ArrayList<QueueEvent>();
+ for (int i = 0; i < itemIds.length; i++) {
+ if (!itemListContains(queue, itemIds[i])) {
+ final FeedItem item = DBReader.getFeedItem(itemIds[i]);
+
+
+ if (item != null) {
+ // 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));
+ } else {
+ queue.add(item);
+ events.add(QueueEvent.added(item, queue.size() - 1));
+ }
+ queueModified = true;
+ if (item.isNew()) {
+ markAsUnplayedIds.add(item.getId());
}
}
}
- if (queueModified) {
- adapter.setQueue(queue);
- EventBus.getDefault().post(new QueueEvent(QueueEvent.Action.ADDED_ITEMS, queue));
+ }
+ if (queueModified) {
+ adapter.setQueue(queue);
+ for (QueueEvent event : events) {
+ EventBus.getDefault().post(event);
+ }
+ if (markAsUnplayedIds.size() > 0) {
+ DBWriter.markItemPlayed(FeedItem.UNPLAYED, markAsUnplayedIds.toArray());
}
}
- adapter.close();
+ }
+ adapter.close();
+ if (performAutoDownload) {
DBTasks.autodownloadUndownloadedItems(context);
}
}
});
-
}
/**
* Removes all FeedItem objects from the queue.
*
- * @param context A context that is used for opening a database connection.
*/
- public static Future<?> clearQueue(final Context context) {
- return dbExec.submit(new Runnable() {
-
- @Override
- public void run() {
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- adapter.clearQueue();
- adapter.close();
-
- EventBus.getDefault().post(new QueueEvent(QueueEvent.Action.CLEARED));
- }
+ public static Future<?> clearQueue() {
+ return dbExec.submit(() -> {
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.clearQueue();
+ adapter.close();
+
+ EventBus.getDefault().post(QueueEvent.cleared());
});
}
@@ -458,79 +437,99 @@ public class DBWriter {
*/
public static Future<?> removeQueueItem(final Context context,
final FeedItem item, final boolean performAutoDownload) {
- return dbExec.submit(new Runnable() {
-
- @Override
- public void run() {
- final PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- final List<FeedItem> queue = DBReader.getQueue(context, adapter);
-
- if (queue != null) {
- int position = queue.indexOf(item);
- if(position >= 0) {
- queue.remove(position);
- adapter.setQueue(queue);
- EventBus.getDefault().post(new QueueEvent(QueueEvent.Action.REMOVED, item, position));
- } else {
- Log.w(TAG, "Queue was not modified by call to removeQueueItem");
- }
+ return dbExec.submit(() -> {
+ final PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ final List<FeedItem> queue = DBReader.getQueue(adapter);
+
+ if (queue != null) {
+ int position = queue.indexOf(item);
+ if (position >= 0) {
+ queue.remove(position);
+ adapter.setQueue(queue);
+ item.removeTag(FeedItem.TAG_QUEUE);
+ EventBus.getDefault().post(QueueEvent.removed(item));
} else {
- Log.e(TAG, "removeQueueItem: Could not load queue");
- }
- adapter.close();
- if (performAutoDownload) {
- DBTasks.autodownloadUndownloadedItems(context);
+ Log.w(TAG, "Queue was not modified by call to removeQueueItem");
}
+ } else {
+ Log.e(TAG, "removeQueueItem: Could not load queue");
+ }
+ adapter.close();
+ if (performAutoDownload) {
+ DBTasks.autodownloadUndownloadedItems(context);
+ }
+ });
+
+ }
+
+ public static Future<?> addFavoriteItem(final FeedItem item) {
+ return dbExec.submit(() -> {
+ final PodDBAdapter adapter = PodDBAdapter.getInstance().open();
+ adapter.addFavoriteItem(item);
+ adapter.close();
+ item.addTag(FeedItem.TAG_FAVORITE);
+ EventBus.getDefault().post(FavoritesEvent.added(item));
+ });
+ }
+
+ 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));
});
+ }
+ public static Future<?> removeFavoriteItem(final FeedItem item) {
+ return dbExec.submit(() -> {
+ final PodDBAdapter adapter = PodDBAdapter.getInstance().open();
+ adapter.removeFavoriteItem(item);
+ adapter.close();
+ item.removeTag(FeedItem.TAG_FAVORITE);
+ EventBus.getDefault().post(FavoritesEvent.removed(item));
+ });
}
/**
* Moves the specified item to the top of the queue.
- *
- * @param context A context that is used for opening a database connection.
- * @param itemId The item to move to the top of the queue
+ * @param itemId The item to move to the top of the queue
* @param broadcastUpdate true if this operation should trigger a QueueUpdateBroadcast. This option should be set to
- * false if the caller wants to avoid unexpected updates of the GUI.
*/
- public static Future<?> moveQueueItemToTop(final Context context, final long itemId, final boolean broadcastUpdate) {
- return dbExec.submit(new Runnable() {
- @Override
- public void run() {
- LongList queueIdList = DBReader.getQueueIDList(context);
- int index = queueIdList.indexOf(itemId);
- if (index >=0) {
- moveQueueItemHelper(context, index, 0, broadcastUpdate);
- } else {
- Log.e(TAG, "moveQueueItemToTop: item not found");
- }
+ public static Future<?> moveQueueItemToTop(final long itemId, final boolean broadcastUpdate) {
+ return dbExec.submit(() -> {
+ LongList queueIdList = DBReader.getQueueIDList();
+ int index = queueIdList.indexOf(itemId);
+ if (index >=0) {
+ moveQueueItemHelper(index, 0, broadcastUpdate);
+ } else {
+ Log.e(TAG, "moveQueueItemToTop: item not found");
}
});
}
/**
* Moves the specified item to the bottom of the queue.
- *
- * @param context A context that is used for opening a database connection.
- * @param itemId The item to move to the bottom of the queue
+ * @param itemId The item to move to the bottom of the queue
* @param broadcastUpdate true if this operation should trigger a QueueUpdateBroadcast. This option should be set to
- * false if the caller wants to avoid unexpected updates of the GUI.
*/
- public static Future<?> moveQueueItemToBottom(final Context context, final long itemId,
+ public static Future<?> moveQueueItemToBottom(final long itemId,
final boolean broadcastUpdate) {
- return dbExec.submit(new Runnable() {
- @Override
- public void run() {
- LongList queueIdList = DBReader.getQueueIDList(context);
- int index = queueIdList.indexOf(itemId);
- if(index >= 0) {
- moveQueueItemHelper(context, index, queueIdList.size() - 1,
- broadcastUpdate);
- } else {
- Log.e(TAG, "moveQueueItemToBottom: item not found");
- }
+ return dbExec.submit(() -> {
+ LongList queueIdList = DBReader.getQueueIDList();
+ int index = queueIdList.indexOf(itemId);
+ if (index >= 0) {
+ moveQueueItemHelper(index, queueIdList.size() - 1,
+ broadcastUpdate);
+ } else {
+ Log.e(TAG, "moveQueueItemToBottom: item not found");
}
});
}
@@ -538,21 +537,16 @@ public class DBWriter {
/**
* Changes the position of a FeedItem in the queue.
*
- * @param context A context that is used for opening a database connection.
* @param from Source index. Must be in range 0..queue.size()-1.
* @param to Destination index. Must be in range 0..queue.size()-1.
* @param broadcastUpdate true if this operation should trigger a QueueUpdateBroadcast. This option should be set to
* false if the caller wants to avoid unexpected updates of the GUI.
* @throws IndexOutOfBoundsException if (to < 0 || to >= queue.size()) || (from < 0 || from >= queue.size())
*/
- public static Future<?> moveQueueItem(final Context context, final int from,
+ public static Future<?> moveQueueItem(final int from,
final int to, final boolean broadcastUpdate) {
- return dbExec.submit(new Runnable() {
-
- @Override
- public void run() {
- moveQueueItemHelper(context, from, to, broadcastUpdate);
- }
+ return dbExec.submit(() -> {
+ moveQueueItemHelper(from, to, broadcastUpdate);
});
}
@@ -561,32 +555,27 @@ public class DBWriter {
* <p/>
* This function must be run using the ExecutorService (dbExec).
*
- * @param context A context that is used for opening a database connection.
* @param from Source index. Must be in range 0..queue.size()-1.
* @param to Destination index. Must be in range 0..queue.size()-1.
* @param broadcastUpdate true if this operation should trigger a QueueUpdateBroadcast. This option should be set to
* false if the caller wants to avoid unexpected updates of the GUI.
* @throws IndexOutOfBoundsException if (to < 0 || to >= queue.size()) || (from < 0 || from >= queue.size())
*/
- private static void moveQueueItemHelper(final Context context, final int from,
+ private static void moveQueueItemHelper(final int from,
final int to, final boolean broadcastUpdate) {
- final PodDBAdapter adapter = new PodDBAdapter(context);
+ final PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
- final List<FeedItem> queue = DBReader
- .getQueue(context, adapter);
+ final List<FeedItem> queue = DBReader.getQueue(adapter);
if (queue != null) {
- if (from >= 0 && from < queue.size() && to >= 0
- && to < queue.size()) {
-
+ if (from >= 0 && from < queue.size() && to >= 0 && to < queue.size()) {
final FeedItem item = queue.remove(from);
queue.add(to, item);
adapter.setQueue(queue);
if (broadcastUpdate) {
- EventBus.getDefault().post(new QueueEvent(QueueEvent.Action.MOVED, item, to));
+ EventBus.getDefault().post(QueueEvent.moved(item, to));
}
-
}
} else {
Log.e(TAG, "moveQueueItemHelper: Could not load queue");
@@ -594,183 +583,178 @@ public class DBWriter {
adapter.close();
}
- /**
- * Sets the 'read'-attribute of a FeedItem to the specified value.
+ /*
+ * Sets the 'read'-attribute of all specified FeedItems
*
* @param context A context that is used for opening a database connection.
- * @param itemId ID of the FeedItem
- * @param read New value of the 'read'-attribute
+ * @param played New value of the 'read'-attribute, one of FeedItem.PLAYED, FeedItem.NEW,
+ * FeedItem.UNPLAYED
+ * @param itemIds IDs of the FeedItems.
*/
- public static Future<?> markItemRead(final Context context, final long itemId,
- final boolean read) {
- return markItemRead(context, itemId, read, 0, false);
+ public static Future<?> markItemPlayed(final int played, final long... itemIds) {
+ return dbExec.submit(() -> {
+ final PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.setFeedItemRead(played, itemIds);
+ adapter.close();
+ EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast();
+ });
}
/**
* Sets the 'read'-attribute of a FeedItem to the specified value.
- *
- * @param context A context that is used for opening a database connection.
- * @param item The FeedItem object
- * @param read New value of the 'read'-attribute
+ * @param item The FeedItem object
+ * @param played New value of the 'read'-attribute one of FeedItem.PLAYED,
+ * FeedItem.NEW, FeedItem.UNPLAYED
* @param resetMediaPosition true if this method should also reset the position of the FeedItem's FeedMedia object.
- * If the FeedItem has no FeedMedia object, this parameter will be ignored.
*/
- public static Future<?> markItemRead(Context context, FeedItem item, boolean read, boolean resetMediaPosition) {
+ public static Future<?> markItemPlayed(FeedItem item, int played, boolean resetMediaPosition) {
long mediaId = (item.hasMedia()) ? item.getMedia().getId() : 0;
- return markItemRead(context, item.getId(), read, mediaId, resetMediaPosition);
+ return markItemPlayed(item.getId(), played, mediaId, resetMediaPosition);
}
- private static Future<?> markItemRead(final Context context, final long itemId,
- final boolean read, final long mediaId,
- final boolean resetMediaPosition) {
- return dbExec.submit(new Runnable() {
-
- @Override
- public void run() {
- final PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- adapter.setFeedItemRead(read, itemId, mediaId,
- resetMediaPosition);
- adapter.close();
-
- EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast();
- }
+ private static Future<?> markItemPlayed(final long itemId,
+ final int played,
+ final long mediaId,
+ final boolean resetMediaPosition) {
+ return dbExec.submit(() -> {
+ final PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.setFeedItemRead(played, itemId, mediaId,
+ resetMediaPosition);
+ adapter.close();
+
+ EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast();
});
}
/**
* Sets the 'read'-attribute of all FeedItems of a specific Feed to true.
*
- * @param context A context that is used for opening a database connection.
* @param feedId ID of the Feed.
*/
- public static Future<?> markFeedRead(final Context context, final long feedId) {
- return dbExec.submit(new Runnable() {
-
- @Override
- public void run() {
- final PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- Cursor itemCursor = adapter.getAllItemsOfFeedCursor(feedId);
- long[] itemIds = new long[itemCursor.getCount()];
- itemCursor.moveToFirst();
- for (int i = 0; i < itemIds.length; i++) {
- itemIds[i] = itemCursor.getLong(PodDBAdapter.KEY_ID_INDEX);
- itemCursor.moveToNext();
- }
- itemCursor.close();
- adapter.setFeedItemRead(true, itemIds);
- adapter.close();
-
- EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast();
+ public static Future<?> markFeedSeen(final long feedId) {
+ return dbExec.submit(() -> {
+ final PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ Cursor itemCursor = adapter.getNewItemsIdsCursor(feedId);
+ long[] ids = new long[itemCursor.getCount()];
+ itemCursor.moveToFirst();
+ for (int i = 0; i < ids.length; i++) {
+ ids[i] = itemCursor.getLong(0);
+ itemCursor.moveToNext();
}
- });
+ itemCursor.close();
+ adapter.setFeedItemRead(FeedItem.UNPLAYED, ids);
+ adapter.close();
+ EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast();
+ });
}
/**
- * Sets the 'read'-attribute of all FeedItems to true.
+ * Sets the 'read'-attribute of all FeedItems of a specific Feed to true.
*
- * @param context A context that is used for opening a database connection.
+ * @param feedId ID of the Feed.
*/
- public static Future<?> markAllItemsRead(final Context context) {
- return dbExec.submit(new Runnable() {
+ public static Future<?> markFeedRead(final long feedId) {
+ return dbExec.submit(() -> {
+ final PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ Cursor itemCursor = adapter.getAllItemsOfFeedCursor(feedId);
+ long[] itemIds = new long[itemCursor.getCount()];
+ itemCursor.moveToFirst();
+ for (int i = 0; i < itemIds.length; i++) {
+ int indexId = itemCursor.getColumnIndex(PodDBAdapter.KEY_ID);
+ itemIds[i] = itemCursor.getLong(indexId);
+ itemCursor.moveToNext();
+ }
+ itemCursor.close();
+ adapter.setFeedItemRead(FeedItem.PLAYED, itemIds);
+ adapter.close();
- @Override
- public void run() {
- final PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- Cursor itemCursor = adapter.getUnreadItemsCursor();
- long[] itemIds = new long[itemCursor.getCount()];
- itemCursor.moveToFirst();
- for (int i = 0; i < itemIds.length; i++) {
- itemIds[i] = itemCursor.getLong(PodDBAdapter.KEY_ID_INDEX);
- itemCursor.moveToNext();
- }
- itemCursor.close();
- adapter.setFeedItemRead(true, itemIds);
- adapter.close();
+ EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast();
+ });
+ }
- EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast();
+ /**
+ * Sets the 'read'-attribute of all FeedItems to true.
+ */
+ public static Future<?> markAllItemsRead() {
+ return dbExec.submit(() -> {
+ final PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ Cursor itemCursor = adapter.getUnreadItemsCursor();
+ long[] itemIds = new long[itemCursor.getCount()];
+ itemCursor.moveToFirst();
+ for (int i = 0; i < itemIds.length; i++) {
+ int indexId = itemCursor.getColumnIndex(PodDBAdapter.KEY_ID);
+ itemIds[i] = itemCursor.getLong(indexId);
+ itemCursor.moveToNext();
}
+ itemCursor.close();
+ adapter.setFeedItemRead(FeedItem.PLAYED, itemIds);
+ adapter.close();
+
+ EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast();
});
}
static Future<?> addNewFeed(final Context context, final Feed... feeds) {
- return dbExec.submit(new Runnable() {
-
- @Override
- public void run() {
- final PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- adapter.setCompleteFeed(feeds);
- adapter.close();
-
- if (ClientConfig.gpodnetCallbacks.gpodnetEnabled()) {
- for (Feed feed : feeds) {
- GpodnetPreferences.addAddedFeed(feed.getDownload_url());
- }
+ return dbExec.submit(() -> {
+ final PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.setCompleteFeed(feeds);
+ adapter.close();
+
+ if (ClientConfig.gpodnetCallbacks.gpodnetEnabled()) {
+ for (Feed feed : feeds) {
+ GpodnetPreferences.addAddedFeed(feed.getDownload_url());
}
-
- BackupManager backupManager = new BackupManager(context);
- backupManager.dataChanged();
}
+
+ BackupManager backupManager = new BackupManager(context);
+ backupManager.dataChanged();
});
}
- static Future<?> setCompleteFeed(final Context context, final Feed... feeds) {
- return dbExec.submit(new Runnable() {
-
- @Override
- public void run() {
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- adapter.setCompleteFeed(feeds);
- adapter.close();
-
- }
+ static Future<?> setCompleteFeed(final Feed... feeds) {
+ return dbExec.submit(() -> {
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.setCompleteFeed(feeds);
+ adapter.close();
});
-
}
/**
* Saves a FeedMedia object in the database. This method will save all attributes of the FeedMedia object. The
* contents of FeedComponent-attributes (e.g. the FeedMedia's 'item'-attribute) will not be saved.
*
- * @param context A context that is used for opening a database connection.
* @param media The FeedMedia object.
*/
- public static Future<?> setFeedMedia(final Context context,
- final FeedMedia media) {
- return dbExec.submit(new Runnable() {
-
- @Override
- public void run() {
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- adapter.setMedia(media);
- adapter.close();
- }
+ public static Future<?> setFeedMedia(final FeedMedia media) {
+ return dbExec.submit(() -> {
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.setMedia(media);
+ adapter.close();
});
}
/**
- * Saves the 'position' and 'duration' attributes of a FeedMedia object
+ * Saves the 'position', 'duration' and 'last played time' attributes of a FeedMedia object
*
- * @param context A context that is used for opening a database connection.
* @param media The FeedMedia object.
*/
- public static Future<?> setFeedMediaPlaybackInformation(final Context context, final FeedMedia media) {
- return dbExec.submit(new Runnable() {
- @Override
- public void run() {
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- adapter.setFeedMediaPlaybackInformation(media);
- adapter.close();
- }
+ public static Future<?> setFeedMediaPlaybackInformation(final FeedMedia media) {
+ return dbExec.submit(() -> {
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.setFeedMediaPlaybackInformation(media);
+ adapter.close();
});
}
@@ -778,20 +762,15 @@ public class DBWriter {
* Saves a FeedItem object in the database. This method will save all attributes of the FeedItem object including
* the content of FeedComponent-attributes.
*
- * @param context A context that is used for opening a database connection.
* @param item The FeedItem object.
*/
- public static Future<?> setFeedItem(final Context context,
- final FeedItem item) {
- return dbExec.submit(new Runnable() {
-
- @Override
- public void run() {
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- adapter.setSingleFeedItem(item);
- adapter.close();
- }
+ public static Future<?> setFeedItem(final FeedItem item) {
+ return dbExec.submit(() -> {
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.setSingleFeedItem(item);
+ adapter.close();
+ EventBus.getDefault().post(FeedItemEvent.updated(item));
});
}
@@ -799,60 +778,42 @@ 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 context A context that is used for opening a database connection.
* @param image The FeedImage object.
*/
- public static Future<?> setFeedImage(final Context context,
- final FeedImage image) {
- return dbExec.submit(new Runnable() {
-
- @Override
- public void run() {
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- adapter.setImage(image);
- adapter.close();
- }
+ public static Future<?> setFeedImage(final FeedImage image) {
+ return dbExec.submit(() -> {
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.setImage(image);
+ adapter.close();
});
}
/**
- * Updates download URLs of feeds from a given Map. The key of the Map is the original URL of the feed
- * and the value is the updated URL
+ * Updates download URL of a feed
*/
- public static Future<?> updateFeedDownloadURLs(final Context context, final Map<String, String> urls) {
- return dbExec.submit(new Runnable() {
- @Override
- public void run() {
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- for (String key : urls.keySet()) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Replacing URL " + key + " with url " + urls.get(key));
-
- adapter.setFeedDownloadUrl(key, urls.get(key));
- }
- adapter.close();
- }
+ public static Future<?> updateFeedDownloadURL(final String original, final String updated) {
+ Log.d(TAG, "updateFeedDownloadURL(original: " + original + ", updated: " + updated +")");
+ return dbExec.submit(() -> {
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.setFeedDownloadUrl(original, updated);
+ adapter.close();
});
}
/**
* Saves a FeedPreferences object in the database. The Feed ID of the FeedPreferences-object MUST NOT be 0.
*
- * @param context Used for opening a database connection.
* @param preferences The FeedPreferences object.
*/
- public static Future<?> setFeedPreferences(final Context context, final FeedPreferences preferences) {
- return dbExec.submit(new Runnable() {
- @Override
- public void run() {
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- adapter.setFeedPreferences(preferences);
- adapter.close();
- EventDistributor.getInstance().sendFeedUpdateBroadcast();
- }
+ public static Future<?> setFeedPreferences(final FeedPreferences preferences) {
+ return dbExec.submit(() -> {
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.setFeedPreferences(preferences);
+ adapter.close();
+ EventDistributor.getInstance().sendFeedUpdateBroadcast();
});
}
@@ -873,17 +834,13 @@ public class DBWriter {
public static Future<?> setFeedItemFlattrStatus(final Context context,
final FeedItem item,
final boolean startFlattrClickWorker) {
- return dbExec.submit(new Runnable() {
-
- @Override
- public void run() {
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- adapter.setFeedItemFlattrStatus(item);
- adapter.close();
- if (startFlattrClickWorker) {
- new FlattrClickWorker(context).executeAsync();
- }
+ return dbExec.submit(() -> {
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.setFeedItemFlattrStatus(item);
+ adapter.close();
+ if (startFlattrClickWorker) {
+ new FlattrClickWorker(context).executeAsync();
}
});
}
@@ -896,17 +853,13 @@ public class DBWriter {
private static Future<?> setFeedFlattrStatus(final Context context,
final Feed feed,
final boolean startFlattrClickWorker) {
- return dbExec.submit(new Runnable() {
-
- @Override
- public void run() {
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- adapter.setFeedFlattrStatus(feed);
- adapter.close();
- if (startFlattrClickWorker) {
- new FlattrClickWorker(context).executeAsync();
- }
+ return dbExec.submit(() -> {
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.setFeedFlattrStatus(feed);
+ adapter.close();
+ if (startFlattrClickWorker) {
+ new FlattrClickWorker(context).executeAsync();
}
});
}
@@ -916,18 +869,13 @@ public class DBWriter {
*
* @param lastUpdateFailed true if last update failed
*/
- public static Future<?> setFeedLastUpdateFailed(final Context context,
- final long feedId,
- final boolean lastUpdateFailed) {
- return dbExec.submit(new Runnable() {
-
- @Override
- public void run() {
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- adapter.setFeedLastUpdateFailed(feedId, lastUpdateFailed);
- adapter.close();
- }
+ public static Future<?> setFeedLastUpdateFailed(final long feedId,
+ final boolean lastUpdateFailed) {
+ return dbExec.submit(() -> {
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.setFeedLastUpdateFailed(feedId, lastUpdateFailed);
+ adapter.close();
});
}
@@ -955,14 +903,15 @@ public class DBWriter {
*/
public static Future<?> setFlattredStatus(Context context, FlattrThing thing, boolean startFlattrClickWorker) {
// must propagate this to back db
- if (thing instanceof FeedItem)
+ if (thing instanceof FeedItem) {
return setFeedItemFlattrStatus(context, (FeedItem) thing, startFlattrClickWorker);
- else if (thing instanceof Feed)
+ } else if (thing instanceof Feed) {
return setFeedFlattrStatus(context, (Feed) thing, startFlattrClickWorker);
- else if (thing instanceof SimpleFlattrThing) {
- } // SimpleFlattrThings are generated on the fly and do not have DB backing
- else
+ } else if (thing instanceof SimpleFlattrThing) {
+ // SimpleFlattrThings are generated on the fly and do not have DB backing
+ } else {
Log.e(TAG, "flattrQueue processing - thing is neither FeedItem nor Feed nor SimpleFlattrThing");
+ }
return null;
}
@@ -970,16 +919,13 @@ public class DBWriter {
/**
* Reset flattr status to unflattrd for all items
*/
- public static Future<?> clearAllFlattrStatus(final Context context) {
+ public static Future<?> clearAllFlattrStatus() {
Log.d(TAG, "clearAllFlattrStatus()");
- return dbExec.submit(new Runnable() {
- @Override
- public void run() {
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- adapter.clearAllFlattrStatus();
- adapter.close();
- }
+ return dbExec.submit(() -> {
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.clearAllFlattrStatus();
+ adapter.close();
});
}
@@ -987,99 +933,115 @@ public class DBWriter {
* Set flattr status of the feeds/feeditems in flattrList to flattred at the given timestamp,
* where the information has been retrieved from the flattr API
*/
- public static Future<?> setFlattredStatus(final Context context, final List<Flattr> flattrList) {
+ public static Future<?> setFlattredStatus(final List<Flattr> flattrList) {
Log.d(TAG, "setFlattredStatus to status retrieved from flattr api running with " + flattrList.size() + " items");
// clear flattr status in db
- clearAllFlattrStatus(context);
+ clearAllFlattrStatus();
// submit list with flattred things having normalized URLs to db
- return dbExec.submit(new Runnable() {
- @Override
- public void run() {
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- for (Flattr flattr : flattrList) {
- adapter.setItemFlattrStatus(formatURIForQuery(flattr.getThing().getUrl()), new FlattrStatus(flattr.getCreated().getTime()));
- }
- adapter.close();
+ return dbExec.submit(() -> {
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ for (Flattr flattr : flattrList) {
+ adapter.setItemFlattrStatus(formatURIForQuery(flattr.getThing().getUrl()), new FlattrStatus(flattr.getCreated().getTime()));
}
+ adapter.close();
});
}
/**
* Sort the FeedItems in the queue with the given Comparator.
- *
- * @param context A context that is used for opening a database connection.
* @param comparator FeedItem comparator
* @param broadcastUpdate true if this operation should trigger a QueueUpdateBroadcast. This option should be set to
- * false if the caller wants to avoid unexpected updates of the GUI.
*/
- public static Future<?> sortQueue(final Context context, final Comparator<FeedItem> comparator, final boolean broadcastUpdate) {
- return dbExec.submit(new Runnable() {
- @Override
- public void run() {
- final PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- final List<FeedItem> queue = DBReader.getQueue(context, adapter);
-
- if (queue != null) {
- Collections.sort(queue, comparator);
- adapter.setQueue(queue);
- if (broadcastUpdate) {
- EventBus.getDefault().post(new QueueEvent(QueueEvent.Action.SORTED));
- }
- } else {
- Log.e(TAG, "sortQueue: Could not load queue");
+ public static Future<?> sortQueue(final Comparator<FeedItem> comparator, final boolean broadcastUpdate) {
+ return dbExec.submit(() -> {
+ final PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ final List<FeedItem> queue = DBReader.getQueue(adapter);
+
+ if (queue != null) {
+ Collections.sort(queue, comparator);
+ adapter.setQueue(queue);
+ if (broadcastUpdate) {
+ EventBus.getDefault().post(QueueEvent.sorted(queue));
}
- adapter.close();
+ } else {
+ Log.e(TAG, "sortQueue: Could not load queue");
}
+ adapter.close();
});
}
/**
* Sets the 'auto_download'-attribute of specific FeedItem.
*
- * @param context A context that is used for opening a database connection.
* @param feedItem FeedItem.
+ * @param autoDownload true enables auto download, false disables it
*/
- public static Future<?> setFeedItemAutoDownload(final Context context, final FeedItem feedItem,
+ public static Future<?> setFeedItemAutoDownload(final FeedItem feedItem,
final boolean autoDownload) {
- Log.d(TAG, "FeedItem[id=" + feedItem.getId() + "] SET auto_download " + autoDownload);
- return dbExec.submit(new Runnable() {
-
- @Override
- public void run() {
- final PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- adapter.setFeedItemAutoDownload(feedItem, autoDownload);
- adapter.close();
+ return dbExec.submit(() -> {
+ final PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.setFeedItemAutoDownload(feedItem, autoDownload ? 1 : 0);
+ adapter.close();
+ EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast();
+ });
+ }
- EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast();
+ public static Future<?> saveFeedItemAutoDownloadFailed(final FeedItem feedItem) {
+ return dbExec.submit(() -> {
+ int failedAttempts = feedItem.getFailedAutoDownloadAttempts() + 1;
+ long autoDownload;
+ if(!feedItem.getAutoDownload() || failedAttempts >= 10) {
+ autoDownload = 0; // giving up, disable auto download
+ feedItem.setAutoDownload(false);
+ } else {
+ long now = System.currentTimeMillis();
+ autoDownload = (now / 10) * 10 + failedAttempts;
}
+ final PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.setFeedItemAutoDownload(feedItem, autoDownload);
+ adapter.close();
+ EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast();
});
+ }
+ /**
+ * Sets the 'auto_download'-attribute of specific FeedItem.
+ *
+ * @param feed This feed's episodes will be processed.
+ * @param autoDownload If true, auto download will be enabled for the feed's episodes. Else,
+ */
+ public static Future<?> setFeedsItemsAutoDownload(final Feed feed,
+ final boolean autoDownload) {
+ Log.d(TAG, (autoDownload ? "Enabling" : "Disabling") + " auto download for items of feed " + feed.getId());
+ return dbExec.submit(() -> {
+ final PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.setFeedsItemsAutoDownload(feed, autoDownload);
+ adapter.close();
+ EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast();
+ });
}
+
/**
* Set filter of the feed
- *
- * @param context Used for opening a database connection.
- * @param feedId The feed's ID
+ * @param feedId The feed's ID
* @param filterValues Values that represent properties to filter by
*/
- public static Future<?> setFeedItemsFilter(final Context context, final long feedId,
- final List<String> filterValues) {
- Log.d(TAG, "setFeedFilter");
-
- return dbExec.submit(new Runnable() {
- @Override
- public void run() {
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- adapter.setFeedItemFilter(feedId, filterValues);
- adapter.close();
- EventBus.getDefault().post(new FeedEvent(FeedEvent.Action.FILTER_CHANGED, feedId));
- }
+ public static Future<?> setFeedItemsFilter(final long feedId,
+ final Set<String> filterValues) {
+ Log.d(TAG, "setFeedItemsFilter() called with: " + "feedId = [" + feedId + "], filterValues = [" + filterValues + "]");
+ return dbExec.submit(() -> {
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.setFeedItemFilter(feedId, filterValues);
+ adapter.close();
+ EventBus.getDefault().post(new FeedEvent(FeedEvent.Action.FILTER_CHANGED, feedId));
});
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java
index ca6aa0178..0dc1dadeb 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
@@ -3,22 +3,20 @@ package de.danoeh.antennapod.core.storage;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
import android.util.Log;
import android.webkit.URLUtil;
import org.apache.commons.io.FilenameUtils;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.commons.lang3.Validate;
import java.io.File;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import de.danoeh.antennapod.core.BuildConfig;
-import de.danoeh.antennapod.core.feed.EventDistributor;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.FeedFile;
-import de.danoeh.antennapod.core.feed.FeedImage;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.download.DownloadRequest;
@@ -73,10 +71,8 @@ public class DownloadRequester {
* call will return false.
* @return True if the download request was accepted, false otherwise.
*/
- public synchronized boolean download(Context context, DownloadRequest request) {
- Validate.notNull(context);
- Validate.notNull(request);
-
+ public synchronized boolean download(@NonNull Context context,
+ @NonNull DownloadRequest request) {
if (downloads.containsKey(request.getSource())) {
if (BuildConfig.DEBUG) Log.i(TAG, "DownloadRequest is already stored.");
return false;
@@ -86,7 +82,7 @@ public class DownloadRequester {
Intent launchIntent = new Intent(context, DownloadService.class);
launchIntent.putExtra(DownloadService.EXTRA_REQUEST, request);
context.startService(launchIntent);
- EventDistributor.getInstance().sendDownloadQueuedBroadcast();
+
return true;
}
@@ -147,7 +143,7 @@ public class DownloadRequester {
private boolean isFilenameAvailable(String path) {
for (String key : downloads.keySet()) {
DownloadRequest r = downloads.get(key);
- if (StringUtils.equals(r.getDestination(), path)) {
+ if (TextUtils.equals(r.getDestination(), path)) {
if (BuildConfig.DEBUG)
Log.d(TAG, path
+ " is already used by another requested download");
@@ -171,7 +167,7 @@ public class DownloadRequester {
if (feedFileValid(feed)) {
String username = (feed.getPreferences() != null) ? feed.getPreferences().getUsername() : null;
String password = (feed.getPreferences() != null) ? feed.getPreferences().getPassword() : null;
- long ifModifiedSince = feed.getLastUpdate().getTime();
+ long ifModifiedSince = feed.isPaged() ? 0 : feed.getLastUpdate().getTime();
Bundle args = new Bundle();
args.putInt(REQUEST_ARG_PAGE_NR, feed.getPageNr());
@@ -186,15 +182,6 @@ public class DownloadRequester {
downloadFeed(context, feed, false);
}
- public synchronized void downloadImage(Context context, FeedImage image)
- throws DownloadRequestException {
- if (feedFileValid(image)) {
- FeedFile container = (image.getOwner() instanceof FeedFile) ? (FeedFile) image.getOwner() : null;
- download(context, image, container, new File(getImagefilePath(context),
- getImagefileName(image)), false, null, null, 0, false, null);
- }
- }
-
public synchronized void downloadMedia(Context context, FeedMedia feedmedia)
throws DownloadRequestException {
if (feedFileValid(feedmedia)) {
@@ -332,20 +319,6 @@ public class DownloadRequester {
return "feed-" + FileNameGenerator.generateFileName(filename);
}
- public synchronized String getImagefilePath(Context context)
- throws DownloadRequestException {
- return getExternalFilesDirOrThrowException(context, IMAGE_DOWNLOADPATH)
- .toString() + "/";
- }
-
- public synchronized String getImagefileName(FeedImage image) {
- String filename = image.getDownload_url();
- if (image.getOwner() != null && image.getOwner().getHumanReadableIdentifier() != null) {
- filename = image.getOwner().getHumanReadableIdentifier();
- }
- return "image-" + FileNameGenerator.generateFileName(filename);
- }
-
public synchronized String getMediafilePath(Context context, FeedMedia media)
throws DownloadRequestException {
File externalStorage = getExternalFilesDirOrThrowException(
@@ -359,7 +332,7 @@ public class DownloadRequester {
private File getExternalFilesDirOrThrowException(Context context,
String type) throws DownloadRequestException {
- File result = UserPreferences.getDataFolder(context, type);
+ File result = UserPreferences.getDataFolder(type);
if (result == null) {
throw new DownloadRequestException(
"Failed to access external storage");
@@ -375,7 +348,7 @@ public class DownloadRequester {
if (media.getItem() != null && media.getItem().getTitle() != null) {
String title = media.getItem().getTitle();
// Delete reserved characters
- titleBaseFilename = title.replaceAll("[\\\\/%\\?\\*:|<>\"\\p{Cntrl}]", "");
+ titleBaseFilename = title.replaceAll("[^a-zA-Z0-9 ._()-]", "");
titleBaseFilename = titleBaseFilename.trim();
}
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 6a8b4a441..0f402745c 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
@@ -2,35 +2,60 @@ package de.danoeh.antennapod.core.storage;
import android.content.Context;
-import java.util.List;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
-import de.danoeh.antennapod.core.feed.FeedItem;
-
-public interface EpisodeCleanupAlgorithm<T> {
+public abstract class EpisodeCleanupAlgorithm {
/**
* Deletes downloaded episodes that are no longer needed. What episodes are deleted and how many
* of them depends on the implementation.
*
- * @param context Can be used for accessing the database
- * @param parameter An additional parameter. This parameter is either returned by getDefaultCleanupParameter
- * or getPerformCleanupParameter.
+ * @param context Can be used for accessing the database
+ * @param numToRemove An additional parameter. This parameter is either returned by getDefaultCleanupParameter
+ * or getPerformCleanupParameter.
* @return The number of episodes that were deleted.
*/
- public int performCleanup(Context context, T parameter);
+ public abstract int performCleanup(Context context, int numToRemove);
+
+ public int performCleanup(Context context) {
+ return performCleanup(context, getDefaultCleanupParameter());
+ }
/**
* Returns a parameter for performCleanup. The implementation of this interface should decide how much
* space to free to satisfy the episode cache conditions. If the conditions are already satisfied, this
* method should not have any effects.
*/
- public T getDefaultCleanupParameter(Context context);
+ public abstract int getDefaultCleanupParameter();
/**
- * Returns a parameter for performCleanup.
+ * Cleans up just enough episodes to make room for the requested number
*
- * @param items A list of FeedItems that are about to be downloaded. The implementation of this interface
- * should decide how much space to free to satisfy the episode cache conditions.
+ * @param context Can be used for accessing the database
+ * @param amountOfRoomNeeded the number of episodes we need space for
+ * @return The number of epiosdes that were deleted
+ */
+ public int makeRoomForEpisodes(Context context, int amountOfRoomNeeded) {
+ return performCleanup(context, getNumEpisodesToCleanup(amountOfRoomNeeded));
+ }
+
+ /**
+ * @param amountOfRoomNeeded the number of episodes we want to download
+ * @return the number of episodes to delete in order to make room
*/
- public T getPerformCleanupParameter(Context context, List<FeedItem> items);
+ protected int getNumEpisodesToCleanup(final int amountOfRoomNeeded) {
+ if (amountOfRoomNeeded >= 0
+ && UserPreferences.getEpisodeCacheSize() != UserPreferences
+ .getEpisodeCacheSizeUnlimited()) {
+ int downloadedEpisodes = DBReader
+ .getNumberOfDownloadedEpisodes();
+ if (downloadedEpisodes + amountOfRoomNeeded >= UserPreferences
+ .getEpisodeCacheSize()) {
+
+ return downloadedEpisodes + amountOfRoomNeeded
+ - UserPreferences.getEpisodeCacheSize();
+ }
+ }
+ return 0;
+ }
}
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 f6a59836b..09949b87e 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
@@ -1,5 +1,7 @@
package de.danoeh.antennapod.core.storage;
+import android.database.Cursor;
+
import java.util.Date;
/**
@@ -36,6 +38,15 @@ public class FeedItemStatistics {
}
}
+ public static FeedItemStatistics fromCursor(Cursor cursor) {
+ return new FeedItemStatistics(
+ cursor.getLong(0),
+ cursor.getInt(1),
+ cursor.getInt(2),
+ cursor.getInt(4),
+ new Date(cursor.getLong(3)));
+ }
+
public long getFeedID() {
return feedID;
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java b/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java
index 4780098e0..85ff8fc8c 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
@@ -9,16 +9,16 @@ 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.text.TextUtils;
import android.util.Log;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.commons.lang3.Validate;
-
import java.util.Arrays;
import java.util.List;
+import java.util.Set;
-import de.danoeh.antennapod.core.BuildConfig;
-import de.danoeh.antennapod.core.ClientConfig;
+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;
@@ -26,11 +26,11 @@ 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.preferences.UserPreferences;
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;
// TODO Remove media column from feeditem table
@@ -38,6 +38,7 @@ import de.danoeh.antennapod.core.util.flattr.FlattrStatus;
* Implements methods for accessing the database
*/
public class PodDBAdapter {
+
private static final String TAG = "PodDBAdapter";
public static final String DATABASE_NAME = "Antennapod.db";
@@ -51,63 +52,6 @@ public class PodDBAdapter {
*/
public static final int SEARCH_LIMIT = 30;
- // ----------- Column indices
- // ----------- General indices
- public static final int KEY_ID_INDEX = 0;
- public static final int KEY_TITLE_INDEX = 1;
- public static final int KEY_FILE_URL_INDEX = 2;
- public static final int KEY_DOWNLOAD_URL_INDEX = 3;
- public static final int KEY_DOWNLOADED_INDEX = 4;
- public static final int KEY_LINK_INDEX = 5;
- public static final int KEY_DESCRIPTION_INDEX = 6;
- public static final int KEY_PAYMENT_LINK_INDEX = 7;
- // ----------- Feed indices
- public static final int KEY_LAST_UPDATE_INDEX = 8;
- public static final int KEY_LANGUAGE_INDEX = 9;
- public static final int KEY_AUTHOR_INDEX = 10;
- public static final int KEY_IMAGE_INDEX = 11;
- public static final int KEY_TYPE_INDEX = 12;
- public static final int KEY_FEED_IDENTIFIER_INDEX = 13;
- public static final int KEY_FEED_FLATTR_STATUS_INDEX = 14;
- public static final int KEY_FEED_USERNAME_INDEX = 15;
- public static final int KEY_FEED_PASSWORD_INDEX = 16;
- public static final int KEY_IS_PAGED_INDEX = 17;
- public static final int KEY_LOAD_ALL_PAGES_INDEX = 18;
- public static final int KEY_NEXT_PAGE_LINK_INDEX = 19;
- // ----------- FeedItem indices
- public static final int KEY_CONTENT_ENCODED_INDEX = 2;
- public static final int KEY_PUBDATE_INDEX = 3;
- public static final int KEY_READ_INDEX = 4;
- public static final int KEY_MEDIA_INDEX = 8;
- public static final int KEY_FEED_INDEX = 9;
- public static final int KEY_HAS_SIMPLECHAPTERS_INDEX = 10;
- public static final int KEY_ITEM_IDENTIFIER_INDEX = 11;
- public static final int KEY_ITEM_FLATTR_STATUS_INDEX = 12;
- // ---------- FeedMedia indices
- public static final int KEY_DURATION_INDEX = 1;
- public static final int KEY_POSITION_INDEX = 5;
- public static final int KEY_SIZE_INDEX = 6;
- public static final int KEY_MIME_TYPE_INDEX = 7;
- public static final int KEY_PLAYBACK_COMPLETION_DATE_INDEX = 8;
- public static final int KEY_MEDIA_FEEDITEM_INDEX = 9;
- public static final int KEY_PLAYED_DURATION_INDEX = 10;
- // --------- Download log indices
- public static final int KEY_FEEDFILE_INDEX = 1;
- public static final int KEY_FEEDFILETYPE_INDEX = 2;
- public static final int KEY_REASON_INDEX = 3;
- public static final int KEY_SUCCESSFUL_INDEX = 4;
- public static final int KEY_COMPLETION_DATE_INDEX = 5;
- public static final int KEY_REASON_DETAILED_INDEX = 6;
- public static final int KEY_DOWNLOADSTATUS_TITLE_INDEX = 7;
- // --------- Queue indices
- public static final int KEY_FEEDITEM_INDEX = 1;
- public static final int KEY_QUEUE_FEED_INDEX = 2;
- // --------- Chapters indices
- public static final int KEY_CHAPTER_START_INDEX = 2;
- public static final int KEY_CHAPTER_FEEDITEM_INDEX = 3;
- public static final int KEY_CHAPTER_LINK_INDEX = 4;
- public static final int KEY_CHAPTER_TYPE_INDEX = 5;
-
// Key-constants
public static final String KEY_ID = "id";
public static final String KEY_TITLE = "title";
@@ -148,6 +92,8 @@ public class PodDBAdapter {
public static final String KEY_CHAPTER_TYPE = "type";
public static final String KEY_PLAYBACK_COMPLETION_DATE = "playback_completion_date";
public static final String KEY_AUTO_DOWNLOAD = "auto_download";
+ public static final String KEY_KEEP_UPDATED = "keep_updated";
+ public static final String KEY_AUTO_DELETE_ACTION = "auto_delete_action";
public static final String KEY_PLAYED_DURATION = "played_duration";
public static final String KEY_USERNAME = "username";
public static final String KEY_PASSWORD = "password";
@@ -155,6 +101,10 @@ public class PodDBAdapter {
public static final String KEY_NEXT_PAGE_LINK = "next_page_link";
public static final String KEY_HIDE = "hide";
public static final String KEY_LAST_UPDATE_FAILED = "last_update_failed";
+ public static final String KEY_HAS_EMBEDDED_PICTURE = "has_embedded_picture";
+ public static final String KEY_LAST_PLAYED_TIME = "last_played_time";
+ public static final String KEY_INCLUDE_FILTER = "include_filter";
+ public static final String KEY_EXCLUDE_FILTER = "exclude_filter";
// Table names
public static final String TABLE_NAME_FEEDS = "Feeds";
@@ -164,6 +114,7 @@ public class PodDBAdapter {
public static final String TABLE_NAME_DOWNLOAD_LOG = "DownloadLog";
public static final String TABLE_NAME_QUEUE = "Queue";
public static final String TABLE_NAME_SIMPLECHAPTERS = "SimpleChapters";
+ public static final String TABLE_NAME_FAVORITES = "Favorites";
// SQL Statements for creating new tables
private static final String TABLE_PRIMARY_KEY = KEY_ID
@@ -180,10 +131,14 @@ public class PodDBAdapter {
+ KEY_FLATTR_STATUS + " INTEGER,"
+ KEY_USERNAME + " TEXT,"
+ KEY_PASSWORD + " TEXT,"
+ + KEY_INCLUDE_FILTER + " TEXT DEFAULT '',"
+ + KEY_EXCLUDE_FILTER + " TEXT DEFAULT '',"
+ + KEY_KEEP_UPDATED + " INTEGER DEFAULT 1,"
+ KEY_IS_PAGED + " INTEGER DEFAULT 0,"
+ KEY_NEXT_PAGE_LINK + " TEXT,"
+ KEY_HIDE + " TEXT,"
- + KEY_LAST_UPDATE_FAILED + " INTEGER DEFAULT 0)";
+ + KEY_LAST_UPDATE_FAILED + " INTEGER DEFAULT 0,"
+ + KEY_AUTO_DELETE_ACTION + " INTEGER DEFAULT 0)";
public static final String CREATE_TABLE_FEED_ITEMS = "CREATE TABLE "
+ TABLE_NAME_FEED_ITEMS + " (" + TABLE_PRIMARY_KEY + KEY_TITLE
@@ -209,7 +164,8 @@ public class PodDBAdapter {
+ KEY_PLAYBACK_COMPLETION_DATE + " INTEGER,"
+ KEY_FEEDITEM + " INTEGER,"
+ KEY_PLAYED_DURATION + " INTEGER,"
- + KEY_AUTO_DOWNLOAD + " INTEGER)";
+ + KEY_HAS_EMBEDDED_PICTURE + " INTEGER,"
+ + KEY_LAST_PLAYED_TIME + " INTEGER)";
public static final String CREATE_TABLE_DOWNLOAD_LOG = "CREATE TABLE "
+ TABLE_NAME_DOWNLOAD_LOG + " (" + TABLE_PRIMARY_KEY + KEY_FEEDFILE
@@ -236,6 +192,15 @@ public class PodDBAdapter {
+ 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 "
+ + 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 "
+ + TABLE_NAME_FEED_ITEMS + "_" + KEY_READ + " ON " + TABLE_NAME_FEED_ITEMS + " ("
+ + KEY_READ + ")";
+
+
public static final String CREATE_INDEX_QUEUE_FEEDITEM = "CREATE INDEX "
+ TABLE_NAME_QUEUE + "_" + KEY_FEEDITEM + " ON " + TABLE_NAME_QUEUE + " ("
+ KEY_FEEDITEM + ")";
@@ -248,11 +213,10 @@ public class PodDBAdapter {
+ TABLE_NAME_SIMPLECHAPTERS + "_" + KEY_FEEDITEM + " ON " + TABLE_NAME_SIMPLECHAPTERS + " ("
+ KEY_FEEDITEM + ")";
-
- private SQLiteDatabase db;
- private final Context context;
- private PodDBHelper helper;
-
+ public static final String CREATE_TABLE_FAVORITES = "CREATE TABLE "
+ + TABLE_NAME_FAVORITES + "(" + KEY_ID + " INTEGER PRIMARY KEY,"
+ + KEY_FEEDITEM + " INTEGER," + KEY_FEED + " INTEGER)";
+
/**
* Select all columns from the feed-table
*/
@@ -272,6 +236,7 @@ public class PodDBAdapter {
TABLE_NAME_FEEDS + "." + KEY_TYPE,
TABLE_NAME_FEEDS + "." + KEY_FEED_IDENTIFIER,
TABLE_NAME_FEEDS + "." + KEY_AUTO_DOWNLOAD,
+ TABLE_NAME_FEEDS + "." + KEY_KEEP_UPDATED,
TABLE_NAME_FEEDS + "." + KEY_FLATTR_STATUS,
TABLE_NAME_FEEDS + "." + KEY_IS_PAGED,
TABLE_NAME_FEEDS + "." + KEY_NEXT_PAGE_LINK,
@@ -279,30 +244,11 @@ public class PodDBAdapter {
TABLE_NAME_FEEDS + "." + KEY_PASSWORD,
TABLE_NAME_FEEDS + "." + KEY_HIDE,
TABLE_NAME_FEEDS + "." + KEY_LAST_UPDATE_FAILED,
+ TABLE_NAME_FEEDS + "." + KEY_AUTO_DELETE_ACTION,
+ TABLE_NAME_FEEDS + "." + KEY_INCLUDE_FILTER,
+ TABLE_NAME_FEEDS + "." + KEY_EXCLUDE_FILTER
};
-
- // column indices for FEED_SEL_STD
- public static final int IDX_FEED_SEL_STD_ID = 0;
- public static final int IDX_FEED_SEL_STD_TITLE = 1;
- public static final int IDX_FEED_SEL_STD_FILE_URL = 2;
- public static final int IDX_FEED_SEL_STD_DOWNLOAD_URL = 3;
- public static final int IDX_FEED_SEL_STD_DOWNLOADED = 4;
- public static final int IDX_FEED_SEL_STD_LINK = 5;
- public static final int IDX_FEED_SEL_STD_DESCRIPTION = 6;
- public static final int IDX_FEED_SEL_STD_PAYMENT_LINK = 7;
- public static final int IDX_FEED_SEL_STD_LASTUPDATE = 8;
- public static final int IDX_FEED_SEL_STD_LANGUAGE = 9;
- public static final int IDX_FEED_SEL_STD_AUTHOR = 10;
- public static final int IDX_FEED_SEL_STD_IMAGE = 11;
- public static final int IDX_FEED_SEL_STD_TYPE = 12;
- public static final int IDX_FEED_SEL_STD_FEED_IDENTIFIER = 13;
- public static final int IDX_FEED_SEL_PREFERENCES_AUTO_DOWNLOAD = 14;
- public static final int IDX_FEED_SEL_STD_FLATTR_STATUS = 15;
- public static final int IDX_FEED_SEL_STD_IS_PAGED = 16;
- public static final int IDX_FEED_SEL_STD_NEXT_PAGE_LINK = 17;
- public static final int IDX_FEED_SEL_PREFERENCES_USERNAME = 18;
- public static final int IDX_FEED_SEL_PREFERENCES_PASSWORD = 19;
-
+
/**
* Select all columns from the feeditems-table except description and
* content-encoded.
@@ -313,7 +259,8 @@ public class PodDBAdapter {
TABLE_NAME_FEED_ITEMS + "." + KEY_PUBDATE,
TABLE_NAME_FEED_ITEMS + "." + KEY_READ,
TABLE_NAME_FEED_ITEMS + "." + KEY_LINK,
- TABLE_NAME_FEED_ITEMS + "." + KEY_PAYMENT_LINK, KEY_MEDIA,
+ TABLE_NAME_FEED_ITEMS + "." + KEY_PAYMENT_LINK,
+ TABLE_NAME_FEED_ITEMS + "." + KEY_MEDIA,
TABLE_NAME_FEED_ITEMS + "." + KEY_FEED,
TABLE_NAME_FEED_ITEMS + "." + KEY_HAS_CHAPTERS,
TABLE_NAME_FEED_ITEMS + "." + KEY_ITEM_IDENTIFIER,
@@ -323,6 +270,20 @@ public class PodDBAdapter {
};
/**
+ * All the tables in the database
+ */
+ 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,
+ TABLE_NAME_SIMPLECHAPTERS,
+ TABLE_NAME_FAVORITES
+ };
+
+ /**
* Contains FEEDITEM_SEL_FI_SMALL as comma-separated list. Useful for raw queries.
*/
private static final String SEL_FI_SMALL_STR;
@@ -332,74 +293,56 @@ public class PodDBAdapter {
SEL_FI_SMALL_STR = selFiSmall.substring(1, selFiSmall.length() - 1);
}
- // column indices for FEEDITEM_SEL_FI_SMALL
-
- public static final int IDX_FI_SMALL_ID = 0;
- public static final int IDX_FI_SMALL_TITLE = 1;
- public static final int IDX_FI_SMALL_PUBDATE = 2;
- public static final int IDX_FI_SMALL_READ = 3;
- public static final int IDX_FI_SMALL_LINK = 4;
- public static final int IDX_FI_SMALL_PAYMENT_LINK = 5;
- public static final int IDX_FI_SMALL_MEDIA = 6;
- public static final int IDX_FI_SMALL_FEED = 7;
- public static final int IDX_FI_SMALL_HAS_CHAPTERS = 8;
- public static final int IDX_FI_SMALL_ITEM_IDENTIFIER = 9;
- public static final int IDX_FI_SMALL_FLATTR_STATUS = 10;
- public static final int IDX_FI_SMALL_IMAGE = 11;
-
/**
* Select id, description and content-encoded column from feeditems.
*/
private static final String[] SEL_FI_EXTRA = {KEY_ID, KEY_DESCRIPTION,
KEY_CONTENT_ENCODED, KEY_FEED};
- // column indices for SEL_FI_EXTRA
- public static final int IDX_FI_EXTRA_ID = 0;
- public static final int IDX_FI_EXTRA_DESCRIPTION = 1;
- public static final int IDX_FI_EXTRA_CONTENT_ENCODED = 2;
- public static final int IDX_FI_EXTRA_FEED = 3;
+ private static SQLiteDatabase db;
+ private static Context context;
+ private static PodDBHelper dbHelper;
+ private static int counter = 0;
- static PodDBHelper dbHelperSingleton;
+ public static void init(Context context) {
+ PodDBAdapter.context = context.getApplicationContext();
+ }
- private static synchronized PodDBHelper getDbHelperSingleton(Context appContext) {
- if (dbHelperSingleton == null) {
- dbHelperSingleton = new PodDBHelper(appContext, DATABASE_NAME, null,
- ClientConfig.storageCallbacks.getDatabaseVersion());
+ public static synchronized PodDBAdapter getInstance() {
+ if(dbHelper == null) {
+ dbHelper = new PodDBHelper(PodDBAdapter.context, DATABASE_NAME, null);
}
- return dbHelperSingleton;
+ return new PodDBAdapter();
}
- public PodDBAdapter(Context c) {
- this.context = c;
- helper = getDbHelperSingleton(c.getApplicationContext());
- }
+ private PodDBAdapter() {}
public PodDBAdapter open() {
if (db == null || !db.isOpen() || db.isReadOnly()) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Opening DB");
+ Log.v(TAG, "Opening DB");
try {
- db = helper.getWritableDatabase();
+ db = dbHelper.getWritableDatabase();
} catch (SQLException ex) {
- ex.printStackTrace();
- db = helper.getReadableDatabase();
+ Log.e(TAG, Log.getStackTraceString(ex));
+ db = dbHelper.getReadableDatabase();
}
}
return this;
}
public void close() {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Closing DB");
- //db.close();
+ // do nothing
}
- public static boolean deleteDatabase(Context context) {
- Log.w(TAG, "Deleting database");
- dbHelperSingleton.close();
- dbHelperSingleton = null;
- return context.deleteDatabase(DATABASE_NAME);
+ public static boolean deleteDatabase() {
+ PodDBAdapter adapter = getInstance();
+ adapter.open();
+ for (String tableName : ALL_TABLES) {
+ db.delete(tableName, "1", null);
+ }
+ adapter.close();
+ return true;
}
/**
@@ -435,7 +378,7 @@ public class PodDBAdapter {
values.put(KEY_IS_PAGED, feed.isPaged());
values.put(KEY_NEXT_PAGE_LINK, feed.getNextPageLink());
if(feed.getItemFilter() != null && feed.getItemFilter().getValues().length > 0) {
- values.put(KEY_HIDE, StringUtils.join(feed.getItemFilter().getValues(), ","));
+ values.put(KEY_HIDE, TextUtils.join( ",", feed.getItemFilter().getValues()));
} else {
values.put(KEY_HIDE, "");
}
@@ -458,15 +401,20 @@ public class PodDBAdapter {
}
ContentValues values = new ContentValues();
values.put(KEY_AUTO_DOWNLOAD, prefs.getAutoDownload());
+ values.put(KEY_KEEP_UPDATED, prefs.getKeepUpdated());
+ values.put(KEY_AUTO_DELETE_ACTION,prefs.getAutoDeleteAction().ordinal());
values.put(KEY_USERNAME, prefs.getUsername());
values.put(KEY_PASSWORD, prefs.getPassword());
+ values.put(KEY_INCLUDE_FILTER, prefs.getFilter().getIncludeFilter());
+ values.put(KEY_EXCLUDE_FILTER, prefs.getFilter().getExcludeFilter());
db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?", new String[]{String.valueOf(prefs.getFeedID())});
}
- public void setFeedItemFilter(long feedId, List<String> filterValues) {
+ public void setFeedItemFilter(long feedId, Set<String> filterValues) {
+ Log.d(TAG, "setFeedItemFilter() called with: " + "feedId = [" + feedId + "], " +
+ "filterValues = [" + TextUtils.join(",", filterValues) + "]");
ContentValues values = new ContentValues();
- values.put(KEY_HIDE, StringUtils.join(filterValues, ","));
- Log.d(TAG, StringUtils.join(filterValues, ","));
+ values.put(KEY_HIDE, TextUtils.join(",", filterValues));
db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?", new String[]{String.valueOf(feedId)});
}
@@ -476,7 +424,12 @@ public class PodDBAdapter {
* @return the id of the entry
*/
public long setImage(FeedImage image) {
- db.beginTransaction();
+ boolean startedTransaction = false;
+ if(false == db.inTransaction()) {
+ db.beginTransaction();
+ startedTransaction = true;
+ }
+
ContentValues values = new ContentValues();
values.put(KEY_TITLE, image.getTitle());
values.put(KEY_DOWNLOAD_URL, image.getDownload_url());
@@ -497,8 +450,10 @@ public class PodDBAdapter {
db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?", new String[]{String.valueOf(image.getOwner().getId())});
}
}
- db.setTransactionSuccessful();
- db.endTransaction();
+ if(startedTransaction) {
+ db.setTransactionSuccessful();
+ db.endTransaction();
+ }
return image.getId();
}
@@ -516,10 +471,11 @@ public class PodDBAdapter {
values.put(KEY_DOWNLOAD_URL, media.getDownload_url());
values.put(KEY_DOWNLOADED, media.isDownloaded());
values.put(KEY_FILE_URL, media.getFile_url());
+ values.put(KEY_HAS_EMBEDDED_PICTURE, media.hasEmbeddedPicture());
+ values.put(KEY_LAST_PLAYED_TIME, media.getLastPlayedTime());
if (media.getPlaybackCompletionDate() != null) {
- values.put(KEY_PLAYBACK_COMPLETION_DATE, media
- .getPlaybackCompletionDate().getTime());
+ values.put(KEY_PLAYBACK_COMPLETION_DATE, media.getPlaybackCompletionDate().getTime());
} else {
values.put(KEY_PLAYBACK_COMPLETION_DATE, 0);
}
@@ -541,6 +497,7 @@ public class PodDBAdapter {
values.put(KEY_POSITION, media.getPosition());
values.put(KEY_DURATION, media.getDuration());
values.put(KEY_PLAYED_DURATION, media.getPlayedDuration());
+ values.put(KEY_LAST_PLAYED_TIME, media.getLastPlayedTime());
db.update(TABLE_NAME_FEED_MEDIA, values, KEY_ID + "=?",
new String[]{String.valueOf(media.getId())});
} else {
@@ -736,7 +693,13 @@ public class PodDBAdapter {
setFeed(item.getFeed());
}
values.put(KEY_FEED, item.getFeed().getId());
- values.put(KEY_READ, item.isRead());
+ if(item.isNew()) {
+ values.put(KEY_READ, FeedItem.NEW);
+ } else if(item.isPlayed()) {
+ values.put(KEY_READ, FeedItem.PLAYED);
+ } else {
+ values.put(KEY_READ, FeedItem.UNPLAYED);
+ }
values.put(KEY_HAS_CHAPTERS, item.getChapters() != null || item.hasChapters());
values.put(KEY_ITEM_IDENTIFIER, item.getItemIdentifier());
values.put(KEY_FLATTR_STATUS, item.getFlattrStatus().toLong());
@@ -763,12 +726,12 @@ public class PodDBAdapter {
return item.getId();
}
- public void setFeedItemRead(boolean read, long itemId, long mediaId,
+ public void setFeedItemRead(int played, long itemId, long mediaId,
boolean resetMediaPosition) {
db.beginTransaction();
ContentValues values = new ContentValues();
- values.put(KEY_READ, read);
+ values.put(KEY_READ, played);
db.update(TABLE_NAME_FEED_ITEMS, values, KEY_ID + "=?", new String[]{String.valueOf(itemId)});
if (resetMediaPosition) {
@@ -781,7 +744,12 @@ public class PodDBAdapter {
db.endTransaction();
}
- public void setFeedItemRead(boolean read, long... itemIds) {
+ /**
+ * Sets the 'read' attribute of the item.
+ * @param read must be one of FeedItem.PLAYED, FeedItem.NEW, FeedItem.UNPLAYED
+ * @param itemIds items to change the value of
+ */
+ public void setFeedItemRead(int read, long... itemIds) {
db.beginTransaction();
ContentValues values = new ContentValues();
for (long id : itemIds) {
@@ -802,8 +770,7 @@ public class PodDBAdapter {
values.put(KEY_LINK, chapter.getLink());
values.put(KEY_CHAPTER_TYPE, chapter.getChapterType());
if (chapter.getId() == 0) {
- chapter.setId(db
- .insert(TABLE_NAME_SIMPLECHAPTERS, null, values));
+ chapter.setId(db.insert(TABLE_NAME_SIMPLECHAPTERS, null, values));
} else {
db.update(TABLE_NAME_SIMPLECHAPTERS, values, KEY_ID + "=?",
new String[]{String.valueOf(chapter.getId())});
@@ -839,11 +806,65 @@ public class PodDBAdapter {
return status.getId();
}
- public void setFeedItemAutoDownload(FeedItem feedItem, boolean autoDownload) {
+ public void setFeedItemAutoDownload(FeedItem feedItem, long autoDownload) {
ContentValues values = new ContentValues();
values.put(KEY_AUTO_DOWNLOAD, autoDownload);
db.update(TABLE_NAME_FEED_ITEMS, values, KEY_ID + "=?",
- new String[] { String.valueOf(feedItem.getId()) } );
+ new String[]{String.valueOf(feedItem.getId())});
+ }
+
+ public void setFeedsItemsAutoDownload(Feed feed, boolean autoDownload) {
+ final String sql = "UPDATE " + TABLE_NAME_FEED_ITEMS
+ + " SET " + KEY_AUTO_DOWNLOAD + "="+ (autoDownload ? "1" : "0")
+ + " WHERE " + KEY_FEED + "=" + feed.getId();
+ db.execSQL(sql);
+ }
+
+ public void setFavorites(List<FeedItem> favorites) {
+ ContentValues values = new ContentValues();
+ db.beginTransaction();
+ db.delete(TABLE_NAME_FAVORITES, null, null);
+ for (int i = 0; i < favorites.size(); i++) {
+ FeedItem item = favorites.get(i);
+ values.put(KEY_ID, i);
+ values.put(KEY_FEEDITEM, item.getId());
+ values.put(KEY_FEED, item.getFeed().getId());
+ db.insertWithOnConflict(TABLE_NAME_FAVORITES, null, values, SQLiteDatabase.CONFLICT_REPLACE);
+ }
+ db.setTransactionSuccessful();
+ db.endTransaction();
+ }
+
+ /**
+ * Adds the item to favorites
+ */
+ public void addFavoriteItem(FeedItem item) {
+ // don't add an item that's already there...
+ if (isItemInFavorites(item)) {
+ Log.d(TAG, "item already in favorites");
+ return;
+ }
+ ContentValues values = new ContentValues();
+ values.put(KEY_FEEDITEM, item.getId());
+ values.put(KEY_FEED, item.getFeedId());
+ db.insert(TABLE_NAME_FAVORITES, null, values);
+ }
+
+ public void removeFavoriteItem(FeedItem item) {
+ String deleteClause = String.format("DELETE FROM %s WHERE %s=%s AND %s=%s",
+ TABLE_NAME_FAVORITES,
+ KEY_FEEDITEM, item.getId(),
+ KEY_FEED, item.getFeedId());
+ db.execSQL(deleteClause);
+ }
+
+ public 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);
+ int count = c.getCount();
+ c.close();
+ return count > 0;
}
public long getDownloadLogSize() {
@@ -857,14 +878,6 @@ public class PodDBAdapter {
return count;
}
- public void removeDownloadLogItems(long count) {
- if (count > 0) {
- final String sql = String.format("DELETE FROM %s WHERE %s in (SELECT %s from %s ORDER BY %s ASC LIMIT %d)",
- TABLE_NAME_DOWNLOAD_LOG, KEY_ID, KEY_ID, TABLE_NAME_DOWNLOAD_LOG, KEY_COMPLETION_DATE, count);
- db.execSQL(sql, null);
- }
- }
-
public void setQueue(List<FeedItem> queue) {
ContentValues values = new ContentValues();
db.beginTransaction();
@@ -874,8 +887,7 @@ public class PodDBAdapter {
values.put(KEY_ID, i);
values.put(KEY_FEEDITEM, item.getId());
values.put(KEY_FEED, item.getFeed().getId());
- db.insertWithOnConflict(TABLE_NAME_QUEUE, null, values,
- SQLiteDatabase.CONFLICT_REPLACE);
+ db.insertWithOnConflict(TABLE_NAME_QUEUE, null, values, SQLiteDatabase.CONFLICT_REPLACE);
}
db.setTransactionSuccessful();
db.endTransaction();
@@ -886,6 +898,10 @@ public class PodDBAdapter {
}
public 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) });
+
db.delete(TABLE_NAME_FEED_MEDIA, KEY_ID + "=?",
new String[]{String.valueOf(media.getId())});
}
@@ -930,6 +946,9 @@ public class PodDBAdapter {
removeFeedItem(item);
}
}
+ // delete download log entries for feed
+ db.delete(TABLE_NAME_DOWNLOAD_LOG, KEY_FEEDFILE + "=? AND " + KEY_FEEDFILETYPE +"=?",
+ new String[] { String.valueOf(feed.getId()), String.valueOf(Feed.FEEDFILETYPE_FEED) });
db.delete(TABLE_NAME_FEEDS, KEY_ID + "=?",
new String[]{String.valueOf(feed.getId())});
@@ -937,11 +956,6 @@ public class PodDBAdapter {
db.endTransaction();
}
- public void removeDownloadStatus(DownloadStatus remove) {
- db.delete(TABLE_NAME_DOWNLOAD_LOG, KEY_ID + "=?",
- new String[]{String.valueOf(remove.getId())});
- }
-
public void clearPlaybackHistory() {
ContentValues values = new ContentValues();
values.put(KEY_PLAYBACK_COMPLETION_DATE, 0);
@@ -967,13 +981,6 @@ public class PodDBAdapter {
return db.query(TABLE_NAME_FEEDS, new String[]{KEY_ID, KEY_DOWNLOAD_URL}, null, null, null, null, null);
}
- public final Cursor getExpiredFeedsCursor(long expirationTime) {
- Cursor c = db.query(TABLE_NAME_FEEDS, FEED_SEL_STD, KEY_LASTUPDATE + " < " + String.valueOf(System.currentTimeMillis() - expirationTime),
- null, null, null,
- null);
- return c;
- }
-
/**
* Returns a cursor with all FeedItems of a Feed. Uses FEEDITEM_SEL_FI_SMALL
*
@@ -1017,15 +1024,44 @@ public class PodDBAdapter {
}
/**
- * Returns a cursor for a DB query in the FeedImages table for a given ID.
+ * Returns a cursor for a DB query in the FeedImages table for given IDs.
*
- * @param id ID of the FeedImage
+ * @param imageIds IDs of the images
* @return The cursor of the query
*/
- public final Cursor getImageCursor(final long id) {
- Cursor c = db.query(TABLE_NAME_FEED_IMAGES, null, KEY_ID + "=?",
- new String[]{String.valueOf(id)}, null, null, null);
- return c;
+ public final Cursor getImageCursor(String... imageIds) {
+ int length = imageIds.length;
+ if (length > IN_OPERATOR_MAXIMUM) {
+ Log.w(TAG, "Length of id array is larger than "
+ + IN_OPERATOR_MAXIMUM + ". Creating multiple cursors");
+ int numCursors = (int) (((double) length) / (IN_OPERATOR_MAXIMUM)) + 1;
+ Cursor[] cursors = new Cursor[numCursors];
+ for (int i = 0; i < numCursors; i++) {
+ int neededLength;
+ String[] parts;
+ final int elementsLeft = length - i * IN_OPERATOR_MAXIMUM;
+
+ if (elementsLeft >= IN_OPERATOR_MAXIMUM) {
+ neededLength = IN_OPERATOR_MAXIMUM;
+ parts = Arrays.copyOfRange(imageIds, i
+ * IN_OPERATOR_MAXIMUM, (i + 1)
+ * IN_OPERATOR_MAXIMUM);
+ } else {
+ neededLength = elementsLeft;
+ parts = Arrays.copyOfRange(imageIds, i
+ * IN_OPERATOR_MAXIMUM, (i * IN_OPERATOR_MAXIMUM)
+ + neededLength);
+ }
+
+ cursors[i] = db.rawQuery("SELECT * FROM "
+ + TABLE_NAME_FEED_IMAGES + " WHERE " + KEY_ID + " IN "
+ + buildInOperator(neededLength), parts);
+ }
+ return new MergeCursor(cursors);
+ } else {
+ return db.query(TABLE_NAME_FEED_IMAGES, null, KEY_ID + " IN "
+ + buildInOperator(length), imageIds, null, null, null);
+ }
}
public final Cursor getSimpleChaptersOfFeedItemCursor(final FeedItem item) {
@@ -1053,23 +1089,17 @@ public class PodDBAdapter {
/**
* Returns a cursor which contains all feed items in the queue. The returned
* cursor uses the FEEDITEM_SEL_FI_SMALL selection.
+ * cursor uses the FEEDITEM_SEL_FI_SMALL selection.
*/
public final Cursor getQueueCursor() {
- Object[] args = (Object[]) new String[]{
- SEL_FI_SMALL_STR + "," + TABLE_NAME_QUEUE + "." + KEY_ID,
+ Object[] args = new String[] {
+ SEL_FI_SMALL_STR,
TABLE_NAME_FEED_ITEMS, TABLE_NAME_QUEUE,
TABLE_NAME_FEED_ITEMS + "." + KEY_ID,
TABLE_NAME_QUEUE + "." + KEY_FEEDITEM,
- TABLE_NAME_QUEUE + "." + KEY_ID};
- String query = String.format(
- "SELECT %s FROM %s INNER JOIN %s ON %s=%s ORDER BY %s", args);
+ TABLE_NAME_QUEUE + "." + KEY_ID };
+ String query = String.format("SELECT %s FROM %s INNER JOIN %s ON %s=%s ORDER BY %s", args);
Cursor c = db.rawQuery(query, null);
- /*
- * Cursor c = db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL,
- * "INNER JOIN ? ON ?=?", new String[] { TABLE_NAME_QUEUE,
- * TABLE_NAME_FEED_ITEMS + "." + KEY_ID, TABLE_NAME_QUEUE + "." +
- * KEY_FEEDITEM }, null, null, TABLE_NAME_QUEUE + "." + KEY_FEEDITEM);
- */
return c;
}
@@ -1078,51 +1108,58 @@ public class PodDBAdapter {
return c;
}
+
+ public final Cursor getFavoritesCursor() {
+ Object[] args = new String[] {
+ SEL_FI_SMALL_STR,
+ TABLE_NAME_FEED_ITEMS, TABLE_NAME_FAVORITES,
+ TABLE_NAME_FEED_ITEMS + "." + KEY_ID,
+ TABLE_NAME_FAVORITES + "." + KEY_FEEDITEM,
+ TABLE_NAME_FEED_ITEMS + "." + KEY_PUBDATE };
+ String query = String.format("SELECT %s FROM %s INNER JOIN %s ON %s=%s ORDER BY %s DESC", args);
+ Cursor c = db.rawQuery(query, null);
+ return c;
+ }
+
/**
* Returns a cursor which contains all feed items in the unread items list.
* The returned cursor uses the FEEDITEM_SEL_FI_SMALL selection.
*/
public final Cursor getUnreadItemsCursor() {
Cursor c = db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, KEY_READ
- + "=0", null, null, null, KEY_PUBDATE + " DESC");
+ + "<" + FeedItem.PLAYED, null, null, null, KEY_PUBDATE + " DESC");
return c;
}
- public final Cursor getNewItemIdsCursor() {
- final String query = "SELECT " + TABLE_NAME_FEED_ITEMS + "." + KEY_ID
+ /**
+ * Returns a cursor which contains all items of a feed that are considered new.
+ * The returned cursor uses the FEEDITEM_SEL_FI_SMALL selection.
+ */
+ public final Cursor getNewItemsIdsCursor(long feedId) {
+ final String query = "SELECT " + KEY_ID
+ " FROM " + TABLE_NAME_FEED_ITEMS
- + " INNER JOIN " + TABLE_NAME_FEED_MEDIA + " ON "
- + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + "="
- + TABLE_NAME_FEED_MEDIA + "." + KEY_FEEDITEM
- + " LEFT OUTER JOIN " + TABLE_NAME_QUEUE + " ON "
- + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + "="
- + TABLE_NAME_QUEUE + "." + KEY_FEEDITEM
- + " WHERE "
- + TABLE_NAME_FEED_ITEMS + "." + KEY_READ + " = 0 AND " // unplayed
- + TABLE_NAME_FEED_MEDIA + "." + KEY_DOWNLOADED + " = 0 AND " // undownloaded
- + TABLE_NAME_FEED_MEDIA + "." + KEY_POSITION + " = 0 AND " // not partially played
- + TABLE_NAME_QUEUE + "." + KEY_ID + " IS NULL"; // not in queue
- return db.rawQuery(query, null);
+ + " WHERE " + KEY_FEED + "=" + feedId
+ + " AND " + KEY_READ + "=" + FeedItem.NEW
+ + " ORDER BY " + KEY_PUBDATE + " DESC";
+ Cursor c = db.rawQuery(query, null);
+ return c;
}
/**
* Returns a cursor which contains all feed items that are considered new.
+ * Excludes those feeds that do not have 'Keep Updated' enabled.
* The returned cursor uses the FEEDITEM_SEL_FI_SMALL selection.
*/
public final Cursor getNewItemsCursor() {
- final String query = "SELECT " + SEL_FI_SMALL_STR + " FROM " + TABLE_NAME_FEED_ITEMS
- + " INNER JOIN " + TABLE_NAME_FEED_MEDIA + " ON "
- + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + "="
- + TABLE_NAME_FEED_MEDIA + "." + KEY_FEEDITEM
- + " LEFT OUTER JOIN " + TABLE_NAME_QUEUE + " ON "
- + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + "="
- + TABLE_NAME_QUEUE + "." + KEY_FEEDITEM
- + " WHERE "
- + TABLE_NAME_FEED_ITEMS + "." + KEY_READ + " = 0 AND " // unplayed
- + TABLE_NAME_FEED_MEDIA + "." + KEY_DOWNLOADED + " = 0 AND " // undownloaded
- + TABLE_NAME_FEED_MEDIA + "." + KEY_POSITION + " = 0 AND " // not partially played
- + TABLE_NAME_QUEUE + "." + KEY_ID + " IS NULL" // not in queue
- + " ORDER BY " + KEY_PUBDATE + " DESC";
+ String[] args = new String[] {
+ SEL_FI_SMALL_STR,
+ TABLE_NAME_FEED_ITEMS,
+ TABLE_NAME_FEEDS,
+ TABLE_NAME_FEED_ITEMS + "." + KEY_FEED + "=" + TABLE_NAME_FEEDS + "." + KEY_ID,
+ TABLE_NAME_FEED_ITEMS + "." + KEY_READ + "=" + FeedItem.NEW + " AND " + TABLE_NAME_FEEDS + "." + KEY_KEEP_UPDATED + " > 0",
+ KEY_PUBDATE + " DESC"
+ };
+ final String query = String.format("SELECT %s FROM %s INNER JOIN %s ON %s WHERE %s ORDER BY %s", args);
Cursor c = db.rawQuery(query, null);
return c;
}
@@ -1133,11 +1170,11 @@ public class PodDBAdapter {
}
public Cursor getDownloadedItemsCursor() {
- final String query = "SELECT " + SEL_FI_SMALL_STR + " FROM " + TABLE_NAME_FEED_ITEMS
- + " INNER JOIN " + TABLE_NAME_FEED_MEDIA + " ON "
- + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + "="
- + TABLE_NAME_FEED_MEDIA + "." + KEY_FEEDITEM + " WHERE "
- + TABLE_NAME_FEED_MEDIA + "." + KEY_DOWNLOADED + ">0";
+ final String query = "SELECT " + SEL_FI_SMALL_STR
+ + " FROM " + TABLE_NAME_FEED_ITEMS
+ + " INNER JOIN " + TABLE_NAME_FEED_MEDIA
+ + " ON " + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + "=" + TABLE_NAME_FEED_MEDIA + "." + KEY_FEEDITEM
+ + " WHERE " + TABLE_NAME_FEED_MEDIA + "." + KEY_DOWNLOADED + ">0";
Cursor c = db.rawQuery(query, null);
return c;
}
@@ -1151,7 +1188,9 @@ public class PodDBAdapter {
* @throws IllegalArgumentException if limit < 0
*/
public final Cursor getCompletedMediaCursor(int limit) {
- Validate.isTrue(limit >= 0, "Limit must be >= 0");
+ if(limit < 0) {
+ throw new IllegalArgumentException("Limit must be >= 0");
+ }
Cursor c = db.query(TABLE_NAME_FEED_MEDIA, null,
KEY_PLAYBACK_COMPLETION_DATE + " > 0", null, null,
@@ -1163,26 +1202,26 @@ public class PodDBAdapter {
return db.query(TABLE_NAME_FEED_MEDIA, null, KEY_ID + "=?", new String[]{String.valueOf(id)}, null, null, null);
}
- public final Cursor getFeedMediaCursorByItemID(String... mediaIds) {
- int length = mediaIds.length;
+ public final Cursor getFeedMediaCursor(String... itemIds) {
+ int length = itemIds.length;
if (length > IN_OPERATOR_MAXIMUM) {
Log.w(TAG, "Length of id array is larger than "
+ IN_OPERATOR_MAXIMUM + ". Creating multiple cursors");
int numCursors = (int) (((double) length) / (IN_OPERATOR_MAXIMUM)) + 1;
Cursor[] cursors = new Cursor[numCursors];
for (int i = 0; i < numCursors; i++) {
- int neededLength = 0;
- String[] parts = null;
+ int neededLength;
+ String[] parts;
final int elementsLeft = length - i * IN_OPERATOR_MAXIMUM;
if (elementsLeft >= IN_OPERATOR_MAXIMUM) {
neededLength = IN_OPERATOR_MAXIMUM;
- parts = Arrays.copyOfRange(mediaIds, i
+ parts = Arrays.copyOfRange(itemIds, i
* IN_OPERATOR_MAXIMUM, (i + 1)
* IN_OPERATOR_MAXIMUM);
} else {
neededLength = elementsLeft;
- parts = Arrays.copyOfRange(mediaIds, i
+ parts = Arrays.copyOfRange(itemIds, i
* IN_OPERATOR_MAXIMUM, (i * IN_OPERATOR_MAXIMUM)
+ neededLength);
}
@@ -1194,7 +1233,7 @@ public class PodDBAdapter {
return new MergeCursor(cursors);
} else {
return db.query(TABLE_NAME_FEED_MEDIA, null, KEY_FEEDITEM + " IN "
- + buildInOperator(length), mediaIds, null, null, null);
+ + buildInOperator(length), itemIds, null, null, null);
}
}
@@ -1237,25 +1276,31 @@ public class PodDBAdapter {
}
public final Cursor getFeedItemCursor(final String podcastUrl, final String episodeUrl) {
- final String query = "SELECT " + SEL_FI_SMALL_STR + " FROM " + TABLE_NAME_FEED_ITEMS
- + " INNER JOIN " +
- TABLE_NAME_FEEDS + " ON " + TABLE_NAME_FEED_ITEMS + "." + KEY_FEED + "=" +
- TABLE_NAME_FEEDS + "." + KEY_ID + " WHERE " + TABLE_NAME_FEED_ITEMS + "." + KEY_ITEM_IDENTIFIER + "='" +
- episodeUrl + "' AND " + TABLE_NAME_FEEDS + "." + KEY_DOWNLOAD_URL + "='" + podcastUrl + "'";
+ String downloadUrl = DatabaseUtils.sqlEscapeString(podcastUrl);
+ String itemIdentifier = DatabaseUtils.sqlEscapeString(episodeUrl);
+ final String query = ""
+ + "SELECT " + SEL_FI_SMALL_STR + " FROM " + TABLE_NAME_FEED_ITEMS
+ + " INNER JOIN " + TABLE_NAME_FEEDS
+ + " ON " + TABLE_NAME_FEED_ITEMS + "." + KEY_FEED + "=" + TABLE_NAME_FEEDS + "." + KEY_ID
+ + " WHERE " + TABLE_NAME_FEED_ITEMS + "." + KEY_ITEM_IDENTIFIER + "=" + itemIdentifier
+ + " AND " + TABLE_NAME_FEEDS + "." + KEY_DOWNLOAD_URL + "=" + downloadUrl;
return db.rawQuery(query, null);
}
public Cursor getImageAuthenticationCursor(final String imageUrl) {
- final String query = "SELECT " + KEY_USERNAME + "," + KEY_PASSWORD + " FROM "
- + TABLE_NAME_FEED_IMAGES + " INNER JOIN " + TABLE_NAME_FEEDS + " ON " +
- TABLE_NAME_FEED_IMAGES + "." + KEY_ID + "=" + TABLE_NAME_FEEDS + "." + KEY_IMAGE + " WHERE "
- + TABLE_NAME_FEED_IMAGES + "." + KEY_DOWNLOAD_URL + "='" + imageUrl + "' UNION SELECT "
- + KEY_USERNAME + "," + KEY_PASSWORD + " FROM " + TABLE_NAME_FEED_IMAGES + " INNER JOIN "
- + TABLE_NAME_FEED_ITEMS + " ON " + TABLE_NAME_FEED_IMAGES + "." + KEY_ID + "=" +
- TABLE_NAME_FEED_ITEMS + "." + KEY_IMAGE + " INNER JOIN " + TABLE_NAME_FEEDS + " ON "
- + TABLE_NAME_FEED_ITEMS + "." + KEY_FEED + "=" + TABLE_NAME_FEEDS + "." + KEY_ID + " WHERE "
- + TABLE_NAME_FEED_IMAGES + "." + KEY_DOWNLOAD_URL + "='" + imageUrl + "'";
- Log.d(TAG, "Query: " + query);
+ 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
+ + " 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;
return db.rawQuery(query, null);
}
@@ -1271,19 +1316,9 @@ public class PodDBAdapter {
}
public final int getNumberOfNewItems() {
- final String query = "SELECT COUNT(" + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + ")"
- +" FROM " + TABLE_NAME_FEED_ITEMS
- + " LEFT JOIN " + TABLE_NAME_FEED_MEDIA + " ON "
- + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + "="
- + TABLE_NAME_FEED_MEDIA + "." + KEY_FEEDITEM
- + " LEFT JOIN " + TABLE_NAME_QUEUE + " ON "
- + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + "="
- + TABLE_NAME_QUEUE + "." + KEY_FEEDITEM
- + " WHERE "
- + TABLE_NAME_FEED_ITEMS + "." + KEY_READ + " = 0 AND " // unplayed
- + TABLE_NAME_FEED_MEDIA + "." + KEY_DOWNLOADED + " = 0 AND " // undownloaded
- + TABLE_NAME_FEED_MEDIA + "." + KEY_POSITION + " = 0 AND " // not partially played
- + TABLE_NAME_QUEUE + "." + KEY_ID + " IS NULL"; // not in queue
+ final String query = "SELECT COUNT(" + KEY_ID + ")"
+ + " FROM " + TABLE_NAME_FEED_ITEMS
+ + " WHERE " + KEY_READ + "=" + FeedItem.NEW;
Cursor c = db.rawQuery(query, null);
int result = 0;
if (c.moveToFirst()) {
@@ -1293,12 +1328,37 @@ public class PodDBAdapter {
return result;
}
- public final LongIntMap getNumberOfUnreadFeedItems(long... feedIds) {
+ public final LongIntMap getFeedCounters(long... feedIds) {
+ int setting = UserPreferences.getFeedCounterSetting();
+ String whereRead;
+ if(setting == UserPreferences.FEED_COUNTER_SHOW_NEW_UNPLAYED_SUM) {
+ whereRead = "(" + KEY_READ + "=" + FeedItem.NEW
+ + " OR " + KEY_READ + "=" + FeedItem.UNPLAYED + ")";
+ } else if(setting == UserPreferences.FEED_COUNTER_SHOW_NEW) {
+ whereRead = KEY_READ + "=" + FeedItem.NEW;
+ } else if(setting == UserPreferences.FEED_COUNTER_SHOW_UNPLAYED) {
+ whereRead = KEY_READ + "=" + FeedItem.UNPLAYED;
+ } else { // NONE
+ return new LongIntMap(0);
+ }
+
+ // work around TextUtils.join wanting only boxed items
+ // and StringUtils.join() causing NoSuchMethodErrors on MIUI
+ StringBuilder builder = new StringBuilder();
+ for (long id : feedIds) {
+ builder.append(id);
+ builder.append(',');
+ }
+ if (feedIds.length > 0) {
+ // there's an extra ',', get rid of it
+ builder.deleteCharAt(builder.length() - 1);
+ }
+
final String query = "SELECT " + KEY_FEED + ", COUNT(" + KEY_ID + ") AS count "
+ " FROM " + TABLE_NAME_FEED_ITEMS
- + " WHERE " + KEY_FEED + " IN (" + StringUtils.join(feedIds, ',') + ") "
- + " AND " + KEY_READ + " = 0"
- + " GROUP BY " + KEY_FEED;
+ + " WHERE " + KEY_FEED + " IN (" + builder.toString() + ") "
+ + " AND " + whereRead + " GROUP BY " + KEY_FEED;
+
Cursor c = db.rawQuery(query, null);
LongIntMap result = new LongIntMap(c.getCount());
if (c.moveToFirst()) {
@@ -1451,17 +1511,22 @@ public class PodDBAdapter {
* Helper class for opening the Antennapod database.
*/
private static class PodDBHelper extends SQLiteOpenHelper {
+
+ private final static int VERSION = 1050003;
+
+ private Context context;
+
/**
* Constructor.
*
* @param context Context to use
* @param name Name of the database
* @param factory to use for creating cursor objects
- * @param version number of the database
*/
public PodDBHelper(final Context context, final String name,
- final CursorFactory factory, final int version) {
- super(context, name, factory, version);
+ final CursorFactory factory) {
+ super(context, name, factory, VERSION);
+ this.context = context;
}
@Override
@@ -1473,9 +1538,12 @@ public class PodDBAdapter {
db.execSQL(CREATE_TABLE_DOWNLOAD_LOG);
db.execSQL(CREATE_TABLE_QUEUE);
db.execSQL(CREATE_TABLE_SIMPLECHAPTERS);
+ 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);
db.execSQL(CREATE_INDEX_QUEUE_FEEDITEM);
db.execSQL(CREATE_INDEX_SIMPLECHAPTERS_FEEDITEM);
@@ -1485,7 +1553,259 @@ public class PodDBAdapter {
@Override
public void onUpgrade(final SQLiteDatabase db, final int oldVersion,
final int newVersion) {
- ClientConfig.storageCallbacks.onUpgrade(db, oldVersion, newVersion);
+ 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");
+ }
+
+ EventBus.getDefault().post(ProgressEvent.end());
}
}
}
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 413a11f8e..9280db8a3 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
@@ -1,8 +1,8 @@
package de.danoeh.antennapod.core.syndication.handler;
+import android.support.v4.util.ArrayMap;
+
import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Stack;
@@ -32,7 +32,7 @@ public class HandlerState {
/**
* Namespaces that have been defined so far.
*/
- protected HashMap<String, Namespace> namespaces;
+ protected Map<String, Namespace> namespaces;
protected Stack<Namespace> defaultNamespaces;
/**
* Buffer for saving characters.
@@ -42,16 +42,16 @@ public class HandlerState {
/**
* Temporarily saved objects.
*/
- protected HashMap<String, Object> tempObjects;
+ protected Map<String, Object> tempObjects;
public HandlerState(Feed feed) {
this.feed = feed;
- alternateUrls = new LinkedHashMap<String, String>();
+ alternateUrls = new ArrayMap<>();
items = new ArrayList<FeedItem>();
tagstack = new Stack<SyndElement>();
- namespaces = new HashMap<String, Namespace>();
+ namespaces = new ArrayMap<>();
defaultNamespaces = new Stack<Namespace>();
- tempObjects = new HashMap<String, Object>();
+ tempObjects = new ArrayMap<>();
}
public Feed getFeed() {
@@ -105,7 +105,7 @@ public class HandlerState {
alternateUrls.put(url, title);
}
- public HashMap<String, Object> getTempObjects() {
+ public Map<String, Object> getTempObjects() {
return tempObjects;
}
}
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 32cd538d5..4d56e1365 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
@@ -1,8 +1,7 @@
package de.danoeh.antennapod.core.syndication.handler;
import android.util.Log;
-import de.danoeh.antennapod.core.BuildConfig;
-import de.danoeh.antennapod.core.feed.Feed;
+
import org.apache.commons.io.input.XmlStreamReader;
import org.jsoup.Jsoup;
import org.xmlpull.v1.XmlPullParser;
@@ -14,6 +13,8 @@ import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.Reader;
+import de.danoeh.antennapod.core.feed.Feed;
+
/** Gets the type of a specific feed by reading the root element. */
public class TypeGetter {
private static final String TAG = "TypeGetter";
@@ -28,11 +29,13 @@ public class TypeGetter {
public Type getType(Feed feed) throws UnsupportedFeedtypeException {
XmlPullParserFactory factory;
if (feed.getFile_url() != null) {
+ Reader reader = null;
try {
factory = XmlPullParserFactory.newInstance();
factory.setNamespaceAware(true);
XmlPullParser xpp = factory.newPullParser();
- xpp.setInput(createReader(feed));
+ reader = createReader(feed);
+ xpp.setInput(reader);
int eventType = xpp.getEventType();
while (eventType != XmlPullParser.END_DOCUMENT) {
@@ -40,38 +43,30 @@ public class TypeGetter {
String tag = xpp.getName();
if (tag.equals(ATOM_ROOT)) {
feed.setType(Feed.TYPE_ATOM1);
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Recognized type Atom");
+ Log.d(TAG, "Recognized type Atom");
return Type.ATOM;
} else if (tag.equals(RSS_ROOT)) {
- String strVersion = xpp.getAttributeValue(null,
- "version");
+ String strVersion = xpp.getAttributeValue(null, "version");
if (strVersion != null) {
-
if (strVersion.equals("2.0")) {
feed.setType(Feed.TYPE_RSS2);
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Recognized type RSS 2.0");
+ Log.d(TAG, "Recognized type RSS 2.0");
return Type.RSS20;
} else if (strVersion.equals("0.91")
|| strVersion.equals("0.92")) {
- if (BuildConfig.DEBUG)
- Log.d(TAG,
- "Recognized type RSS 0.91/0.92");
+ Log.d(TAG, "Recognized type RSS 0.91/0.92");
return Type.RSS091;
}
}
throw new UnsupportedFeedtypeException(Type.INVALID);
} else {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Type is invalid");
+ Log.d(TAG, "Type is invalid");
throw new UnsupportedFeedtypeException(Type.INVALID, tag);
}
} else {
eventType = xpp.next();
}
}
-
} catch (XmlPullParserException e) {
e.printStackTrace();
// XML document might actually be a HTML document -> try to parse as HTML
@@ -88,10 +83,17 @@ public class TypeGetter {
} catch (IOException e) {
e.printStackTrace();
+ } finally {
+ if(reader != null) {
+ try {
+ reader.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
}
}
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Type is invalid");
+ Log.d(TAG, "Type is invalid");
throw new UnsupportedFeedtypeException(Type.INVALID);
}
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 ff9828eba..99c4cd67a 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
@@ -1,11 +1,14 @@
package de.danoeh.antennapod.core.syndication.namespace;
-import de.danoeh.antennapod.core.feed.FeedImage;
-import de.danoeh.antennapod.core.syndication.handler.HandlerState;
+import android.text.TextUtils;
+
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 {
public static final String NSTAG = "itunes";
public static final String NSURI = "http://www.itunes.com/dtds/podcast-1.0.dtd";
@@ -16,6 +19,8 @@ public class NSITunes extends Namespace {
private static final String AUTHOR = "author";
public static final String DURATION = "duration";
+ public static final String SUBTITLE = "subtitle";
+ public static final String SUMMARY = "summary";
@Override
@@ -34,7 +39,8 @@ public class NSITunes extends Namespace {
} else {
// this is the feed image
- if (state.getFeed().getImage() == null) {
+ // prefer to all other images
+ if(!TextUtils.isEmpty(image.getDownload_url())) {
image.setOwner(state.getFeed());
state.getFeed().setImage(image);
}
@@ -63,13 +69,28 @@ public class NSITunes extends Namespace {
} else {
return;
}
-
state.getTempObjects().put(DURATION, duration);
} catch (NumberFormatException e) {
e.printStackTrace();
}
+ } else if (localName.equals(SUBTITLE)) {
+ String subtitle = state.getContentBuf().toString();
+ if (state.getCurrentItem() != null) {
+ if (TextUtils.isEmpty(state.getCurrentItem().getDescription())) {
+ state.getCurrentItem().setDescription(subtitle);
+ }
+ } else {
+ if (TextUtils.isEmpty(state.getFeed().getDescription())) {
+ state.getFeed().setDescription(subtitle);
+ }
+ }
+ } else if (localName.equals(SUMMARY)) {
+ String summary = state.getContentBuf().toString();
+ if (state.getCurrentItem() != null) {
+ state.getCurrentItem().setDescription(summary);
+ } else {
+ state.getFeed().setDescription(summary);
+ }
}
-
}
-
}
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 6455332be..7e19213be 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
@@ -5,6 +5,7 @@ import android.util.Log;
import org.xml.sax.Attributes;
import de.danoeh.antennapod.core.BuildConfig;
+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;
@@ -57,6 +58,10 @@ public class NSRSS20 extends Namespace {
long size = 0;
try {
size = Long.parseLong(attributes.getValue(ENC_LEN));
+ if(size < 16384) {
+ // less than 16kb is suspicious, check manually
+ size = 0;
+ }
} catch (NumberFormatException e) {
if (BuildConfig.DEBUG)
Log.d(TAG, "Length attribute could not be parsed.");
@@ -69,8 +74,11 @@ public class NSRSS20 extends Namespace {
if (state.getTagstack().size() >= 1) {
String parent = state.getTagstack().peek().getName();
if (parent.equals(CHANNEL)) {
- state.getFeed().setImage(new FeedImage());
- state.getFeed().getImage().setOwner(state.getFeed());
+ Feed feed = state.getFeed();
+ if(feed.getImage() == null) {
+ feed.setImage(new FeedImage());
+ feed.getImage().setOwner(state.getFeed());
+ }
}
}
}
@@ -115,13 +123,16 @@ public class NSRSS20 extends Namespace {
state.getCurrentItem().setItemIdentifier(content);
}
} else if (top.equals(TITLE)) {
+ String title = content.trim();
if (second.equals(ITEM)) {
- state.getCurrentItem().setTitle(content);
+ state.getCurrentItem().setTitle(title);
} else if (second.equals(CHANNEL)) {
- state.getFeed().setTitle(content);
+ state.getFeed().setTitle(title);
} else if (second.equals(IMAGE) && third != null
&& third.equals(CHANNEL)) {
- state.getFeed().getImage().setTitle(content);
+ if(state.getFeed().getImage().getTitle() == null) {
+ state.getFeed().getImage().setTitle(title);
+ }
}
} else if (top.equals(LINK)) {
if (second.equals(CHANNEL)) {
@@ -134,7 +145,9 @@ public class NSRSS20 extends Namespace {
DateUtils.parse(content));
} else if (top.equals(URL) && second.equals(IMAGE) && third != null
&& third.equals(CHANNEL)) {
- state.getFeed().getImage().setDownload_url(content);
+ if(state.getFeed().getImage().getDownload_url() == null) { // prefer itunes:image
+ state.getFeed().getImage().setDownload_url(content);
+ }
} else if (localName.equals(DESCR)) {
if (second.equals(CHANNEL)) {
state.getFeed().setDescription(content);
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 abff5b2db..b23a142af 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
@@ -4,7 +4,6 @@ import android.util.Log;
import org.xml.sax.Attributes;
-import de.danoeh.antennapod.core.BuildConfig;
import de.danoeh.antennapod.core.feed.FeedImage;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia;
@@ -88,16 +87,15 @@ public class NSAtom extends Namespace {
size = Long.parseLong(strSize);
}
} catch (NumberFormatException e) {
- if (BuildConfig.DEBUG) Log.d(TAG, "Length attribute could not be parsed.");
+ Log.d(TAG, "Length attribute could not be parsed.");
}
String type = attributes.getValue(LINK_TYPE);
if (SyndTypeUtils.enclosureTypeValid(type)
- || (type = SyndTypeUtils
- .getValidMimeTypeFromUrl(href)) != null) {
- state.getCurrentItem().setMedia(
- new FeedMedia(state.getCurrentItem(), href,
- size, type)
- );
+ || (type = SyndTypeUtils.getValidMimeTypeFromUrl(href)) != null) {
+ FeedItem currItem = state.getCurrentItem();
+ if(!currItem.hasMedia()) {
+ currItem.setMedia(new FeedMedia(currItem, href, size, type));
+ }
}
} else if (rel.equals(LINK_REL_PAYMENT)) {
state.getCurrentItem().setPaymentLink(href);
@@ -111,9 +109,11 @@ public class NSAtom extends Namespace {
* LINK_TYPE_HTML or LINK_TYPE_XHTML
*/
if ((type == null && state.getFeed().getLink() == null)
- || (type != null && (type.equals(LINK_TYPE_HTML) || type.equals(LINK_TYPE_XHTML)))) {
+ || (type != null && (type.equals(LINK_TYPE_HTML)
+ || type.equals(LINK_TYPE_XHTML)))) {
state.getFeed().setLink(href);
- } else if (type != null && (type.equals(LINK_TYPE_ATOM) || type.equals(LINK_TYPE_RSS))) {
+ } else if (type != null && (type.equals(LINK_TYPE_ATOM)
+ || type.equals(LINK_TYPE_RSS))) {
// treat as podlove alternate feed
String title = attributes.getValue(LINK_TITLE);
if (title == null) {
@@ -199,7 +199,9 @@ public class NSAtom extends Namespace {
DateUtils.parse(content));
}
} else if (top.equals(IMAGE)) {
- state.getFeed().setImage(new FeedImage(state.getFeed(), content, null));
+ if(state.getFeed().getImage() == null) {
+ state.getFeed().setImage(new FeedImage(state.getFeed(), content, null));
+ }
}
}
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 a0b514bd6..1b929b214 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
@@ -1,7 +1,10 @@
package de.danoeh.antennapod.core.util;
+import android.content.Context;
import android.util.Log;
+import de.danoeh.antennapod.core.R;
+
/** Provides methods for converting various units. */
public final class Converter {
/** Class shall not be instantiated. */
@@ -23,7 +26,7 @@ public final class Converter {
/** Determines the length of the number for best readability.*/
private static final int NUM_LENGTH = 1024;
-
+ private static final int DAYS_MIL = 86400000;
private static final int HOURS_MIL = 3600000;
private static final int MINUTES_MIL = 60000;
private static final int SECONDS_MIL = 1000;
@@ -99,5 +102,21 @@ public final class Converter {
return Integer.valueOf(parts[0]) * 3600 * 1000 +
Integer.valueOf(parts[1]) * 1000 * 60;
}
+
+ /** Converts milliseconds to a localized string containing hours and minutes */
+ public static String getDurationStringLocalized(Context context, long duration) {
+ int h = (int)(duration / HOURS_MIL);
+ int rest = (int)(duration - h * HOURS_MIL);
+ int m = rest / MINUTES_MIL;
+
+ String result = "";
+ if(h > 0) {
+ String hours = context.getResources().getQuantityString(R.plurals.time_hours_quantified, h, h);
+ result += hours + " ";
+ }
+ String minutes = context.getResources().getQuantityString(R.plurals.time_minutes_quantified, m, m);
+ result += minutes;
+ return result;
+ }
}
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 b6df2dc85..4b4201b50 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/DateUtils.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/DateUtils.java
@@ -1,5 +1,6 @@
package de.danoeh.antennapod.core.util;
+import android.content.Context;
import android.util.Log;
import org.apache.commons.lang3.StringUtils;
@@ -7,7 +8,9 @@ import org.apache.commons.lang3.StringUtils;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.Date;
+import java.util.GregorianCalendar;
import java.util.Locale;
+import java.util.TimeZone;
/**
* Parses several date formats.
@@ -16,60 +19,88 @@ public class DateUtils {
private static final String TAG = "DateUtils";
+ private static final SimpleDateFormat parser = new SimpleDateFormat("", Locale.US);
+ private static final TimeZone defaultTimezone = TimeZone.getTimeZone("GMT");
+
+ static {
+ parser.setLenient(false);
+ parser.setTimeZone(defaultTimezone);
+ }
+
public static Date parse(final String input) {
if(input == null) {
- throw new IllegalArgumentException("Date most not be null");
+ throw new IllegalArgumentException("Date must not be null");
}
- String date = input.replace('/', '-');
+ String date = input.trim().replace('/', '-').replaceAll("( ){2,}+", " ");
+
+ // if datetime is more precise than seconds, make sure the value is in ms
if(date.contains(".")) {
int start = date.indexOf('.');
int current = start+1;
while(current < date.length() && Character.isDigit(date.charAt(current))) {
current++;
}
+ // even more precise than microseconds: discard further decimal places
if(current - start > 4) {
if(current < date.length()-1) {
date = date.substring(0, start + 4) + date.substring(current);
} else {
date = date.substring(0, start + 4);
}
+ // less than 4 decimal places: pad to have a consistent format for the parser
} else if(current - start < 4) {
if(current < date.length()-1) {
date = date.substring(0, current) + StringUtils.repeat("0", 4-(current-start)) + date.substring(current);
} else {
date = date.substring(0, current) + StringUtils.repeat("0", 4-(current-start));
}
-
}
}
String[] patterns = {
"dd MMM yy HH:mm:ss Z",
"dd MMM yy HH:mm Z",
"EEE, dd MMM yyyy HH:mm:ss Z",
+ "EEE, dd MMM yyyy HH:mm:ss",
"EEE, dd MMMM yyyy HH:mm:ss Z",
+ "EEE, dd MMMM yyyy HH:mm:ss",
+ "EEEE, dd MMM yyyy HH:mm:ss Z",
"EEEE, dd MMM yy HH:mm:ss Z",
+ "EEEE, dd MMM yyyy HH:mm:ss",
+ "EEEE, dd MMM yy HH:mm:ss",
"EEE MMM d HH:mm:ss yyyy",
+ "EEE, dd MMM yyyy HH:mm Z",
+ "EEE, dd MMM yyyy HH:mm",
+ "EEE, dd MMMM yyyy HH:mm Z",
+ "EEE, dd MMMM yyyy HH:mm",
+ "EEEE, dd MMM yyyy HH:mm Z",
+ "EEEE, dd MMM yy HH:mm Z",
+ "EEEE, dd MMM yyyy HH:mm",
+ "EEEE, dd MMM yy HH:mm",
+ "EEE MMM d HH:mm yyyy",
"yyyy-MM-dd'T'HH:mm:ss",
- "yyyy-MM-dd'T'HH:mm:ss.SSS",
"yyyy-MM-dd'T'HH:mm:ss.SSS Z",
+ "yyyy-MM-dd'T'HH:mm:ss.SSS",
"yyyy-MM-dd'T'HH:mm:ssZ",
"yyyy-MM-dd'T'HH:mm:ss'Z'",
"yyyy-MM-ddZ",
"yyyy-MM-dd"
};
- SimpleDateFormat parser = new SimpleDateFormat("", Locale.US);
- parser.setLenient(false);
+
ParsePosition pos = new ParsePosition(0);
for(String pattern : patterns) {
parser.applyPattern(pattern);
pos.setIndex(0);
- Date result = parser.parse(date, pos);
- if(result != null && pos.getIndex() == date.length()) {
- return result;
+ try {
+ Date result = parser.parse(date, pos);
+ if (result != null && pos.getIndex() == date.length()) {
+ return result;
+ }
+ } catch(Exception e) {
+ Log.e(TAG, Log.getStackTraceString(e));
}
}
- Log.d(TAG, "Could not parse date string \"" + input + "\"");
+ Log.d(TAG, "Could not parse date string \"" + input + "\" [" + date + "]");
return null;
}
@@ -109,6 +140,20 @@ public class DateUtils {
public static String formatRFC3339UTC(Date date) {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
+ format.setTimeZone(defaultTimezone);
return format.format(date);
}
+
+ public static String formatAbbrev(final Context context, final Date date) {
+ GregorianCalendar cal = new GregorianCalendar();
+ cal.add(GregorianCalendar.YEAR, -1);
+ // some padding, because no one really remembers what day of the month it is
+ cal.add(GregorianCalendar.DAY_OF_MONTH, 10);
+ boolean withinLastYear = date.after(cal.getTime());
+ int format = android.text.format.DateUtils.FORMAT_ABBREV_ALL;
+ if(withinLastYear) {
+ format |= android.text.format.DateUtils.FORMAT_NO_YEAR;
+ }
+ return android.text.format.DateUtils.formatDateTime(context, date.getTime(), format);
+ }
}
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
new file mode 100644
index 000000000..892e5ff38
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/FeedItemUtil.java
@@ -0,0 +1,78 @@
+package de.danoeh.antennapod.core.util;
+
+import java.util.List;
+
+import de.danoeh.antennapod.core.feed.FeedItem;
+
+public class FeedItemUtil {
+
+ public static int indexOfItemWithDownloadUrl(List<FeedItem> items, String downloadUrl) {
+ if(items == null) {
+ return -1;
+ }
+ for(int i=0; i < items.size(); i++) {
+ FeedItem item = items.get(i);
+ if(item.hasMedia() && item.getMedia().getDownload_url().equals(downloadUrl)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ public static int indexOfItemWithId(List<FeedItem> items, long id) {
+ for(int i=0; i < items.size(); i++) {
+ FeedItem item = items.get(i);
+ if(item != null && item.getId() == id) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ public static int indexOfItemWithMediaId(List<FeedItem> items, long mediaId) {
+ for(int i=0; i < items.size(); i++) {
+ FeedItem item = items.get(i);
+ if(item != null && item.getMedia() != null && item.getMedia().getId() == mediaId) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ public static long[] getIds(FeedItem... items) {
+ if(items == null || items.length == 0) {
+ return new long[0];
+ }
+ long[] result = new long[items.length];
+ for(int i=0; i < items.length; i++) {
+ result[i] = items[i].getId();
+ }
+ return result;
+ }
+
+ public static long[] getIds(List<FeedItem> items) {
+ if(items == null || items.size() == 0) {
+ return new long[0];
+ }
+ long[] result = new long[items.size()];
+ for(int i=0; i < items.size(); i++) {
+ result[i] = items.get(i).getId();
+ }
+ return result;
+ }
+
+ public static boolean containsAnyId(List<FeedItem> items, long[] ids) {
+ if(items == null || items.size() == 0) {
+ return false;
+ }
+ for(FeedItem item : items) {
+ for(long id : ids) {
+ if(item.getId() == id) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+}
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
new file mode 100644
index 000000000..2d5a6e5a1
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/IntentUtils.java
@@ -0,0 +1,18 @@
+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 java.util.List;
+
+public class IntentUtils {
+
+ public static boolean isCallable(final Context context, final Intent intent) {
+ List<ResolveInfo> list = context.getPackageManager().queryIntentActivities(intent,
+ PackageManager.MATCH_DEFAULT_ONLY);
+ return list.size() > 0;
+ }
+
+}
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 07432d28a..287ec4d0c 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
@@ -1,14 +1,15 @@
package de.danoeh.antennapod.core.util;
+import android.support.v4.util.ArrayMap;
+
import java.nio.charset.Charset;
-import java.util.HashMap;
public class LangUtils {
public static final Charset UTF_8 = Charset.forName("UTF-8");
- private static HashMap<String, String> languages;
+ private static ArrayMap<String, String> languages;
static {
- languages = new HashMap<String, String>();
+ languages = new ArrayMap<>();
languages.put("af", "Afrikaans");
languages.put("sq", "Albanian");
languages.put("sq", "Albanian");
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/LongList.java b/core/src/main/java/de/danoeh/antennapod/core/util/LongList.java
index 8934f3272..6ed8b820e 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/LongList.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/LongList.java
@@ -30,6 +30,17 @@ public final class LongList {
size = 0;
}
+ public static LongList of(long... values) {
+ if(values == null || values.length == 0) {
+ return new LongList(0);
+ }
+ LongList result = new LongList(values.length);
+ for(long value : values) {
+ result.add(value);
+ }
+ return result;
+ }
+
@Override
public int hashCode() {
int hashCode = 1;
@@ -166,6 +177,28 @@ public final class LongList {
}
/**
+ * Removes values from this list.
+ *
+ * @param values values to remove
+ */
+ public void removeAll(long[] values) {
+ for(long value : values) {
+ remove(value);
+ }
+ }
+
+ /**
+ * Removes values from this list.
+ *
+ * @param list List with values to remove
+ */
+ public void removeAll(LongList list) {
+ for(long value : list.values) {
+ remove(value);
+ }
+ }
+
+ /**
* Removes an element at a given index, shifting elements at greater
* indicies down one.
*
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 3a349e221..c2cd273b8 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
@@ -5,19 +5,35 @@ import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
+import android.text.TextUtils;
import android.util.Log;
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
+
+import java.io.File;
+import java.io.IOException;
import java.util.Arrays;
import java.util.List;
-import de.danoeh.antennapod.core.BuildConfig;
+import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.preferences.UserPreferences;
+import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
+import de.danoeh.antennapod.core.storage.DBWriter;
+import rx.Observable;
+import rx.Subscriber;
+import rx.android.schedulers.AndroidSchedulers;
+import rx.schedulers.Schedulers;
public class NetworkUtils {
- private static final String TAG = "NetworkUtils";
- private NetworkUtils() {
+ private static final String TAG = NetworkUtils.class.getSimpleName();
+
+ private static Context context;
+ public static void init(Context context) {
+ NetworkUtils.context = context;
}
/**
@@ -26,18 +42,16 @@ public class NetworkUtils {
* network that is on the 'selected networks' list of the Wi-Fi filter for
* automatic downloads and false otherwise.
* */
- public static boolean autodownloadNetworkAvailable(Context context) {
+ public static boolean autodownloadNetworkAvailable() {
ConnectivityManager cm = (ConnectivityManager) context
.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = cm.getActiveNetworkInfo();
if (networkInfo != null) {
if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Device is connected to Wi-Fi");
+ Log.d(TAG, "Device is connected to Wi-Fi");
if (networkInfo.isConnected()) {
if (!UserPreferences.isEnableAutodownloadWifiFilter()) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Auto-dl filter is disabled");
+ Log.d(TAG, "Auto-dl filter is disabled");
return true;
} else {
WifiManager wm = (WifiManager) context
@@ -48,31 +62,28 @@ public class NetworkUtils {
.getAutodownloadSelectedNetworks());
if (selectedNetworks.contains(Integer.toString(wifiInfo
.getNetworkId()))) {
- if (BuildConfig.DEBUG)
- Log.d(TAG,
- "Current network is on the selected networks list");
+ Log.d(TAG, "Current network is on the selected networks list");
return true;
}
}
}
}
}
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Network for auto-dl is not available");
+ Log.d(TAG, "Network for auto-dl is not available");
return false;
}
- public static boolean networkAvailable(Context context) {
+ public static boolean networkAvailable() {
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo info = cm.getActiveNetworkInfo();
return info != null && info.isConnected();
}
- public static boolean isDownloadAllowed(Context context) {
- return UserPreferences.isAllowMobileUpdate() || NetworkUtils.connectedToWifi(context);
+ public static boolean isDownloadAllowed() {
+ return UserPreferences.isAllowMobileUpdate() || NetworkUtils.connectedToWifi();
}
- public static boolean connectedToWifi(Context context) {
+ public static boolean connectedToWifi() {
ConnectivityManager connManager = (ConnectivityManager) context
.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo mWifi = connManager
@@ -81,4 +92,67 @@ public class NetworkUtils {
return mWifi.isConnected();
}
+ public static Observable<Long> getFeedMediaSizeObservable(FeedMedia media) {
+ return Observable.create(new Observable.OnSubscribe<Long>() {
+ @Override
+ public void call(Subscriber<? super Long> subscriber) {
+ if (false == 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 (false == 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));
+ }
+ }
+ } 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);
+ }
+ })
+ .subscribeOn(Schedulers.newThread())
+ .observeOn(AndroidSchedulers.mainThread());
+ }
+
}
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 9a1496b75..71d6040ba 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
@@ -83,7 +83,7 @@ public class QueueSorter {
}
if (comparator != null) {
- DBWriter.sortQueue(context, comparator, broadcastUpdate);
+ DBWriter.sortQueue(comparator, broadcastUpdate);
}
}
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/RewindAfterPauseUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/RewindAfterPauseUtils.java
new file mode 100644
index 000000000..ee306a401
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/RewindAfterPauseUtils.java
@@ -0,0 +1,47 @@
+package de.danoeh.antennapod.core.util;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * This class calculates the proper rewind time after the pause and resume.
+ * <p>
+ * User might loose context if he/she pauses and resumes the media after longer time.
+ * Media file should be "rewinded" x seconds after user resumes the playback.
+ */
+public class RewindAfterPauseUtils {
+
+ public static final long ELAPSED_TIME_FOR_SHORT_REWIND = TimeUnit.MINUTES.toMillis(1);
+ public static final long ELAPSED_TIME_FOR_MEDIUM_REWIND = TimeUnit.HOURS.toMillis(1);
+ public static final long ELAPSED_TIME_FOR_LONG_REWIND = TimeUnit.DAYS.toMillis(1);
+
+ public static final long SHORT_REWIND = TimeUnit.SECONDS.toMillis(3);
+ public static final long MEDIUM_REWIND = TimeUnit.SECONDS.toMillis(10);
+ public static final long LONG_REWIND = TimeUnit.SECONDS.toMillis(20);
+
+ /**
+ * @param currentPosition current position in a media file in ms
+ * @param lastPlayedTime timestamp when was media paused
+ * @return new rewinded position for playback in milliseconds
+ */
+ public static int calculatePositionWithRewind(int currentPosition, long lastPlayedTime) {
+ if (currentPosition > 0 && lastPlayedTime > 0) {
+ long elapsedTime = System.currentTimeMillis() - lastPlayedTime;
+ long rewindTime = 0;
+
+ if (elapsedTime > ELAPSED_TIME_FOR_LONG_REWIND) {
+ rewindTime = LONG_REWIND;
+ } else if (elapsedTime > ELAPSED_TIME_FOR_MEDIUM_REWIND) {
+ rewindTime = MEDIUM_REWIND;
+ } else if (elapsedTime > ELAPSED_TIME_FOR_SHORT_REWIND) {
+ rewindTime = SHORT_REWIND;
+ }
+
+ int newPosition = currentPosition - (int) rewindTime;
+
+ return newPosition > 0 ? newPosition : 0;
+ }
+ else {
+ return currentPosition;
+ }
+ }
+}
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 85f32ed50..35916a604 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,6 +2,8 @@ package de.danoeh.antennapod.core.util;
import android.content.Context;
import android.content.Intent;
+
+import de.danoeh.antennapod.core.R;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.FeedItem;
@@ -11,24 +13,49 @@ public class ShareUtils {
private ShareUtils() {}
- public static void shareLink(Context context, String link) {
+ public static void shareLink(Context context, String text) {
Intent i = new Intent(Intent.ACTION_SEND);
i.setType("text/plain");
- i.putExtra(Intent.EXTRA_SUBJECT, "Sharing URL");
- i.putExtra(Intent.EXTRA_TEXT, link);
- context.startActivity(Intent.createChooser(i, "Share URL"));
+ i.putExtra(Intent.EXTRA_TEXT, text);
+ context.startActivity(Intent.createChooser(i, context.getString(R.string.share_url_label)));
}
-
- public static void shareFeedItemLink(Context context, FeedItem item) {
- shareLink(context, item.getLink());
+
+ public static void shareFeedlink(Context context, Feed feed) {
+ shareLink(context, feed.getTitle() + ": " + feed.getLink());
}
public static void shareFeedDownloadLink(Context context, Feed feed) {
- shareLink(context, feed.getDownload_url());
+ shareLink(context, feed.getTitle() + ": " + feed.getDownload_url());
}
-
- public static void shareFeedlink(Context context, Feed feed) {
- shareLink(context, feed.getLink());
+
+ public static void shareFeedItemLink(Context context, FeedItem item) {
+ shareFeedItemLink(context, item, false);
+ }
+
+ public static void shareFeedItemDownloadLink(Context context, FeedItem item) {
+ shareFeedItemDownloadLink(context, item, false);
+ }
+
+ private static String getItemShareText(FeedItem item) {
+ return item.getFeed().getTitle() + ": " + item.getTitle();
+ }
+
+ public static void shareFeedItemLink(Context context, FeedItem item, boolean withPosition) {
+ String text = getItemShareText(item) + " " + item.getLink();
+ if(withPosition) {
+ int pos = item.getMedia().getPosition();
+ text = item.getLink() + " [" + Converter.getDurationStringLong(pos) + "]";
+ }
+ shareLink(context, text);
+ }
+
+ public static void shareFeedItemDownloadLink(Context context, FeedItem item, boolean withPosition) {
+ String text = getItemShareText(item) + " " + item.getMedia().getDownload_url();
+ if(withPosition) {
+ int pos = item.getMedia().getPosition();
+ text += " [" + Converter.getDurationStringLong(pos) + "]";
+ }
+ shareLink(context, text);
}
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/StorageUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/StorageUtils.java
index dea380937..1ef81bf64 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/StorageUtils.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/StorageUtils.java
@@ -1,14 +1,12 @@
package de.danoeh.antennapod.core.util;
import android.app.Activity;
-import android.content.Context;
import android.os.Build;
import android.os.StatFs;
import android.util.Log;
import java.io.File;
-import de.danoeh.antennapod.core.BuildConfig;
import de.danoeh.antennapod.core.ClientConfig;
import de.danoeh.antennapod.core.preferences.UserPreferences;
@@ -18,13 +16,12 @@ import de.danoeh.antennapod.core.preferences.UserPreferences;
public class StorageUtils {
private static final String TAG = "StorageUtils";
- public static boolean storageAvailable(Context context) {
- File dir = UserPreferences.getDataFolder(context, null);
+ public static boolean storageAvailable() {
+ File dir = UserPreferences.getDataFolder(null);
if (dir != null) {
return dir.exists() && dir.canRead() && dir.canWrite();
} else {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Storage not available: data folder is null");
+ Log.d(TAG, "Storage not available: data folder is null");
return false;
}
}
@@ -39,7 +36,7 @@ public class StorageUtils {
* @return true if external storage is available
*/
public static boolean checkStorageAvailability(Activity activity) {
- boolean storageAvailable = storageAvailable(activity);
+ boolean storageAvailable = storageAvailable();
if (!storageAvailable) {
activity.finish();
activity.startActivity(ClientConfig.applicationCallbacks.getStorageErrorActivity(activity));
@@ -51,8 +48,19 @@ public class StorageUtils {
* Get the number of free bytes that are available on the external storage.
*/
public static long getFreeSpaceAvailable() {
- StatFs stat = new StatFs(UserPreferences.getDataFolder(
- ClientConfig.applicationCallbacks.getApplicationInstance(), null).getAbsolutePath());
+ File dataFolder = UserPreferences.getDataFolder(null);
+ if (dataFolder != null) {
+ return getFreeSpaceAvailable(dataFolder.getAbsolutePath());
+ } else {
+ return 0;
+ }
+ }
+
+ /**
+ * Get the number of free bytes that are available on the external storage.
+ */
+ public static long getFreeSpaceAvailable(String path) {
+ StatFs stat = new StatFs(path);
long availableBlocks;
long blockSize;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/URLChecker.java b/core/src/main/java/de/danoeh/antennapod/core/util/URLChecker.java
index 4300556d2..415a1d3a2 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/URLChecker.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/URLChecker.java
@@ -3,8 +3,6 @@ package de.danoeh.antennapod.core.util;
import android.net.Uri;
import android.util.Log;
-import org.apache.commons.lang3.StringUtils;
-
import de.danoeh.antennapod.core.BuildConfig;
/**
@@ -32,19 +30,19 @@ public final class URLChecker {
* @return The prepared url
*/
public static String prepareURL(String url) {
- url = StringUtils.trim(url);
+ url = url.trim();
if (url.startsWith("feed://")) {
if (BuildConfig.DEBUG) Log.d(TAG, "Replacing feed:// with http://");
return url.replaceFirst("feed://", "http://");
} else if (url.startsWith("pcast://")) {
if (BuildConfig.DEBUG) Log.d(TAG, "Removing pcast://");
- return prepareURL(StringUtils.removeStart(url, "pcast://"));
+ return prepareURL(url.substring("pcast://".length()));
} else if (url.startsWith("itpc")) {
if (BuildConfig.DEBUG) Log.d(TAG, "Replacing itpc:// with http://");
return url.replaceFirst("itpc://", "http://");
} else if (url.startsWith(AP_SUBSCRIBE)) {
if (BuildConfig.DEBUG) Log.d(TAG, "Removing antennapod-subscribe://");
- return prepareURL(StringUtils.removeStart(url, AP_SUBSCRIBE));
+ return prepareURL(url.substring(AP_SUBSCRIBE.length()));
} else if (!(url.startsWith("http://") || url.startsWith("https://"))) {
if (BuildConfig.DEBUG) Log.d(TAG, "Adding http:// at the beginning of the URL");
return "http://" + url;
@@ -66,7 +64,7 @@ public final class URLChecker {
if (base == null) {
return prepareURL(url);
}
- url = StringUtils.trim(url);
+ url = url.trim();
base = prepareURL(base);
Uri urlUri = Uri.parse(url);
Uri baseUri = Uri.parse(base);
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 50792ae26..6ddfb0366 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
@@ -1,6 +1,5 @@
package de.danoeh.antennapod.core.util.flattr;
-import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
@@ -8,9 +7,10 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.preference.PreferenceManager;
+import android.support.v7.app.AlertDialog;
+import android.text.TextUtils;
import android.util.Log;
-import org.apache.commons.lang3.StringUtils;
import org.shredzone.flattr4j.FlattrService;
import org.shredzone.flattr4j.exception.FlattrException;
import org.shredzone.flattr4j.model.Flattr;
@@ -42,12 +42,6 @@ public class FlattrUtils {
private static final String PREF_ACCESS_TOKEN = "de.danoeh.antennapod.preference.flattrAccessToken";
- // Flattr URL for this app.
- public static final String APP_URL = "http://antennapod.com";
- // Human-readable flattr-page.
- public static final String APP_LINK = "https://flattr.com/thing/745609/";
- public static final String APP_THING_ID = "745609";
-
private static volatile AccessToken cachedToken;
private static AndroidAuthenticator createAuthenticator() {
@@ -84,8 +78,8 @@ public class FlattrUtils {
* Returns true if FLATTR_APP_KEY and FLATTR_APP_SECRET in BuildConfig are not null and not empty
*/
public static boolean hasAPICredentials() {
- return StringUtils.isNotEmpty(ClientConfig.flattrCallbacks.getFlattrAppKey())
- && StringUtils.isNotEmpty(ClientConfig.flattrCallbacks.getFlattrAppSecret());
+ return !TextUtils.isEmpty(ClientConfig.flattrCallbacks.getFlattrAppKey())
+ && !TextUtils.isEmpty(ClientConfig.flattrCallbacks.getFlattrAppSecret());
}
public static boolean hasToken() {
@@ -110,18 +104,6 @@ public class FlattrUtils {
storeToken(null);
}
- public static Thing getAppThing(Context context) {
- FlattrService fs = FlattrServiceCreator.getService(retrieveToken());
- try {
- Thing thing = fs.getThing(Thing.withId(APP_THING_ID));
- return thing;
- } catch (FlattrException e) {
- e.printStackTrace();
- showErrorDialog(context, e.getMessage());
- return null;
- }
- }
-
public static void clickUrl(Context context, String url)
throws FlattrException {
if (hasToken()) {
@@ -185,7 +167,7 @@ public class FlattrUtils {
deleteToken();
FlattrServiceCreator.deleteFlattrService();
showRevokeDialog(context);
- DBWriter.clearAllFlattrStatus(context);
+ DBWriter.clearAllFlattrStatus();
}
// ------------------------------------------------ DIALOGS
@@ -245,37 +227,6 @@ public class FlattrUtils {
}
}
- public static void showForbiddenDialog(final Context context,
- final String url) {
- AlertDialog.Builder builder = new AlertDialog.Builder(context);
- builder.setTitle(R.string.action_forbidden_title);
- builder.setMessage(R.string.action_forbidden_msg);
- builder.setPositiveButton(R.string.authenticate_now_label,
- new OnClickListener() {
-
- @Override
- public void onClick(DialogInterface dialog, int 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));
- }
-
- }
- );
- builder.create().show();
- }
-
public static void showErrorDialog(final Context context, final String msg) {
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle(R.string.error_label);
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/gui/UndoBarController.java b/core/src/main/java/de/danoeh/antennapod/core/util/gui/UndoBarController.java
deleted file mode 100644
index 26c712af3..000000000
--- a/core/src/main/java/de/danoeh/antennapod/core/util/gui/UndoBarController.java
+++ /dev/null
@@ -1,125 +0,0 @@
-package de.danoeh.antennapod.core.util.gui;
-
-import android.os.Handler;
-import android.view.View;
-import android.widget.TextView;
-
-import com.nineoldandroids.animation.Animator;
-import com.nineoldandroids.animation.AnimatorListenerAdapter;
-import com.nineoldandroids.view.ViewHelper;
-import com.nineoldandroids.view.ViewPropertyAnimator;
-
-import de.danoeh.antennapod.core.R;
-
-import static com.nineoldandroids.view.ViewPropertyAnimator.animate;
-
-public class UndoBarController<T> {
- private View mBarView;
- private TextView mMessageView;
- private ViewPropertyAnimator mBarAnimator;
- private Handler mHideHandler = new Handler();
-
- private UndoListener<T> mUndoListener;
-
- // State objects
- private T mUndoToken;
- private CharSequence mUndoMessage;
-
- public interface UndoListener<T> {
- /**
- * This callback function is called when the undo button is pressed
- *
- * @param token
- */
- void onUndo(T token);
-
- /**
- *
- * This callback function is called when the bar fades out without button press
- *
- * @param token
- */
- void onHide(T token);
- }
-
- public UndoBarController(View undoBarView, UndoListener<T> undoListener) {
- mBarView = undoBarView;
- mBarAnimator = animate(mBarView);
- mUndoListener = undoListener;
-
- mMessageView = (TextView) mBarView.findViewById(R.id.undobar_message);
- mBarView.findViewById(R.id.undobar_button)
- .setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- hideUndoBar(false);
- mUndoListener.onUndo(mUndoToken);
- }
- });
-
- hideUndoBar(true);
- }
-
- public void showUndoBar(boolean immediate, CharSequence message, T undoToken) {
- mUndoToken = undoToken;
- mUndoMessage = message;
- mMessageView.setText(mUndoMessage);
-
- mHideHandler.removeCallbacks(mHideRunnable);
- mHideHandler.postDelayed(mHideRunnable,
- mBarView.getResources().getInteger(R.integer.undobar_hide_delay));
-
- mBarView.setVisibility(View.VISIBLE);
- if (immediate) {
- ViewHelper.setAlpha(mBarView, 1);
- } else {
- mBarAnimator.cancel();
- mBarAnimator
- .alpha(1)
- .setDuration(
- mBarView.getResources()
- .getInteger(android.R.integer.config_shortAnimTime))
- .setListener(null);
- }
- }
-
- public boolean isShowing() {
- return mBarView.getVisibility() == View.VISIBLE;
- }
-
- public void close() {
- hideUndoBar(true);
- mUndoListener.onHide(mUndoToken);
- }
-
- public void hideUndoBar(boolean immediate) {
- mHideHandler.removeCallbacks(mHideRunnable);
- if (immediate) {
- mBarView.setVisibility(View.GONE);
- ViewHelper.setAlpha(mBarView, 0);
- mUndoMessage = null;
- } else {
- mBarAnimator.cancel();
- mBarAnimator
- .alpha(0)
- .setDuration(mBarView.getResources()
- .getInteger(android.R.integer.config_shortAnimTime))
- .setListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- mBarView.setVisibility(View.GONE);
- mUndoMessage = null;
- mUndoToken = null;
- }
- });
- }
- }
-
- private Runnable mHideRunnable = new Runnable() {
- @Override
- public void run() {
- hideUndoBar(false);
- mUndoListener.onHide(mUndoToken);
- }
- };
-}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/AudioPlayer.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/AudioPlayer.java
index aafcea307..f0850e6df 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
@@ -3,7 +3,9 @@ package de.danoeh.antennapod.core.util.playback;
import android.content.Context;
import android.util.Log;
import android.view.SurfaceHolder;
-import com.aocate.media.MediaPlayer;
+import org.antennapod.audio.MediaPlayer;
+
+import de.danoeh.antennapod.core.preferences.UserPreferences;
public class AudioPlayer extends MediaPlayer implements IPlayer {
private static final String TAG = "AudioPlayer";
@@ -16,7 +18,6 @@ public class AudioPlayer extends MediaPlayer implements IPlayer {
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
@@ -31,4 +32,14 @@ public class AudioPlayer extends MediaPlayer implements IPlayer {
public void setVideoScalingMode(int mode) {
throw new UnsupportedOperationException("Setting scaling mode is not supported in Audio Player");
}
+
+ @Override
+ protected boolean useSonic() {
+ return UserPreferences.useSonic();
+ }
+
+ @Override
+ protected boolean downmix() {
+ return UserPreferences.stereoToMono();
+ }
}
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 49769f4f0..ec50dce7c 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
@@ -20,6 +20,7 @@ public class ExternalMedia implements Playable {
public static final String PREF_SOURCE_URL = "ExternalMedia.PrefSourceUrl";
public static final String PREF_POSITION = "ExternalMedia.PrefPosition";
public static final String PREF_MEDIA_TYPE = "ExternalMedia.PrefMediaType";
+ public static final String PREF_LAST_PLAYED_TIME = "ExternalMedia.PrefLastPlayedTime";
private String source;
@@ -29,6 +30,7 @@ public class ExternalMedia implements Playable {
private List<Chapter> chapters;
private int duration;
private int position;
+ private long lastPlayedTime;
public ExternalMedia(String source, MediaType mediaType) {
super();
@@ -36,9 +38,10 @@ public class ExternalMedia implements Playable {
this.mediaType = mediaType;
}
- public ExternalMedia(String source, MediaType mediaType, int position) {
+ public ExternalMedia(String source, MediaType mediaType, int position, long lastPlayedTime) {
this(source, mediaType);
this.position = position;
+ this.lastPlayedTime = lastPlayedTime;
}
@Override
@@ -51,6 +54,7 @@ public class ExternalMedia implements Playable {
dest.writeString(source);
dest.writeString(mediaType.toString());
dest.writeInt(position);
+ dest.writeLong(lastPlayedTime);
}
@Override
@@ -58,6 +62,7 @@ public class ExternalMedia implements Playable {
prefEditor.putString(PREF_SOURCE_URL, source);
prefEditor.putString(PREF_MEDIA_TYPE, mediaType.toString());
prefEditor.putInt(PREF_POSITION, position);
+ prefEditor.putLong(PREF_LAST_PLAYED_TIME, lastPlayedTime);
}
@Override
@@ -145,6 +150,11 @@ public class ExternalMedia implements Playable {
}
@Override
+ public long getLastPlayedTime() {
+ return lastPlayedTime;
+ }
+
+ @Override
public MediaType getMediaType() {
return mediaType;
}
@@ -170,10 +180,12 @@ public class ExternalMedia implements Playable {
}
@Override
- public void saveCurrentPosition(SharedPreferences pref, int newPosition) {
+ public void saveCurrentPosition(SharedPreferences pref, int newPosition, long timestamp) {
SharedPreferences.Editor editor = pref.edit();
editor.putInt(PREF_POSITION, newPosition);
+ editor.putLong(PREF_LAST_PLAYED_TIME, timestamp);
position = newPosition;
+ lastPlayedTime = timestamp;
editor.commit();
}
@@ -188,6 +200,11 @@ public class ExternalMedia implements Playable {
}
@Override
+ public void setLastPlayedTime(long lastPlayedTime) {
+ this.lastPlayedTime = lastPlayedTime;
+ }
+
+ @Override
public void onPlaybackStart() {
}
@@ -215,8 +232,12 @@ public class ExternalMedia implements Playable {
if (in.dataAvail() > 0) {
position = in.readInt();
}
- ExternalMedia extMedia = new ExternalMedia(source, type, position);
- return extMedia;
+ long lastPlayedTime = 0;
+ if (in.dataAvail() > 0) {
+ lastPlayedTime = in.readLong();
+ }
+
+ return new ExternalMedia(source, type, position, lastPlayedTime);
}
public ExternalMedia[] newArray(int size) {
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 147c7848d..d67153a4e 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
@@ -10,6 +10,8 @@ public interface IPlayer {
boolean canSetSpeed();
+ boolean canDownmix();
+
float getCurrentPitchStepsAdjustment();
int getCurrentPosition();
@@ -57,6 +59,8 @@ public interface IPlayer {
void setPlaybackSpeed(float f);
+ void setDownmix(boolean enable);
+
void setVolume(float left, float right);
void start();
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 7ebd580f7..86ec4fbd0 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
@@ -7,7 +7,7 @@ import android.util.Log;
import java.util.List;
-import de.danoeh.antennapod.core.asynctask.PicassoImageResource;
+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;
@@ -18,7 +18,7 @@ import de.danoeh.antennapod.core.util.ShownotesProvider;
* Interface for objects that can be played by the PlaybackService.
*/
public interface Playable extends Parcelable,
- ShownotesProvider, PicassoImageResource {
+ ShownotesProvider, ImageResource {
/**
* Save information about the playable in a preference so that it can be
@@ -82,6 +82,12 @@ public interface Playable extends Parcelable,
public int getPosition();
/**
+ * Returns last time (in ms) when this playable was played or 0
+ * if last played time is unknown.
+ */
+ public long getLastPlayedTime();
+
+ /**
* Returns the type of media. This method should return the correct value
* BEFORE loadMetadata() is called.
*/
@@ -115,14 +121,23 @@ public interface Playable extends Parcelable,
* Saves the current position of this object. Implementations can use the
* provided SharedPreference to save this information and retrieve it later
* via PlayableUtils.createInstanceFromPreferences.
+ *
+ * @param pref shared prefs that might be used to store this object
+ * @param newPosition new playback position in ms
+ * @param timestamp current time in ms
*/
- public void saveCurrentPosition(SharedPreferences pref, int newPosition);
+ public void saveCurrentPosition(SharedPreferences pref, int newPosition, long timestamp);
public void setPosition(int newPosition);
public void setDuration(int newDuration);
/**
+ * @param lastPlayedTimestamp timestamp in ms
+ */
+ public void setLastPlayedTime(long lastPlayedTimestamp);
+
+ /**
* Is called by the PlaybackService when playback starts.
*/
public void onPlaybackStart();
@@ -159,28 +174,42 @@ public interface Playable extends Parcelable,
*/
public static Playable createInstanceFromPreferences(Context context, int type,
SharedPreferences pref) {
+ Playable result = null;
// ADD new Playable types here:
switch (type) {
case FeedMedia.PLAYABLE_TYPE_FEEDMEDIA:
- long mediaId = pref.getLong(FeedMedia.PREF_MEDIA_ID, -1);
- if (mediaId != -1) {
- return DBReader.getFeedMedia(context, mediaId);
- }
+ result = createFeedMediaInstance(pref);
break;
case ExternalMedia.PLAYABLE_TYPE_EXTERNAL_MEDIA:
- String source = pref.getString(ExternalMedia.PREF_SOURCE_URL,
- null);
- String mediaType = pref.getString(
- ExternalMedia.PREF_MEDIA_TYPE, null);
- if (source != null && mediaType != null) {
- int position = pref.getInt(ExternalMedia.PREF_POSITION, 0);
- return new ExternalMedia(source,
- MediaType.valueOf(mediaType), position);
- }
+ result = createExternalMediaInstance(pref);
break;
}
- Log.e(TAG, "Could not restore Playable object from preferences");
- return null;
+ if (result == null) {
+ Log.e(TAG, "Could not restore Playable object from preferences");
+ }
+ return result;
+ }
+
+ private static Playable createFeedMediaInstance(SharedPreferences pref) {
+ Playable result = null;
+ long mediaId = pref.getLong(FeedMedia.PREF_MEDIA_ID, -1);
+ if (mediaId != -1) {
+ result = DBReader.getFeedMedia(mediaId);
+ }
+ return result;
+ }
+
+ private static Playable createExternalMediaInstance(SharedPreferences pref) {
+ Playable result = null;
+ String source = pref.getString(ExternalMedia.PREF_SOURCE_URL, null);
+ String mediaType = pref.getString(ExternalMedia.PREF_MEDIA_TYPE, null);
+ if (source != null && mediaType != null) {
+ int position = pref.getInt(ExternalMedia.PREF_POSITION, 0);
+ long lastPlayedTime = pref.getLong(ExternalMedia.PREF_LAST_PLAYED_TIME, 0);
+ result = new ExternalMedia(source, MediaType.valueOf(mediaType),
+ position, lastPlayedTime);
+ }
+ return result;
}
}
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 a0d12d3e7..27935978c 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
@@ -11,24 +11,22 @@ import android.content.SharedPreferences;
import android.content.res.TypedArray;
import android.media.MediaPlayer;
import android.os.AsyncTask;
+import android.os.Build;
import android.os.IBinder;
import android.preference.PreferenceManager;
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
import android.view.SurfaceHolder;
-import android.view.View;
import android.view.View.OnClickListener;
import android.widget.ImageButton;
import android.widget.SeekBar;
import android.widget.TextView;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.commons.lang3.Validate;
-
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
-import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@@ -37,6 +35,7 @@ import de.danoeh.antennapod.core.feed.Chapter;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.feed.MediaType;
import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
import de.danoeh.antennapod.core.service.playback.PlaybackServiceMediaPlayer;
import de.danoeh.antennapod.core.service.playback.PlayerStatus;
@@ -49,6 +48,7 @@ import de.danoeh.antennapod.core.util.playback.Playable.PlayableUtils;
* control playback instead of communicating with the PlaybackService directly.
*/
public abstract class PlaybackController {
+
private static final String TAG = "PlaybackController";
public static final int INVALID_TIME = -1;
@@ -56,7 +56,7 @@ public abstract class PlaybackController {
private final Activity activity;
private PlaybackService playbackService;
- private Playable media;
+ protected Playable media;
private PlayerStatus status;
private ScheduledThreadPoolExecutor schedExecutor;
@@ -74,22 +74,16 @@ public abstract class PlaybackController {
*/
private boolean reinitOnPause;
- public PlaybackController(Activity activity, boolean reinitOnPause) {
- Validate.notNull(activity);
+ public PlaybackController(@NonNull Activity activity, boolean reinitOnPause) {
this.activity = activity;
this.reinitOnPause = reinitOnPause;
schedExecutor = new ScheduledThreadPoolExecutor(SCHED_EX_POOLSIZE,
- new ThreadFactory() {
-
- @Override
- public Thread newThread(Runnable r) {
- Thread t = new Thread(r);
- t.setPriority(Thread.MIN_PRIORITY);
- return t;
- }
+ r -> {
+ Thread t = new Thread(r);
+ t.setPriority(Thread.MIN_PRIORITY);
+ return t;
}, new RejectedExecutionHandler() {
-
@Override
public void rejectedExecution(Runnable r,
ThreadPoolExecutor executor) {
@@ -106,10 +100,10 @@ public abstract class PlaybackController {
*/
public void init() {
activity.registerReceiver(statusUpdate, new IntentFilter(
- PlaybackService.ACTION_PLAYER_STATUS_CHANGED));
+ PlaybackService.ACTION_PLAYER_STATUS_CHANGED));
activity.registerReceiver(notificationReceiver, new IntentFilter(
- PlaybackService.ACTION_PLAYER_NOTIFICATION));
+ PlaybackService.ACTION_PLAYER_NOTIFICATION));
activity.registerReceiver(shutdownReceiver, new IntentFilter(
PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE));
@@ -120,6 +114,7 @@ public abstract class PlaybackController {
throw new IllegalStateException(
"Can't call init() after release() has been called");
}
+ checkMediaInfoLoaded();
}
/**
@@ -240,7 +235,7 @@ public abstract class PlaybackController {
return null;
}
- public abstract void setupGUI();
+
private void setupPositionObserver() {
if ((positionObserverFuture != null && positionObserverFuture
@@ -264,8 +259,6 @@ public abstract class PlaybackController {
}
}
- public abstract void onPositionObserverUpdate();
-
private ServiceConnection mConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder service) {
playbackService = ((PlaybackService.LocalBinder) service)
@@ -359,7 +352,7 @@ public abstract class PlaybackController {
@Override
public void onReceive(Context context, Intent intent) {
if (isConnectedToPlaybackService()) {
- if (StringUtils.equals(intent.getAction(),
+ if (TextUtils.equals(intent.getAction(),
PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE)) {
release();
onShutdownNotification();
@@ -368,26 +361,31 @@ public abstract class PlaybackController {
}
};
- public abstract void onPlaybackSpeedChange();
+ public void setupGUI() {};
+
+ public void onPositionObserverUpdate() {};
+
- public abstract void onShutdownNotification();
+ public void onPlaybackSpeedChange() {};
+
+ public void onShutdownNotification() {};
/**
* Called when the currently displayed information should be refreshed.
*/
- public abstract void onReloadNotification(int code);
+ public void onReloadNotification(int code) {};
- public abstract void onBufferStart();
+ public void onBufferStart() {};
- public abstract void onBufferEnd();
+ public void onBufferEnd() {};
- public abstract void onBufferUpdate(float progress);
+ public void onBufferUpdate(float progress) {};
- public abstract void onSleepTimerUpdate();
+ public void onSleepTimerUpdate() {};
- public abstract void handleError(int code);
+ public void handleError(int code) {};
- public abstract void onPlaybackEnd();
+ public void onPlaybackEnd() {};
public void repeatHandleStatus() {
if (status != null && playbackService != null) {
@@ -418,7 +416,6 @@ public abstract class PlaybackController {
Log.d(TAG, "status: " + status.toString());
switch (status) {
-
case ERROR:
postStatusMsg(R.string.player_error_msg);
handleError(MediaPlayer.MEDIA_ERROR_UNKNOWN);
@@ -479,19 +476,25 @@ public abstract class PlaybackController {
private void updatePlayButtonAppearance(int resource, CharSequence contentDescription) {
ImageButton butPlay = getPlayButton();
- butPlay.setImageResource(resource);
- butPlay.setContentDescription(contentDescription);
+ if(butPlay != null) {
+ butPlay.setImageResource(resource);
+ butPlay.setContentDescription(contentDescription);
+ }
}
- public abstract ImageButton getPlayButton();
+ public ImageButton getPlayButton() {
+ return null;
+ };
- public abstract void postStatusMsg(int msg);
+ public void postStatusMsg(int msg) {};
- public abstract void clearStatusMsg();
+ public void clearStatusMsg() {};
- public abstract boolean loadMediaInfo();
+ public boolean loadMediaInfo() {
+ return false;
+ };
- public abstract void onAwaitingVideoSurface();
+ public void onAwaitingVideoSurface() {};
/**
* Called when connection to playback service has been established or
@@ -525,7 +528,7 @@ public abstract class PlaybackController {
}
}
- public abstract void onServiceQueried();
+ public void onServiceQueried() {};
/**
* Should be used by classes which implement the OnSeekBarChanged interface.
@@ -555,7 +558,7 @@ public abstract class PlaybackController {
* Should be used by classes which implement the OnSeekBarChanged interface.
*/
public void onSeekBarStopTrackingTouch(SeekBar seekBar, float prog) {
- if (playbackService != null) {
+ if (playbackService != null && media != null) {
playbackService.seekTo((int) (prog * media.getDuration()));
setupPositionObserver();
}
@@ -572,37 +575,32 @@ public abstract class PlaybackController {
}
public OnClickListener newOnPlayButtonClickListener() {
- return new OnClickListener() {
- @Override
- public void onClick(View v) {
- if (playbackService != null) {
- switch (status) {
- case PLAYING:
- playbackService.pause(true, reinitOnPause);
- break;
- case PAUSED:
- case PREPARED:
- playbackService.resume();
- break;
- case PREPARING:
- playbackService.setStartWhenPrepared(!playbackService
- .isStartWhenPrepared());
- if (reinitOnPause
- && playbackService.isStartWhenPrepared() == false) {
- playbackService.reinit();
- }
- break;
- case INITIALIZED:
- playbackService.setStartWhenPrepared(true);
- playbackService.prepare();
- break;
+ return v -> {
+ if (playbackService == null) {
+ Log.w(TAG, "Play/Pause button was pressed, but playbackservice was null!");
+ return;
+ }
+ switch (status) {
+ case PLAYING:
+ playbackService.pause(true, reinitOnPause);
+ break;
+ case PAUSED:
+ case PREPARED:
+ playbackService.resume();
+ break;
+ case PREPARING:
+ playbackService.setStartWhenPrepared(!playbackService
+ .isStartWhenPrepared());
+ if (reinitOnPause
+ && playbackService.isStartWhenPrepared() == false) {
+ playbackService.reinit();
}
- } else {
- Log.w(TAG,
- "Play/Pause button was pressed, but playbackservice was null!");
- }
+ break;
+ case INITIALIZED:
+ playbackService.setStartWhenPrepared(true);
+ playbackService.prepare();
+ break;
}
-
};
}
@@ -652,9 +650,9 @@ public abstract class PlaybackController {
}
}
- public void setSleepTimer(long time) {
+ public void setSleepTimer(long time, boolean shakeToReset, boolean vibrate) {
if (playbackService != null) {
- playbackService.setSleepTimer(time);
+ playbackService.setSleepTimer(time, shakeToReset, vibrate);
}
}
@@ -681,6 +679,11 @@ public abstract class PlaybackController {
}
public boolean canSetPlaybackSpeed() {
+ if (org.antennapod.audio.MediaPlayer.isPrestoLibraryInstalled(activity.getApplicationContext())
+ || UserPreferences.useSonic()
+ || Build.VERSION.SDK_INT >= 23) {
+ return true;
+ }
return playbackService != null && playbackService.canSetSpeed();
}
@@ -690,6 +693,12 @@ public abstract class PlaybackController {
}
}
+ public void setVolume(float leftVolume, float rightVolume) {
+ if (playbackService != null) {
+ playbackService.setVolume(leftVolume, rightVolume);
+ }
+ }
+
public float getCurrentPlaybackSpeedMultiplier() {
if (canSetPlaybackSpeed()) {
return playbackService.getCurrentPlaybackSpeed();
@@ -698,6 +707,16 @@ public abstract class PlaybackController {
}
}
+ public boolean canDownmix() {
+ return playbackService != null && playbackService.canDownmix();
+ }
+
+ public void setDownmix(boolean enable) {
+ if(playbackService != null) {
+ playbackService.setDownmix(enable);
+ }
+ }
+
public boolean isPlayingVideo() {
if (playbackService != null) {
return PlaybackService.getCurrentMediaType() == MediaType.VIDEO;
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 f31297b41..2eee1ac87 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
@@ -2,10 +2,10 @@ package de.danoeh.antennapod.core.util.playback;
import android.content.Context;
import android.content.res.TypedArray;
+import android.support.annotation.NonNull;
import android.util.Log;
import android.util.TypedValue;
-import org.apache.commons.lang3.Validate;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
@@ -14,7 +14,6 @@ import org.jsoup.select.Elements;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
-import de.danoeh.antennapod.core.BuildConfig;
import de.danoeh.antennapod.core.util.Converter;
import de.danoeh.antennapod.core.util.ShownotesProvider;
@@ -59,6 +58,8 @@ public class Timeline {
private static final Pattern TIMECODE_LINK_REGEX = Pattern.compile("antennapod://timecode/((\\d+))");
private static final String TIMECODE_LINK = "<a class=\"timecode\" href=\"antennapod://timecode/%d\">%s</a>";
private static final Pattern TIMECODE_REGEX = Pattern.compile("\\b(?:(?:(([0-9][0-9])):))?(([0-9][0-9])):(([0-9][0-9]))\\b");
+ private static final Pattern LINE_BREAK_REGEX = Pattern.compile("<br *\\/?>");
+
/**
* Applies an app-specific CSS stylesheet and adds timecode links (optional).
@@ -82,11 +83,15 @@ public class Timeline {
return null;
}
if (shownotes == null) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "shownotesProvider contained no shownotes. Returning empty string");
+ Log.d(TAG, "shownotesProvider contained no shownotes. Returning empty string");
return "";
}
+ // 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>")) {
+ shownotes = shownotes.replace("\n", "<br />");
+ }
+
Document document = Jsoup.parse(shownotes);
// apply style
@@ -97,10 +102,9 @@ public class Timeline {
// apply timecode links
if (addTimecodes) {
Elements elementsWithTimeCodes = document.body().getElementsMatchingOwnText(TIMECODE_REGEX);
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Recognized " + elementsWithTimeCodes.size() + " timecodes");
+ Log.d(TAG, "Recognized " + elementsWithTimeCodes.size() + " timecodes");
for (Element element : elementsWithTimeCodes) {
- Matcher matcherLong = TIMECODE_REGEX.matcher(element.text());
+ Matcher matcherLong = TIMECODE_REGEX.matcher(element.html());
StringBuffer buffer = new StringBuffer();
while (matcherLong.find()) {
String h = matcherLong.group(1);
@@ -154,8 +158,7 @@ public class Timeline {
}
- public void setShownotesProvider(ShownotesProvider shownotesProvider) {
- Validate.notNull(shownotesProvider);
+ public void setShownotesProvider(@NonNull ShownotesProvider shownotesProvider) {
this.shownotesProvider = shownotesProvider;
}
}
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 dc5270d8f..368379509 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
@@ -17,6 +17,11 @@ public class VideoPlayer extends MediaPlayer implements IPlayer {
}
@Override
+ public boolean canDownmix() {
+ return false;
+ }
+
+ @Override
public float getCurrentPitchStepsAdjustment() {
return 1;
}
@@ -60,6 +65,12 @@ public class VideoPlayer extends MediaPlayer implements IPlayer {
throw new UnsupportedOperationException("Setting playback speed unsupported in video player");
}
+ @Override
+ public void setDownmix(boolean b) {
+ Log.e(TAG, "Setting downmix unsupported in video player");
+ throw new UnsupportedOperationException("Setting downmix unsupported in video player");
+ }
+
@Override
public void setVideoScalingMode(int mode) {
super.setVideoScalingMode(mode);
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 9588265b8..13cb9f002 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
@@ -1,7 +1,9 @@
package de.danoeh.antennapod.core.util.syndication;
import android.net.Uri;
-import org.apache.commons.lang3.StringUtils;
+import android.support.v4.util.ArrayMap;
+import android.text.TextUtils;
+
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
@@ -9,7 +11,6 @@ import org.jsoup.select.Elements;
import java.io.File;
import java.io.IOException;
-import java.util.LinkedHashMap;
import java.util.Map;
/**
@@ -45,12 +46,12 @@ public class FeedDiscoverer {
}
private Map<String, String> findLinks(Document document, String baseUrl) {
- Map<String, String> res = new LinkedHashMap<String, String>();
+ Map<String, String> res = new ArrayMap<>();
Elements links = document.head().getElementsByTag("link");
for (Element link : links) {
String rel = link.attr("rel");
String href = link.attr("href");
- if (!StringUtils.isEmpty(href) &&
+ if (!TextUtils.isEmpty(href) &&
(rel.equals("alternate") || rel.equals("feed"))) {
String type = link.attr("type");
if (type.equals(MIME_RSS) || type.equals(MIME_ATOM)) {
@@ -58,7 +59,7 @@ public class FeedDiscoverer {
String processedUrl = processURL(baseUrl, href);
if (processedUrl != null) {
res.put(processedUrl,
- (StringUtils.isEmpty(title)) ? href : title);
+ (TextUtils.isEmpty(title)) ? href : title);
}
}
}