summaryrefslogtreecommitdiff
path: root/core/src
diff options
context:
space:
mode:
Diffstat (limited to 'core/src')
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java2
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/DBTasksCallbacks.java20
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/asynctask/DBTaskLoader.java29
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoProvider.java251
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java2
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/menuhandler/MenuItemUtils.java38
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java30
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java105
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java125
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/APCleanupAlgorithm.java103
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/APDownloadAlgorithm.java133
-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.java20
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java178
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java13
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/EpisodeCleanupAlgorithm.java36
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/PowerUtils.java32
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/Timeline.java2
-rw-r--r--core/src/main/res/drawable-hdpi/ic_settings_grey600_24dp.pngbin0 -> 572 bytes
-rw-r--r--core/src/main/res/drawable-hdpi/ic_settings_white_24dp.pngbin0 -> 561 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/ic_settings_grey600_24dp.pngbin0 -> 704 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/ic_settings_white_24dp.pngbin0 -> 737 bytes
-rw-r--r--core/src/main/res/drawable-xxhdpi/ic_settings_grey600_24dp.pngbin0 -> 994 bytes
-rw-r--r--core/src/main/res/drawable-xxhdpi/ic_settings_white_24dp.pngbin0 -> 974 bytes
-rw-r--r--core/src/main/res/layout/refresh_action_view.xml8
-rw-r--r--core/src/main/res/values/attrs.xml1
-rw-r--r--core/src/main/res/values/colors.xml5
-rw-r--r--core/src/main/res/values/dimens.xml2
-rw-r--r--core/src/main/res/values/strings.xml4
-rw-r--r--core/src/main/res/values/styles.xml95
31 files changed, 1248 insertions, 197 deletions
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 e5e609f5f..1a2671555 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java
@@ -22,4 +22,6 @@ 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/DBTasksCallbacks.java b/core/src/main/java/de/danoeh/antennapod/core/DBTasksCallbacks.java
new file mode 100644
index 000000000..edf3e3199
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/DBTasksCallbacks.java
@@ -0,0 +1,20 @@
+package de.danoeh.antennapod.core;
+
+import de.danoeh.antennapod.core.storage.AutomaticDownloadAlgorithm;
+import de.danoeh.antennapod.core.storage.EpisodeCleanupAlgorithm;
+
+/**
+ * Callbacks for the DBTasks class of the storage module.
+ */
+public interface DBTasksCallbacks {
+
+ /**
+ * Returns the client's implementation of the AutomaticDownloadAlgorithm interface.
+ */
+ public AutomaticDownloadAlgorithm getAutomaticDownloadAlgorithm();
+
+ /**
+ * Returns the client's implementation of the EpisodeCacheCleanupAlgorithm interface.
+ */
+ public EpisodeCleanupAlgorithm getEpisodeCacheCleanupAlgorithm();
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/asynctask/DBTaskLoader.java b/core/src/main/java/de/danoeh/antennapod/core/asynctask/DBTaskLoader.java
new file mode 100644
index 000000000..0f402f44a
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/asynctask/DBTaskLoader.java
@@ -0,0 +1,29 @@
+package de.danoeh.antennapod.core.asynctask;
+
+import android.content.Context;
+import android.support.v4.content.AsyncTaskLoader;
+
+/**
+ * Subclass of AsyncTaskLoader that is made for loading data with one of the DB*-classes.
+ * This class will provide a useful default implementation that would otherwise always be necessary when interacting
+ * with the DB*-classes with an AsyncTaskLoader.
+ */
+public abstract class DBTaskLoader<D> extends AsyncTaskLoader<D> {
+
+ public DBTaskLoader(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected void onStopLoading() {
+ super.onStopLoading();
+ cancelLoad();
+ }
+
+ @Override
+ protected void onStartLoading() {
+ super.onStartLoading();
+ // according to https://code.google.com/p/android/issues/detail?id=14944, this has to be called manually
+ forceLoad();
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoProvider.java b/core/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoProvider.java
index 1ed29c23a..b6ece6dc8 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoProvider.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoProvider.java
@@ -14,6 +14,7 @@ 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;
@@ -209,4 +210,254 @@ public class PicassoProvider {
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/feed/FeedItem.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java
index 42e4191f6..c63b61f55 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
@@ -143,7 +143,7 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr
paymentLink = other.paymentLink;
}
if (other.chapters != null) {
- if (chapters == null) {
+ if (!hasChapters) {
chapters = other.chapters;
}
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/menuhandler/MenuItemUtils.java b/core/src/main/java/de/danoeh/antennapod/core/menuhandler/MenuItemUtils.java
new file mode 100644
index 000000000..b8d17bcce
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/menuhandler/MenuItemUtils.java
@@ -0,0 +1,38 @@
+package de.danoeh.antennapod.core.menuhandler;
+
+import android.support.v4.view.MenuItemCompat;
+import android.view.Menu;
+import android.view.MenuItem;
+
+import de.danoeh.antennapod.core.R;
+
+/**
+ * Utilities for menu items
+ */
+public class MenuItemUtils {
+
+ /**
+ * Changes the appearance of a MenuItem depending on whether the given UpdateRefreshMenuItemChecker
+ * is refreshing or not. If it returns true, the menu item will be replaced by an indeterminate progress
+ * bar, otherwise nothing will happen.
+ *
+ * @param menu The menu that the MenuItem belongs to
+ * @param resId The id of the MenuItem
+ * @param checker Is used for checking whether to show the progress indicator or not.
+ * @return The returned value of the UpdateRefreshMenuItemChecker's isRefreshing() method.
+ */
+ public static boolean updateRefreshMenuItem(Menu menu, int resId, UpdateRefreshMenuItemChecker checker) {
+ // expand actionview if feeds are being downloaded, collapse otherwise
+ if (checker.isRefreshing()) {
+ MenuItem refreshItem = menu.findItem(resId);
+ MenuItemCompat.setActionView(refreshItem, R.layout.refresh_action_view);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public static interface UpdateRefreshMenuItemChecker {
+ public boolean isRefreshing();
+ }
+}
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 f18028e8f..a3b9f6049 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
@@ -38,6 +38,7 @@ public class UserPreferences implements
private static final String TAG = "UserPreferences";
public static final String PREF_PAUSE_ON_HEADSET_DISCONNECT = "prefPauseOnHeadsetDisconnect";
+ public static final String PREF_UNPAUSE_ON_HEADSET_RECONNECT = "prefUnpauseOnHeadsetReconnect";
public static final String PREF_FOLLOW_QUEUE = "prefFollowQueue";
public static final String PREF_DOWNLOAD_MEDIA_ON_WIFI_ONLY = "prefDownloadMediaOnWifiOnly";
public static final String PREF_UPDATE_INTERVAL = "prefAutoUpdateIntervall";
@@ -50,6 +51,7 @@ public class UserPreferences implements
public static final String PREF_DATA_FOLDER = "prefDataFolder";
public static final String PREF_ENABLE_AUTODL = "prefEnableAutoDl";
public static final String PREF_ENABLE_AUTODL_WIFI_FILTER = "prefEnableAutoDownloadWifiFilter";
+ public static final String PREF_ENABLE_AUTODL_ON_BATTERY = "prefEnableAutoDownloadOnBattery";
private static final String PREF_AUTODL_SELECTED_NETWORKS = "prefAutodownloadSelectedNetworks";
public static final String PREF_EPISODE_CACHE_SIZE = "prefEpisodeCacheSize";
private static final String PREF_PLAYBACK_SPEED = "prefPlaybackSpeed";
@@ -69,6 +71,7 @@ public class UserPreferences implements
// Preferences
private boolean pauseOnHeadsetDisconnect;
+ private boolean unpauseOnHeadsetReconnect;
private boolean followQueue;
private boolean downloadMediaOnWifiOnly;
private long updateInterval;
@@ -80,6 +83,7 @@ public class UserPreferences implements
private int theme;
private boolean enableAutodownload;
private boolean enableAutodownloadWifiFilter;
+ private boolean enableAutodownloadOnBattery;
private String[] autodownloadSelectedNetworks;
private int episodeCacheSize;
private String playbackSpeed;
@@ -121,6 +125,8 @@ public class UserPreferences implements
R.integer.episode_cache_size_unlimited);
pauseOnHeadsetDisconnect = sp.getBoolean(
PREF_PAUSE_ON_HEADSET_DISCONNECT, true);
+ unpauseOnHeadsetReconnect = sp.getBoolean(
+ PREF_UNPAUSE_ON_HEADSET_RECONNECT, true);
followQueue = sp.getBoolean(PREF_FOLLOW_QUEUE, false);
downloadMediaOnWifiOnly = sp.getBoolean(
PREF_DOWNLOAD_MEDIA_ON_WIFI_ONLY, true);
@@ -140,6 +146,7 @@ public class UserPreferences implements
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);
playbackSpeed = sp.getString(PREF_PLAYBACK_SPEED, "1.0");
playbackSpeedArray = readPlaybackSpeedArray(sp.getString(
PREF_PLAYBACK_SPEED_ARRAY, null));
@@ -221,6 +228,11 @@ public class UserPreferences implements
return instance.pauseOnHeadsetDisconnect;
}
+ public static boolean isUnpauseOnHeadsetReconnect() {
+ instanceAvailable();
+ return instance.unpauseOnHeadsetReconnect;
+ }
+
public static boolean isFollowQueue() {
instanceAvailable();
return instance.followQueue;
@@ -282,6 +294,15 @@ public class UserPreferences implements
return instance.theme;
}
+ public static int getNoTitleTheme() {
+ int theme = getTheme();
+ if (theme == R.style.Theme_AntennaPod_Dark) {
+ return R.style.Theme_AntennaPod_Dark_NoTitle;
+ } else {
+ return R.style.Theme_AntennaPod_Light_NoTitle;
+ }
+ }
+
public static boolean isEnableAutodownloadWifiFilter() {
instanceAvailable();
return instance.enableAutodownloadWifiFilter;
@@ -326,6 +347,11 @@ public class UserPreferences implements
return instance.enableAutodownload;
}
+ public static boolean isEnableAutodownloadOnBattery() {
+ instanceAvailable();
+ return instance.enableAutodownloadOnBattery;
+ }
+
public static boolean shouldPauseForFocusLoss() {
instanceAvailable();
return instance.pauseForFocusLoss;
@@ -377,6 +403,8 @@ public class UserPreferences implements
PREF_EPISODE_CACHE_SIZE, "20"));
} else if (key.equals(PREF_ENABLE_AUTODL)) {
enableAutodownload = sp.getBoolean(PREF_ENABLE_AUTODL, false);
+ } else if (key.equals(PREF_ENABLE_AUTODL_ON_BATTERY)) {
+ enableAutodownloadOnBattery = sp.getBoolean(PREF_ENABLE_AUTODL_ON_BATTERY, true);
} else if (key.equals(PREF_PLAYBACK_SPEED)) {
playbackSpeed = sp.getString(PREF_PLAYBACK_SPEED, "1.0");
} else if (key.equals(PREF_PLAYBACK_SPEED_ARRAY)) {
@@ -388,6 +416,8 @@ public class UserPreferences implements
seekDeltaSecs = Integer.valueOf(sp.getString(PREF_SEEK_DELTA_SECS, "30"));
} else if (key.equals(PREF_PAUSE_ON_HEADSET_DISCONNECT)) {
pauseOnHeadsetDisconnect = sp.getBoolean(PREF_PAUSE_ON_HEADSET_DISCONNECT, true);
+ } else if (key.equals(PREF_UNPAUSE_ON_HEADSET_RECONNECT)) {
+ unpauseOnHeadsetReconnect = sp.getBoolean(PREF_UNPAUSE_ON_HEADSET_RECONNECT, true);
} else if (key.equals(PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD)) {
autoFlattrPlayedDurationThreshold = sp.getFloat(PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD,
PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD_DEFAULT);
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 a5560e3fb..866f1cba3 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
@@ -144,6 +144,10 @@ public class PlaybackService extends Service {
* Is true if service has received a valid start command.
*/
public static boolean started = false;
+ /**
+ * Is true if the service was running, but paused due to headphone disconnect
+ */
+ public static boolean transientPause = false;
private static final int NOTIFICATION_ID = 1;
@@ -206,6 +210,8 @@ 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));
registerReceiver(audioBecomingNoisy, new IntentFilter(
AudioManager.ACTION_AUDIO_BECOMING_NOISY));
registerReceiver(skipCurrentEpisodeReceiver, new IntentFilter(
@@ -228,6 +234,7 @@ public class PlaybackService extends Service {
unregisterReceiver(headsetDisconnected);
unregisterReceiver(shutdownReceiver);
+ unregisterReceiver(bluetoothStateUpdated);
unregisterReceiver(audioBecomingNoisy);
unregisterReceiver(skipCurrentEpisodeReceiver);
mediaPlayer.shutdown();
@@ -284,7 +291,6 @@ public class PlaybackService extends Service {
private void handleKeycode(int keycode) {
if (BuildConfig.DEBUG)
Log.d(TAG, "Handling keycode: " + keycode);
-
final PlaybackServiceMediaPlayer.PSMPInfo info = mediaPlayer.getPSMPInfo();
final PlayerStatus status = info.playerStatus;
switch (keycode) {
@@ -315,12 +321,14 @@ public class PlaybackService extends Service {
break;
case KeyEvent.KEYCODE_MEDIA_PAUSE:
if (status == PlayerStatus.PLAYING) {
- if (UserPreferences.isPersistNotify()) {
- mediaPlayer.pause(false, true);
- } else {
- mediaPlayer.pause(true, true);
- }
+ mediaPlayer.pause(false, true);
+ }
+ if (UserPreferences.isPersistNotify()) {
+ mediaPlayer.pause(false, true);
+ } else {
+ mediaPlayer.pause(true, true);
}
+
break;
case KeyEvent.KEYCODE_MEDIA_NEXT:
case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
@@ -333,7 +341,9 @@ public class PlaybackService extends Service {
case KeyEvent.KEYCODE_MEDIA_STOP:
if (status == PlayerStatus.PLAYING) {
mediaPlayer.pause(true, true);
+ started = false;
}
+
stopForeground(true); // gets rid of persistent notification
break;
default:
@@ -411,10 +421,13 @@ public class PlaybackService extends Service {
taskManager.cancelWidgetUpdater();
if (UserPreferences.isPersistNotify() && android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
// do not remove notification on pause based on user pref and whether android version supports expanded notifications
- } else {
+ // Change [Play] button to [Pause]
+ setupNotification(newInfo);
+ } else if (!UserPreferences.isPersistNotify()) {
// remove notifcation on pause
stopForeground(true);
}
+
break;
case STOPPED:
@@ -431,6 +444,7 @@ public class PlaybackService extends Service {
taskManager.startPositionSaver();
taskManager.startWidgetUpdater();
setupNotification(newInfo);
+ started = true;
break;
case ERROR:
writePlaybackPreferencesNoMediaPlaying();
@@ -734,8 +748,9 @@ public class PlaybackService extends Service {
PlaybackServiceMediaPlayer.PSMPInfo newInfo = mediaPlayer.getPSMPInfo();
final int smallIcon = ClientConfig.playbackServiceCallbacks.getNotificationIconResource(getApplicationContext());
- if (!isCancelled() && info.playerStatus == PlayerStatus.PLAYING
- && info.playable != null) {
+ if (!isCancelled() &&
+ started &&
+ info.playable != null) {
String contentText = info.playable.getFeedTitle();
String contentTitle = info.playable.getEpisodeTitle();
Notification notification = null;
@@ -775,16 +790,30 @@ public class PlaybackService extends Service {
.setContentIntent(pIntent)
.setLargeIcon(icon)
.setSmallIcon(smallIcon)
- .setPriority(UserPreferences.getNotifyPriority()) // set notification priority
- .addAction(android.R.drawable.ic_media_play, //play action
- getString(R.string.play_label),
- playButtonPendingIntent)
- .addAction(android.R.drawable.ic_media_pause, //pause action
- getString(R.string.pause_label),
- pauseButtonPendingIntent)
- .addAction(android.R.drawable.ic_menu_close_clear_cancel, // stop action
- getString(R.string.stop_label),
- stopButtonPendingIntent);
+ .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);
+ }
+
notification = notificationBuilder.build();
} else {
NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(
@@ -793,11 +822,9 @@ public class PlaybackService extends Service {
.setContentText(contentText).setOngoing(true)
.setContentIntent(pIntent).setLargeIcon(icon)
.setSmallIcon(smallIcon);
- notification = notificationBuilder.getNotification();
- }
- if (newInfo.playerStatus == PlayerStatus.PLAYING) {
- startForeground(NOTIFICATION_ID, notification);
+ notification = notificationBuilder.build();
}
+ startForeground(NOTIFICATION_ID, notification);
if (BuildConfig.DEBUG)
Log.d(TAG, "Notification set up");
}
@@ -966,6 +993,7 @@ public class PlaybackService extends Service {
private 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) {
@@ -978,6 +1006,10 @@ public class PlaybackService extends Service {
if (BuildConfig.DEBUG)
Log.d(TAG, "Headset was unplugged during playback.");
pauseIfPauseOnDisconnect();
+ } else if (state == PLUGGED) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Headset was plugged in during playback.");
+ unpauseIfPauseOnDisconnect();
}
} else {
Log.e(TAG, "Received invalid ACTION_HEADSET_PLUG intent");
@@ -986,6 +1018,21 @@ public class PlaybackService extends Service {
}
};
+ private 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) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Received bluetooth connection intent");
+ unpauseIfPauseOnDisconnect();
+ }
+ }
+ }
+ };
+
private BroadcastReceiver audioBecomingNoisy = new BroadcastReceiver() {
@Override
@@ -1003,6 +1050,9 @@ public class PlaybackService extends Service {
*/
private void pauseIfPauseOnDisconnect() {
if (UserPreferences.isPauseOnHeadsetDisconnect()) {
+ if (mediaPlayer.getPlayerStatus() == PlayerStatus.PLAYING) {
+ transientPause = true;
+ }
if (UserPreferences.isPersistNotify()) {
mediaPlayer.pause(false, true);
} else {
@@ -1011,6 +1061,15 @@ public class PlaybackService extends Service {
}
}
+ private void unpauseIfPauseOnDisconnect() {
+ if (transientPause) {
+ transientPause = false;
+ if (UserPreferences.isPauseOnHeadsetDisconnect() && UserPreferences.isUnpauseOnHeadsetReconnect()) {
+ mediaPlayer.resume();
+ }
+ }
+ }
+
private BroadcastReceiver shutdownReceiver = new BroadcastReceiver() {
@Override
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 dbf870eac..c143d7f2c 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
@@ -6,6 +6,9 @@ import android.media.AudioManager;
import android.media.RemoteControlClient;
import android.net.wifi.WifiManager;
import android.os.PowerManager;
+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;
@@ -48,6 +51,10 @@ public class PlaybackServiceMediaPlayer {
private volatile PlayerStatus statusBeforeSeeking;
private volatile IPlayer mediaPlayer;
private volatile Playable media;
+ /**
+ * Only used for Lollipop notifications.
+ */
+ private final MediaSessionCompat mediaSession;
private volatile boolean stream;
private volatile MediaType mediaType;
@@ -89,6 +96,10 @@ public class PlaybackServiceMediaPlayer {
}
);
+ mediaSession = new MediaSessionCompat(context, TAG);
+ mediaSession.setCallback(sessionCallback);
+ mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
+
mediaPlayer = null;
statusBeforeSeeking = null;
pausedBecauseOfTransientAudiofocusLoss = false;
@@ -181,6 +192,7 @@ public class PlaybackServiceMediaPlayer {
setPlayerStatus(PlayerStatus.INITIALIZING, media);
try {
media.loadMetadata();
+ mediaSession.setMetadata(getMediaSessionMetadata(media));
if (stream) {
mediaPlayer.setDataSource(media.getStreamUrl());
} else {
@@ -211,6 +223,13 @@ 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();
+ }
+
/**
* Resumes playback if the PSMP object is in PREPARED or PAUSED state. If the PSMP object is in an invalid state.
@@ -586,6 +605,10 @@ public class PlaybackServiceMediaPlayer {
return mediaType;
}
+ public PlayerStatus getPlayerStatus() {
+ return playerStatus;
+ }
+
public boolean isStreaming() {
return stream;
}
@@ -599,6 +622,9 @@ public class PlaybackServiceMediaPlayer {
if (mediaPlayer != null) {
mediaPlayer.release();
}
+ if (mediaSession != null) {
+ mediaSession.release();
+ }
releaseWifiLockIfNecessary();
}
@@ -663,6 +689,16 @@ public class PlaybackServiceMediaPlayer {
}
/**
+ * Returns a token to this object's MediaSession. The MediaSession should only be used for notifications
+ * at the moment.
+ *
+ * @return The MediaSessionCompat.Token object.
+ */
+ public MediaSessionCompat.Token getSessionToken() {
+ return mediaSession.getSessionToken();
+ }
+
+ /**
* Sets the player status of the PSMP object. PlayerStatus and media attributes have to be set at the same time
* so that getPSMPInfo can't return an invalid state (e.g. status is PLAYING, but media is null).
* <p/>
@@ -679,6 +715,45 @@ public class PlaybackServiceMediaPlayer {
this.playerStatus = newStatus;
this.media = newMedia;
+
+ PlaybackStateCompat.Builder sessionState = new PlaybackStateCompat.Builder();
+
+ int state;
+ if (playerStatus != null) {
+ switch (playerStatus) {
+ case PLAYING:
+ state = PlaybackStateCompat.STATE_PLAYING;
+ break;
+ case PREPARED:
+ case PAUSED:
+ state = PlaybackStateCompat.STATE_PAUSED;
+ break;
+ case STOPPED:
+ state = PlaybackStateCompat.STATE_STOPPED;
+ break;
+ case SEEKING:
+ state = PlaybackStateCompat.STATE_FAST_FORWARDING;
+ break;
+ case PREPARING:
+ case INITIALIZING:
+ state = PlaybackStateCompat.STATE_CONNECTING;
+ break;
+ case INITIALIZED:
+ case INDETERMINATE:
+ state = PlaybackStateCompat.STATE_NONE;
+ break;
+ case ERROR:
+ state = PlaybackStateCompat.STATE_ERROR;
+ break;
+ default:
+ state = PlaybackStateCompat.STATE_NONE;
+ break;
+ }
+ } else {
+ state = PlaybackStateCompat.STATE_NONE;
+ }
+ sessionState.setState(state, PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, getPlaybackSpeed());
+
callback.statusChanged(new PSMPInfo(playerStatus, media));
}
@@ -976,4 +1051,54 @@ public class PlaybackServiceMediaPlayer {
}
});
}
+
+ 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();
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ if (playerStatus == PlayerStatus.PLAYING) {
+ pause(false, true);
+ }
+ 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.getSeekDeltaMs());
+ }
+
+ @Override
+ public void onRewind() {
+ super.onRewind();
+ seekDelta(-UserPreferences.getSeekDeltaMs());
+ }
+
+ @Override
+ public void onSeekTo(long pos) {
+ super.onSeekTo(pos);
+ seekTo((int) pos);
+ }
+ };
}
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
new file mode 100644
index 000000000..499fddf74
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/APCleanupAlgorithm.java
@@ -0,0 +1,103 @@
+package de.danoeh.antennapod.core.storage;
+
+import android.content.Context;
+import android.util.Log;
+
+import java.util.ArrayList;
+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.preferences.UserPreferences;
+import de.danoeh.antennapod.core.util.QueueAccess;
+
+/**
+ * Implementation of the EpisodeCleanupAlgorithm interface used by AntennaPod.
+ */
+public class APCleanupAlgorithm implements EpisodeCleanupAlgorithm<Integer> {
+ private static final String TAG = "APCleanupAlgorithm";
+
+ @Override
+ public int performCleanup(Context context, Integer episodeNumber) {
+ List<FeedItem> candidates = new ArrayList<FeedItem>();
+ List<FeedItem> downloadedItems = DBReader.getDownloadedItems(context);
+ QueueAccess queue = QueueAccess.IDListAccess(DBReader.getQueueIDList(context));
+ List<FeedItem> delete;
+ for (FeedItem item : downloadedItems) {
+ if (item.hasMedia() && item.getMedia().isDownloaded()
+ && !queue.contains(item.getId()) && item.isRead()) {
+ 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();
+
+ if (l == null) {
+ l = new Date(0);
+ }
+ if (r == null) {
+ r = new Date(0);
+ }
+ return l.compareTo(r);
+ }
+ });
+
+ if (candidates.size() > episodeNumber) {
+ delete = candidates.subList(0, episodeNumber);
+ } 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,
+ episodeNumber));
+
+ return counter;
+ }
+
+ @Override
+ public Integer getDefaultCleanupParameter(Context context) {
+ return 0;
+ }
+
+ @Override
+ public Integer getPerformCleanupParameter(Context context, List<FeedItem> items) {
+ return getPerformAutoCleanupArgs(context, items.size());
+ }
+
+ 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;
+ }
+}
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
new file mode 100644
index 000000000..c5f871f48
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/APDownloadAlgorithm.java
@@ -0,0 +1,133 @@
+package de.danoeh.antennapod.core.storage;
+
+import android.content.Context;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+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;
+import de.danoeh.antennapod.core.util.PowerUtils;
+
+/**
+ * Implements the automatic download algorithm used by AntennaPod. This class assumes that
+ * the client uses the APEpisodeCleanupAlgorithm.
+ */
+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
+ * 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) {
+ return new Runnable() {
+ @Override
+ public void run() {
+
+ // true if we should auto download based on network status
+ boolean networkShouldAutoDl = NetworkUtils.autodownloadNetworkAvailable(context)
+ && UserPreferences.isEnableAutodownload();
+
+ // true if we should auto download based on power status
+ boolean powerShouldAutoDl = PowerUtils.deviceCharging(context)
+ || UserPreferences.isEnableAutodownloadOnBattery();
+
+ // we should only auto download if both network AND power are happy
+ if (networkShouldAutoDl && powerShouldAutoDl) {
+
+ Log.d(TAG, "Performing auto-dl of undownloaded episodes");
+
+ final List<FeedItem> queue = DBReader.getQueue(context);
+ final List<FeedItem> unreadItems = DBReader
+ .getUnreadItemsList(context);
+
+ int undownloadedEpisodes = DBTasks.getNumberOfUndownloadedEpisodes(queue,
+ unreadItems);
+ int downloadedEpisodes = DBReader
+ .getNumberOfDownloadedEpisodes(context);
+ int deletedEpisodes = cleanupAlgorithm.performCleanup(context,
+ APCleanupAlgorithm.getPerformAutoCleanupArgs(context, undownloadedEpisodes));
+ int episodeSpaceLeft = undownloadedEpisodes;
+ boolean cacheIsUnlimited = UserPreferences.getEpisodeCacheSize() == UserPreferences
+ .getEpisodeCacheSizeUnlimited();
+
+ if (!cacheIsUnlimited
+ && UserPreferences.getEpisodeCacheSize() < downloadedEpisodes
+ + undownloadedEpisodes) {
+ episodeSpaceLeft = UserPreferences.getEpisodeCacheSize()
+ - (downloadedEpisodes - deletedEpisodes);
+ }
+
+ Arrays.sort(mediaIds); // sort for binary search
+ final boolean ignoreMediaIds = mediaIds.length == 0;
+ List<FeedItem> itemsToDownload = new ArrayList<FeedItem>();
+
+ if (episodeSpaceLeft > 0 && undownloadedEpisodes > 0) {
+ for (int i = 0; i < queue.size(); i++) { // ignore playing item
+ FeedItem item = queue.get(i);
+ long mediaId = (item.hasMedia()) ? item.getMedia().getId() : -1;
+ if ((ignoreMediaIds || Arrays.binarySearch(mediaIds, mediaId) >= 0)
+ && item.hasMedia()
+ && !item.getMedia().isDownloaded()
+ && !item.getMedia().isPlaying()
+ && item.getFeed().getPreferences().getAutoDownload()) {
+ itemsToDownload.add(item);
+ episodeSpaceLeft--;
+ undownloadedEpisodes--;
+ if (episodeSpaceLeft == 0 || undownloadedEpisodes == 0) {
+ break;
+ }
+ }
+ }
+ }
+
+ if (episodeSpaceLeft > 0 && undownloadedEpisodes > 0) {
+ for (FeedItem item : unreadItems) {
+ long mediaId = (item.hasMedia()) ? item.getMedia().getId() : -1;
+ if ((ignoreMediaIds || Arrays.binarySearch(mediaIds, mediaId) >= 0)
+ && item.hasMedia()
+ && !item.getMedia().isDownloaded()
+ && item.getFeed().getPreferences().getAutoDownload()) {
+ itemsToDownload.add(item);
+ episodeSpaceLeft--;
+ undownloadedEpisodes--;
+ if (episodeSpaceLeft == 0 || undownloadedEpisodes == 0) {
+ break;
+ }
+ }
+ }
+ }
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Enqueueing " + itemsToDownload.size()
+ + " items for download");
+
+ 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/APSPCleanupAlgorithm.java b/core/src/main/java/de/danoeh/antennapod/core/storage/APSPCleanupAlgorithm.java
new file mode 100644
index 000000000..420bbc09d
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/APSPCleanupAlgorithm.java
@@ -0,0 +1,139 @@
+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
new file mode 100644
index 000000000..f760ec0ce
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/APSPDownloadAlgorithm.java
@@ -0,0 +1,72 @@
+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
new file mode 100644
index 000000000..9ca9620a7
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/AutomaticDownloadAlgorithm.java
@@ -0,0 +1,20 @@
+package de.danoeh.antennapod.core.storage;
+
+import android.content.Context;
+
+public interface AutomaticDownloadAlgorithm {
+
+ /**
+ * Looks for undownloaded episodes 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.
+ */
+ public Runnable autoDownloadUndownloadedItems(Context context, long... mediaIds);
+}
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 b1aff5594..e73f9599d 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
@@ -8,7 +8,6 @@ import android.util.Log;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
-import java.util.Comparator;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
@@ -35,7 +34,6 @@ import de.danoeh.antennapod.core.service.GpodnetSyncService;
import de.danoeh.antennapod.core.service.download.DownloadStatus;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
import de.danoeh.antennapod.core.util.DownloadError;
-import de.danoeh.antennapod.core.util.NetworkUtils;
import de.danoeh.antennapod.core.util.QueueAccess;
import de.danoeh.antennapod.core.util.comparator.FeedItemPubdateComparator;
import de.danoeh.antennapod.core.util.exception.MediaFileNotFoundException;
@@ -386,8 +384,8 @@ public final class DBTasks {
downloadFeedItems(true, context, items);
}
- private static void downloadFeedItems(boolean performAutoCleanup,
- final Context context, final FeedItem... items)
+ static void downloadFeedItems(boolean performAutoCleanup,
+ final Context context, final FeedItem... items)
throws DownloadRequestException {
final DownloadRequester requester = DownloadRequester.getInstance();
@@ -396,8 +394,10 @@ public final class DBTasks {
@Override
public void run() {
- performAutoCleanup(context,
- getPerformAutoCleanupArgs(context, items.length));
+ ClientConfig.dbTasksCallbacks.getEpisodeCacheCleanupAlgorithm()
+ .performCleanup(context,
+ ClientConfig.dbTasksCallbacks.getEpisodeCacheCleanupAlgorithm()
+ .getPerformCleanupParameter(context, Arrays.asList(items)));
}
}.start();
@@ -427,7 +427,7 @@ public final class DBTasks {
}
}
- private static int getNumberOfUndownloadedEpisodes(
+ static int getNumberOfUndownloadedEpisodes(
final List<FeedItem> queue, final List<FeedItem> unreadItems) {
int counter = 0;
for (FeedItem item : queue) {
@@ -449,7 +449,8 @@ public final class DBTasks {
/**
* Looks for undownloaded episodes in the queue or list of unread items and request a download if
* 1. Network is available
- * 2. There is free space in the episode cache
+ * 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.
@@ -458,107 +459,9 @@ public final class DBTasks {
* @return A Future that can be used for waiting for the methods completion.
*/
public static Future<?> autodownloadUndownloadedItems(final Context context, final long... mediaIds) {
- return autodownloadExec.submit(new Runnable() {
- @Override
- public void run() {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Performing auto-dl of undownloaded episodes");
- if (NetworkUtils.autodownloadNetworkAvailable(context)
- && UserPreferences.isEnableAutodownload()) {
- final List<FeedItem> queue = DBReader.getQueue(context);
- final List<FeedItem> unreadItems = DBReader
- .getUnreadItemsList(context);
-
- int undownloadedEpisodes = getNumberOfUndownloadedEpisodes(queue,
- unreadItems);
- int downloadedEpisodes = DBReader
- .getNumberOfDownloadedEpisodes(context);
- int deletedEpisodes = performAutoCleanup(context,
- getPerformAutoCleanupArgs(context, undownloadedEpisodes));
- int episodeSpaceLeft = undownloadedEpisodes;
- boolean cacheIsUnlimited = UserPreferences.getEpisodeCacheSize() == UserPreferences
- .getEpisodeCacheSizeUnlimited();
-
- if (!cacheIsUnlimited
- && UserPreferences.getEpisodeCacheSize() < downloadedEpisodes
- + undownloadedEpisodes) {
- episodeSpaceLeft = UserPreferences.getEpisodeCacheSize()
- - (downloadedEpisodes - deletedEpisodes);
- }
-
- Arrays.sort(mediaIds); // sort for binary search
- final boolean ignoreMediaIds = mediaIds.length == 0;
- List<FeedItem> itemsToDownload = new ArrayList<FeedItem>();
-
- if (episodeSpaceLeft > 0 && undownloadedEpisodes > 0) {
- for (int i = 0; i < queue.size(); i++) { // ignore playing item
- FeedItem item = queue.get(i);
- long mediaId = (item.hasMedia()) ? item.getMedia().getId() : -1;
- if ((ignoreMediaIds || Arrays.binarySearch(mediaIds, mediaId) >= 0)
- && item.hasMedia()
- && !item.getMedia().isDownloaded()
- && !item.getMedia().isPlaying()
- && item.getFeed().getPreferences().getAutoDownload()) {
- itemsToDownload.add(item);
- episodeSpaceLeft--;
- undownloadedEpisodes--;
- if (episodeSpaceLeft == 0 || undownloadedEpisodes == 0) {
- break;
- }
- }
- }
- }
-
- if (episodeSpaceLeft > 0 && undownloadedEpisodes > 0) {
- for (FeedItem item : unreadItems) {
- long mediaId = (item.hasMedia()) ? item.getMedia().getId() : -1;
- if ((ignoreMediaIds || Arrays.binarySearch(mediaIds, mediaId) >= 0)
- && item.hasMedia()
- && !item.getMedia().isDownloaded()
- && item.getFeed().getPreferences().getAutoDownload()) {
- itemsToDownload.add(item);
- episodeSpaceLeft--;
- undownloadedEpisodes--;
- if (episodeSpaceLeft == 0 || undownloadedEpisodes == 0) {
- break;
- }
- }
- }
- }
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Enqueueing " + itemsToDownload.size()
- + " items for download");
-
- try {
- downloadFeedItems(false, context,
- itemsToDownload.toArray(new FeedItem[itemsToDownload
- .size()])
- );
- } catch (DownloadRequestException e) {
- e.printStackTrace();
- }
+ return autodownloadExec.submit(ClientConfig.dbTasksCallbacks.getAutomaticDownloadAlgorithm()
+ .autoDownloadUndownloadedItems(context, mediaIds));
- }
- }
- });
-
- }
-
- private 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;
}
/**
@@ -570,63 +473,8 @@ public final class DBTasks {
* @param context Used for accessing the DB.
*/
public static void performAutoCleanup(final Context context) {
- performAutoCleanup(context, getPerformAutoCleanupArgs(context, 0));
- }
-
- private static int performAutoCleanup(final Context context,
- final int episodeNumber) {
- List<FeedItem> candidates = new ArrayList<FeedItem>();
- List<FeedItem> downloadedItems = DBReader.getDownloadedItems(context);
- QueueAccess queue = QueueAccess.IDListAccess(DBReader.getQueueIDList(context));
- List<FeedItem> delete;
- for (FeedItem item : downloadedItems) {
- if (item.hasMedia() && item.getMedia().isDownloaded()
- && !queue.contains(item.getId()) && item.isRead()) {
- 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();
-
- if (l == null) {
- l = new Date(0);
- }
- if (r == null) {
- r = new Date(0);
- }
- return l.compareTo(r);
- }
- });
-
- if (candidates.size() > episodeNumber) {
- delete = candidates.subList(0, episodeNumber);
- } else {
- delete = candidates;
- }
-
- for (FeedItem item : delete) {
- try {
- DBWriter.deleteFeedMediaOfItem(context, item.getMedia().getId()).get();
- } catch (InterruptedException e) {
- e.printStackTrace();
- } catch (ExecutionException e) {
- e.printStackTrace();
- }
- }
-
- int counter = delete.size();
-
- if (BuildConfig.DEBUG)
- Log.d(TAG, String.format(
- "Auto-delete deleted %d episodes (%d requested)", counter,
- episodeNumber));
-
- return counter;
+ ClientConfig.dbTasksCallbacks.getEpisodeCacheCleanupAlgorithm().performCleanup(context,
+ ClientConfig.dbTasksCallbacks.getEpisodeCacheCleanupAlgorithm().getDefaultCleanupParameter(context));
}
/**
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 f5ee9e28c..d0cdad649 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
@@ -92,8 +92,9 @@ public class DownloadRequester {
private void download(Context context, FeedFile item, FeedFile container, File dest,
boolean overwriteIfExists, String username, String password, boolean deleteOnFailure, Bundle arguments) {
+ final boolean partiallyDownloadedFileExists = item.getFile_url() != null;
if (!isDownloadingFile(item)) {
- if (!isFilenameAvailable(dest.toString()) || (deleteOnFailure && dest.exists())) {
+ if (!isFilenameAvailable(dest.toString()) || (!partiallyDownloadedFileExists && dest.exists())) {
if (BuildConfig.DEBUG)
Log.d(TAG, "Filename already used.");
if (isFilenameAvailable(dest.toString()) && overwriteIfExists) {
@@ -254,8 +255,7 @@ public class DownloadRequester {
* Cancels all running downloads
*/
public synchronized void cancelAllDownloads(Context context) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Cancelling all running downloads");
+ Log.d(TAG, "Cancelling all running downloads");
context.sendBroadcast(new Intent(
DownloadService.ACTION_CANCEL_ALL_DOWNLOADS));
}
@@ -377,10 +377,13 @@ public class DownloadRequester {
String URLBaseFilename = URLUtil.guessFileName(media.getDownload_url(),
null, media.getMime_type());
- ;
- if (titleBaseFilename != "") {
+ if (!titleBaseFilename.equals("")) {
// Append extension
+ final int FILENAME_MAX_LENGTH = 220;
+ if (titleBaseFilename.length() > FILENAME_MAX_LENGTH) {
+ titleBaseFilename = titleBaseFilename.substring(0, FILENAME_MAX_LENGTH);
+ }
filename = titleBaseFilename + FilenameUtils.EXTENSION_SEPARATOR +
FilenameUtils.getExtension(URLBaseFilename);
} else {
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
new file mode 100644
index 000000000..6a8b4a441
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/EpisodeCleanupAlgorithm.java
@@ -0,0 +1,36 @@
+package de.danoeh.antennapod.core.storage;
+
+import android.content.Context;
+
+import java.util.List;
+
+import de.danoeh.antennapod.core.feed.FeedItem;
+
+public interface EpisodeCleanupAlgorithm<T> {
+
+ /**
+ * 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.
+ * @return The number of episodes that were deleted.
+ */
+ public int performCleanup(Context context, T parameter);
+
+ /**
+ * 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);
+
+ /**
+ * Returns a parameter for performCleanup.
+ *
+ * @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.
+ */
+ public T getPerformCleanupParameter(Context context, List<FeedItem> items);
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/PowerUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/PowerUtils.java
new file mode 100644
index 000000000..39deea36a
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/PowerUtils.java
@@ -0,0 +1,32 @@
+package de.danoeh.antennapod.core.util;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.BatteryManager;
+
+/**
+ * Created by Tom on 1/5/15.
+ */
+public class PowerUtils {
+
+ private static final String TAG = "PowerUtils";
+
+ private PowerUtils() {
+
+ }
+
+ /**
+ * @return true if the device is charging
+ */
+ public static boolean deviceCharging(Context context) {
+ // from http://developer.android.com/training/monitoring-device-state/battery-monitoring.html
+ IntentFilter iFilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
+ Intent batteryStatus = context.registerReceiver(null, iFilter);
+
+ int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
+ return (status == BatteryManager.BATTERY_STATUS_CHARGING ||
+ status == BatteryManager.BATTERY_STATUS_FULL);
+
+ }
+}
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 443ff0ad1..f31297b41 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
@@ -28,7 +28,7 @@ import de.danoeh.antennapod.core.util.ShownotesProvider;
public class Timeline {
private static final String TAG = "Timeline";
- private static final String WEBVIEW_STYLE = "@font-face { font-family: 'Roboto-Light'; src: url('file:///android_asset/Roboto-Light.ttf'); } * { color: %s; font-family: roboto-Light; font-size: 11pt; } a { font-style: normal; text-decoration: none; font-weight: normal; color: #00A8DF; } a.timecode { color: #669900; } img { display: block; margin: 10 auto; max-width: %s; height: auto; } body { margin: %dpx %dpx %dpx %dpx; }";
+ private static final String WEBVIEW_STYLE = "@font-face { font-family: 'Roboto-Light'; src: url('file:///android_asset/Roboto-Light.ttf'); } * { color: %s; font-family: roboto-Light; font-size: 13pt; } a { font-style: normal; text-decoration: none; font-weight: normal; color: #00A8DF; } a.timecode { color: #669900; } img { display: block; margin: 10 auto; max-width: %s; height: auto; } body { margin: %dpx %dpx %dpx %dpx; }";
private ShownotesProvider shownotesProvider;
diff --git a/core/src/main/res/drawable-hdpi/ic_settings_grey600_24dp.png b/core/src/main/res/drawable-hdpi/ic_settings_grey600_24dp.png
new file mode 100644
index 000000000..20d2b66e0
--- /dev/null
+++ b/core/src/main/res/drawable-hdpi/ic_settings_grey600_24dp.png
Binary files differ
diff --git a/core/src/main/res/drawable-hdpi/ic_settings_white_24dp.png b/core/src/main/res/drawable-hdpi/ic_settings_white_24dp.png
new file mode 100644
index 000000000..f9a8915fd
--- /dev/null
+++ b/core/src/main/res/drawable-hdpi/ic_settings_white_24dp.png
Binary files differ
diff --git a/core/src/main/res/drawable-xhdpi/ic_settings_grey600_24dp.png b/core/src/main/res/drawable-xhdpi/ic_settings_grey600_24dp.png
new file mode 100644
index 000000000..2251d2bbb
--- /dev/null
+++ b/core/src/main/res/drawable-xhdpi/ic_settings_grey600_24dp.png
Binary files differ
diff --git a/core/src/main/res/drawable-xhdpi/ic_settings_white_24dp.png b/core/src/main/res/drawable-xhdpi/ic_settings_white_24dp.png
new file mode 100644
index 000000000..12e5d100d
--- /dev/null
+++ b/core/src/main/res/drawable-xhdpi/ic_settings_white_24dp.png
Binary files differ
diff --git a/core/src/main/res/drawable-xxhdpi/ic_settings_grey600_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_settings_grey600_24dp.png
new file mode 100644
index 000000000..6a70402b4
--- /dev/null
+++ b/core/src/main/res/drawable-xxhdpi/ic_settings_grey600_24dp.png
Binary files differ
diff --git a/core/src/main/res/drawable-xxhdpi/ic_settings_white_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_settings_white_24dp.png
new file mode 100644
index 000000000..6bb8f6e08
--- /dev/null
+++ b/core/src/main/res/drawable-xxhdpi/ic_settings_white_24dp.png
Binary files differ
diff --git a/core/src/main/res/layout/refresh_action_view.xml b/core/src/main/res/layout/refresh_action_view.xml
new file mode 100644
index 000000000..66148a553
--- /dev/null
+++ b/core/src/main/res/layout/refresh_action_view.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ProgressBar xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="end"
+ android:indeterminateOnly="true">
+
+</ProgressBar> \ No newline at end of file
diff --git a/core/src/main/res/values/attrs.xml b/core/src/main/res/values/attrs.xml
index c46a2118f..f36119c8d 100644
--- a/core/src/main/res/values/attrs.xml
+++ b/core/src/main/res/values/attrs.xml
@@ -35,6 +35,7 @@
<attr name="av_pause_big" format="reference"/>
<attr name="av_ff_big" format="reference"/>
<attr name="av_rew_big" format="reference"/>
+ <attr name="ic_settings" format="reference"/>
<!-- Used in itemdescription -->
<attr name="non_transparent_background" format="reference"/>
diff --git a/core/src/main/res/values/colors.xml b/core/src/main/res/values/colors.xml
index ab48fafe7..e558a5c4e 100644
--- a/core/src/main/res/values/colors.xml
+++ b/core/src/main/res/values/colors.xml
@@ -11,11 +11,12 @@
<color name="download_failed_red">#CC0000</color>
<color name="status_progress">#E033B5E5</color>
<color name="status_playing">#E0EE5F52</color>
- <color name="overlay_dark">#262C31</color>
- <color name="overlay_light">#DDDDDD</color>
+ <color name="overlay_dark">#2C2C2C</color>
+ <color name="overlay_light">#FFFFFF</color>
<color name="swipe_refresh_secondary_color_light">#EDEDED</color>
<color name="swipe_refresh_secondary_color_dark">#060708</color>
<color name="new_indicator_green">#669900</color>
+ <color name="image_readability_tint">#80000000</color>
<!-- Use Gingerbread-orange -->
<color name="selection_background_color_dark">#FEBB20</color>
diff --git a/core/src/main/res/values/dimens.xml b/core/src/main/res/values/dimens.xml
index 38c14b024..81a55142a 100644
--- a/core/src/main/res/values/dimens.xml
+++ b/core/src/main/res/values/dimens.xml
@@ -33,4 +33,6 @@
<dimen name="listitem_icon_leftpadding">16dp</dimen>
<dimen name="listitem_icon_rightpadding">16dp</dimen>
+ <dimen name="audioplayer_playercontrols_length">64dp</dimen>
+
</resources> \ No newline at end of file
diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml
index 532b5d75d..86208becb 100644
--- a/core/src/main/res/values/strings.xml
+++ b/core/src/main/res/values/strings.xml
@@ -205,6 +205,7 @@
<string name="services_label">Services</string>
<string name="flattr_label">Flattr</string>
<string name="pref_pauseOnHeadsetDisconnect_sum">Pause playback when the headphones are disconnected</string>
+ <string name="pref_unpauseOnHeadsetReconnect_sum">Resume playback when the headphones are reconnected</string>
<string name="pref_followQueue_sum">Jump to next queue item when playback completes</string>
<string name="playback_pref">Playback</string>
<string name="network_pref">Network</string>
@@ -214,6 +215,7 @@
<string name="pref_followQueue_title">Continuous playback</string>
<string name="pref_downloadMediaOnWifiOnly_title">WiFi media download</string>
<string name="pref_pauseOnHeadsetDisconnect_title">Headphones disconnect</string>
+ <string name="pref_unpauseOnHeadsetReconnect_title">Headphones reconnect</string>
<string name="pref_mobileUpdate_title">Mobile updates</string>
<string name="pref_mobileUpdate_sum">Allow updates over the mobile data connection</string>
<string name="refreshing_label">Refreshing</string>
@@ -233,6 +235,8 @@
<string name="pref_automatic_download_sum">Configure the automatic download of episodes.</string>
<string name="pref_autodl_wifi_filter_title">Enable Wi-Fi filter</string>
<string name="pref_autodl_wifi_filter_sum">Allow automatic download only for selected Wi-Fi networks.</string>
+ <string name="pref_automatic_download_on_battery_title">Automatic download on battery</string>
+ <string name="pref_automatic_download_on_battery_sum">Allow automatic download while on battery</string>
<string name="pref_episode_cache_title">Episode cache</string>
<string name="pref_theme_title_light">Light</string>
<string name="pref_theme_title_dark">Dark</string>
diff --git a/core/src/main/res/values/styles.xml b/core/src/main/res/values/styles.xml
index df7ae385c..a2f180395 100644
--- a/core/src/main/res/values/styles.xml
+++ b/core/src/main/res/values/styles.xml
@@ -40,6 +40,7 @@
<item name="attr/av_pause_big">@drawable/ic_pause_grey600_36dp</item>
<item name="attr/av_ff_big">@drawable/ic_fast_forward_grey600_36dp</item>
<item name="attr/av_rew_big">@drawable/ic_fast_rewind_grey600_36dp</item>
+ <item name="attr/ic_settings">@drawable/ic_settings_grey600_24dp</item>
</style>
<style name="Theme.AntennaPod.Dark" parent="@style/Theme.AppCompat">
@@ -80,6 +81,94 @@
<item name="attr/av_pause_big">@drawable/ic_pause_white_36dp</item>
<item name="attr/av_ff_big">@drawable/ic_fast_forward_white_36dp</item>
<item name="attr/av_rew_big">@drawable/ic_fast_rewind_white_36dp</item>
+ <item name="attr/ic_settings">@drawable/ic_settings_white_24dp</item>
+ </style>
+
+ <style name="Theme.AntennaPod.Light.NoTitle" parent="@style/Theme.AppCompat.Light.NoActionBar">
+ <item name="windowActionBar">false</item>
+ <item name="windowActionModeOverlay">true</item>
+ <item name="colorPrimary">@color/primary_light</item>
+ <item name="colorAccent">@color/color_accent</item>
+ <item name="attr/action_about">@drawable/ic_info_grey600_24dp</item>
+ <item name="attr/action_search">@drawable/ic_search_grey600_24dp</item>
+ <item name="attr/action_stream">@drawable/ic_settings_input_antenna_grey600_24dp</item>
+ <item name="attr/av_download">@drawable/ic_file_download_grey600_24dp</item>
+ <item name="attr/av_fast_forward">@drawable/ic_fast_forward_grey600_24dp</item>
+ <item name="attr/av_pause">@drawable/ic_pause_grey600_24dp</item>
+ <item name="attr/av_play">@drawable/ic_play_arrow_grey600_24dp</item>
+ <item name="attr/av_rewind">@drawable/ic_fast_rewind_grey600_24dp</item>
+ <item name="attr/content_discard">@drawable/ic_delete_grey600_24dp</item>
+ <item name="attr/content_new">@drawable/ic_add_grey600_24dp</item>
+ <item name="attr/device_access_time">@drawable/ic_timer_grey600_24dp</item>
+ <item name="attr/location_web_site">@drawable/ic_web_grey600_24dp</item>
+ <item name="attr/navigation_accept">@drawable/ic_done_grey600_24dp</item>
+ <item name="attr/navigation_cancel">@drawable/ic_cancel_grey600_24dp</item>
+ <item name="attr/navigation_expand">@drawable/ic_expand_more_grey600_36dp</item>
+ <item name="attr/navigation_refresh">@drawable/ic_refresh_grey600_24dp</item>
+ <item name="attr/navigation_up">@drawable/navigation_up</item>
+ <item name="attr/navigation_shownotes">@drawable/ic_description_grey600_36dp</item>
+ <item name="attr/navigation_chapters">@drawable/ic_toc_grey600_36dp</item>
+ <item name="attr/social_share">@drawable/ic_share_grey600_24dp</item>
+ <item name="attr/stat_playlist">@drawable/ic_list_grey600_24dp</item>
+ <item name="attr/type_audio">@drawable/ic_hearing_grey600_18dp</item>
+ <item name="attr/type_video">@drawable/ic_remove_red_eye_grey600_18dp</item>
+ <item name="attr/non_transparent_background">@color/white</item>
+ <item name="attr/overlay_background">@color/overlay_light</item>
+ <item name="attr/overlay_drawable">@drawable/overlay_drawable</item>
+ <item name="attr/dragview_background">@drawable/ic_drag_handle</item>
+ <item name="attr/dragview_float_background">@color/white</item>
+ <item name="attr/nav_drawer_background">@color/white</item>
+ <item name="attr/ic_action_overflow">@drawable/ic_more_vert_grey600_24dp</item>
+ <item name="attr/ic_new">@drawable/ic_new_releases_grey600_24dp</item>
+ <item name="attr/ic_history">@drawable/ic_history_grey600_24dp</item>
+ <item name="attr/av_play_big">@drawable/ic_play_arrow_grey600_36dp</item>
+ <item name="attr/av_pause_big">@drawable/ic_pause_grey600_36dp</item>
+ <item name="attr/av_ff_big">@drawable/ic_fast_forward_grey600_36dp</item>
+ <item name="attr/av_rew_big">@drawable/ic_fast_rewind_grey600_36dp</item>
+ <item name="attr/ic_settings">@drawable/ic_settings_grey600_24dp</item>
+ </style>
+
+ <style name="Theme.AntennaPod.Dark.NoTitle" parent="@style/Theme.AppCompat.NoActionBar">
+ <item name="windowActionBar">false</item>
+ <item name="windowActionModeOverlay">true</item>
+ <item name="colorAccent">@color/color_accent</item>
+ <item name="attr/action_about">@drawable/ic_info_white_24dp</item>
+ <item name="attr/action_search">@drawable/ic_search_white_24dp</item>
+ <item name="attr/action_stream">@drawable/ic_settings_input_antenna_white_24dp</item>
+ <item name="attr/av_download">@drawable/ic_file_download_white_24dp</item>
+ <item name="attr/av_fast_forward">@drawable/ic_fast_forward_white_24dp</item>
+ <item name="attr/av_pause">@drawable/ic_pause_white_24dp</item>
+ <item name="attr/av_play">@drawable/ic_play_arrow_white_24dp</item>
+ <item name="attr/av_rewind">@drawable/ic_fast_rewind_white_24dp</item>
+ <item name="attr/content_discard">@drawable/ic_delete_white_24dp</item>
+ <item name="attr/content_new">@drawable/ic_add_white_24dp</item>
+ <item name="attr/device_access_time">@drawable/ic_timer_white_24dp</item>
+ <item name="attr/location_web_site">@drawable/ic_web_white_24dp</item>
+ <item name="attr/navigation_accept">@drawable/ic_done_white_24dp</item>
+ <item name="attr/navigation_cancel">@drawable/ic_cancel_white_24dp</item>
+ <item name="attr/navigation_expand">@drawable/ic_expand_more_white_36dp</item>
+ <item name="attr/navigation_refresh">@drawable/ic_refresh_white_24dp</item>
+ <item name="attr/navigation_up">@drawable/navigation_up_dark</item>
+ <item name="attr/navigation_shownotes">@drawable/ic_description_white_36dp</item>
+ <item name="attr/navigation_chapters">@drawable/ic_toc_white_36dp</item>
+ <item name="attr/social_share">@drawable/ic_share_white_24dp</item>
+ <item name="attr/stat_playlist">@drawable/ic_list_white_24dp</item>
+ <item name="attr/type_audio">@drawable/ic_hearing_white_18dp</item>
+ <item name="attr/type_video">@drawable/ic_remove_red_eye_white_18dp</item>
+ <item name="attr/non_transparent_background">@color/black</item>
+ <item name="attr/overlay_background">@color/overlay_dark</item>
+ <item name="attr/overlay_drawable">@drawable/overlay_drawable_dark</item>
+ <item name="attr/dragview_background">@drawable/ic_drag_handle_dark</item>
+ <item name="attr/dragview_float_background">@color/black</item>
+ <item name="attr/nav_drawer_background">#3B3B3B</item>
+ <item name="attr/ic_action_overflow">@drawable/ic_more_vert_white_24dp</item>
+ <item name="attr/ic_new">@drawable/ic_new_releases_white_24dp</item>
+ <item name="attr/ic_history">@drawable/ic_history_white_24dp</item>
+ <item name="attr/av_play_big">@drawable/ic_play_arrow_white_36dp</item>
+ <item name="attr/av_pause_big">@drawable/ic_pause_white_36dp</item>
+ <item name="attr/av_ff_big">@drawable/ic_fast_forward_white_36dp</item>
+ <item name="attr/av_rew_big">@drawable/ic_fast_rewind_white_36dp</item>
+ <item name="attr/ic_settings">@drawable/ic_settings_white_24dp</item>
</style>
<style name="Theme.AntennaPod.VideoPlayer" parent="@style/Theme.AntennaPod.Dark">
@@ -157,4 +246,10 @@
<item name="android:text">@string/new_label</item>
</style>
+ <style name="BigBlurryBackground">
+ <item name="android:scaleType">centerCrop</item>
+ <item name="android:tint">@color/image_readability_tint</item>
+
+ </style>
+
</resources>