summaryrefslogtreecommitdiff
path: root/core/src
diff options
context:
space:
mode:
Diffstat (limited to 'core/src')
-rw-r--r--core/src/main/AndroidManifest.xml7
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrClickWorker.java5
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/event/ServiceEvent.java13
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java53
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java9
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/receiver/MediaButtonReceiver.java3
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/receiver/PlayerWidget.java56
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/FeedUpdateJobService.java34
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/GpodnetSyncService.java114
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/PlayerWidgetJobService.java176
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java10
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java97
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java87
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java3
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/FeedUpdateUtils.java21
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/NotificationUtils.java72
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/download/AutoUpdateManager.java113
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java20
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java101
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackServiceStarter.java76
-rw-r--r--core/src/main/res/layout/player_widget.xml52
-rw-r--r--core/src/main/res/values/strings.xml13
22 files changed, 894 insertions, 241 deletions
diff --git a/core/src/main/AndroidManifest.xml b/core/src/main/AndroidManifest.xml
index 3257c13a7..951bd2224 100644
--- a/core/src/main/AndroidManifest.xml
+++ b/core/src/main/AndroidManifest.xml
@@ -27,6 +27,7 @@
</service>
<service
android:name=".service.GpodnetSyncService"
+ android:permission="android.permission.BIND_JOB_SERVICE"
android:enabled="true" />
<receiver
@@ -55,6 +56,12 @@
<receiver android:name=".receiver.FeedUpdateReceiver">
</receiver>
+ <service
+ android:name=".service.FeedUpdateJobService"
+ android:permission="android.permission.BIND_JOB_SERVICE" >
+
+ </service>
+
</application>
</manifest>
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 5bd65f4e9..627e601bd 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
@@ -10,6 +10,7 @@ import android.support.v4.app.NotificationCompat;
import android.util.Log;
import android.widget.Toast;
+import de.danoeh.antennapod.core.util.NotificationUtils;
import org.shredzone.flattr4j.exception.FlattrException;
import java.util.LinkedList;
@@ -175,7 +176,7 @@ public class FlattrClickWorker extends AsyncTask<Void, Integer, FlattrClickWorke
PendingIntent contentIntent = PendingIntent.getActivity(context, 0,
ClientConfig.flattrCallbacks.getFlattrAuthenticationActivityIntent(context), 0);
- Notification notification = new NotificationCompat.Builder(context)
+ Notification notification = new NotificationCompat.Builder(context, NotificationUtils.CHANNEL_ID_ERROR)
.setStyle(new NotificationCompat.BigTextStyle().bigText(context.getString(R.string.no_flattr_token_notification_msg)))
.setContentIntent(contentIntent)
.setContentTitle(context.getString(R.string.no_flattr_token_title))
@@ -208,7 +209,7 @@ public class FlattrClickWorker extends AsyncTask<Void, Integer, FlattrClickWorke
+ context.getString(R.string.flattr_click_failure_count, failed);
}
- Notification notification = new NotificationCompat.Builder(context)
+ Notification notification = new NotificationCompat.Builder(context, NotificationUtils.CHANNEL_ID_ERROR)
.setStyle(new NotificationCompat.BigTextStyle().bigText(subtext))
.setContentIntent(contentIntent)
.setContentTitle(title)
diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/ServiceEvent.java b/core/src/main/java/de/danoeh/antennapod/core/event/ServiceEvent.java
new file mode 100644
index 000000000..b3241a8b6
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/event/ServiceEvent.java
@@ -0,0 +1,13 @@
+package de.danoeh.antennapod.core.event;
+
+public class ServiceEvent {
+ public enum Action {
+ SERVICE_STARTED
+ }
+
+ public final Action action;
+
+ public ServiceEvent(Action action) {
+ this.action = action;
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java
index a93012d59..48efdc84c 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java
@@ -1,18 +1,22 @@
package de.danoeh.antennapod.core.preferences;
-import android.app.AlarmManager;
-import android.app.PendingIntent;
import android.content.Context;
-import android.content.Intent;
import android.content.SharedPreferences;
-import android.os.SystemClock;
+import android.os.Build;
import android.preference.PreferenceManager;
import android.support.annotation.IntRange;
import android.support.annotation.NonNull;
import android.support.v4.app.NotificationCompat;
import android.text.TextUtils;
import android.util.Log;
-
+import de.danoeh.antennapod.core.R;
+import de.danoeh.antennapod.core.service.download.ProxyConfig;
+import de.danoeh.antennapod.core.storage.APCleanupAlgorithm;
+import de.danoeh.antennapod.core.storage.APNullCleanupAlgorithm;
+import de.danoeh.antennapod.core.storage.APQueueCleanupAlgorithm;
+import de.danoeh.antennapod.core.storage.EpisodeCleanupAlgorithm;
+import de.danoeh.antennapod.core.util.Converter;
+import de.danoeh.antennapod.core.util.download.AutoUpdateManager;
import org.json.JSONArray;
import org.json.JSONException;
@@ -25,15 +29,6 @@ import java.util.Calendar;
import java.util.List;
import java.util.concurrent.TimeUnit;
-import de.danoeh.antennapod.core.R;
-import de.danoeh.antennapod.core.receiver.FeedUpdateReceiver;
-import de.danoeh.antennapod.core.service.download.ProxyConfig;
-import de.danoeh.antennapod.core.storage.APCleanupAlgorithm;
-import de.danoeh.antennapod.core.storage.APNullCleanupAlgorithm;
-import de.danoeh.antennapod.core.storage.APQueueCleanupAlgorithm;
-import de.danoeh.antennapod.core.storage.EpisodeCleanupAlgorithm;
-import de.danoeh.antennapod.core.util.Converter;
-
/**
* Provides access to preferences set by the user in the settings screen. A
* private instance of this class must first be instantiated via
@@ -797,17 +792,11 @@ public class UserPreferences {
*/
private static void restartUpdateIntervalAlarm(long triggerAtMillis, long intervalMillis) {
Log.d(TAG, "Restarting update alarm.");
- AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
- Intent intent = new Intent(context, FeedUpdateReceiver.class);
- PendingIntent updateIntent = PendingIntent.getBroadcast(context, 0, intent, 0);
- alarmManager.cancel(updateIntent);
- if (intervalMillis > 0) {
- alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
- SystemClock.elapsedRealtime() + triggerAtMillis,
- updateIntent);
- Log.d(TAG, "Changed alarm to new interval " + TimeUnit.MILLISECONDS.toHours(intervalMillis) + " h");
+
+ if (Build.VERSION.SDK_INT >= 24) {
+ AutoUpdateManager.restartJobServiceInterval(context, intervalMillis);
} else {
- Log.d(TAG, "Automatic update was deactivated");
+ AutoUpdateManager.restartAlarmManagerInterval(context, triggerAtMillis, intervalMillis);
}
}
@@ -816,10 +805,6 @@ public class UserPreferences {
*/
private 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();
@@ -828,11 +813,13 @@ public class UserPreferences {
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);
+
+ if (Build.VERSION.SDK_INT >= 24) {
+ long triggerAtMillis = alarm.getTimeInMillis() - now.getTimeInMillis();
+ AutoUpdateManager.restartJobServiceTriggerAt(context, triggerAtMillis);
+ } else {
+ AutoUpdateManager.restartAlarmManagerTimeOfDay(context, alarm);
+ }
}
/**
diff --git a/core/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java b/core/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java
index 9bbeb7c88..05e12f6df 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java
@@ -7,8 +7,7 @@ import android.util.Log;
import de.danoeh.antennapod.core.ClientConfig;
import de.danoeh.antennapod.core.preferences.UserPreferences;
-import de.danoeh.antennapod.core.storage.DBTasks;
-import de.danoeh.antennapod.core.util.NetworkUtils;
+import de.danoeh.antennapod.core.util.FeedUpdateUtils;
/**
* Refreshes all feeds when it receives an intent
@@ -21,11 +20,7 @@ public class FeedUpdateReceiver extends BroadcastReceiver {
public void onReceive(Context context, Intent intent) {
Log.d(TAG, "Received intent");
ClientConfig.initialize(context);
- if (NetworkUtils.networkAvailable() && NetworkUtils.isDownloadAllowed()) {
- DBTasks.refreshAllFeeds(context, null);
- } else {
- Log.d(TAG, "Blocking automatic update: no wifi available / no mobile updates allowed");
- }
+ FeedUpdateUtils.startAutoUpdate(context, null);
UserPreferences.restartUpdateAlarm(false);
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/receiver/MediaButtonReceiver.java b/core/src/main/java/de/danoeh/antennapod/core/receiver/MediaButtonReceiver.java
index 9b4b91151..b191dbf8b 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/receiver/MediaButtonReceiver.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/receiver/MediaButtonReceiver.java
@@ -3,6 +3,7 @@ package de.danoeh.antennapod.core.receiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
+import android.support.v4.content.ContextCompat;
import android.util.Log;
import android.view.KeyEvent;
@@ -29,7 +30,7 @@ public class MediaButtonReceiver extends BroadcastReceiver {
Intent serviceIntent = new Intent(context, PlaybackService.class);
serviceIntent.putExtra(EXTRA_KEYCODE, event.getKeyCode());
serviceIntent.putExtra(EXTRA_SOURCE, event.getSource());
- context.startService(serviceIntent);
+ ContextCompat.startForegroundService(context, serviceIntent);
}
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/receiver/PlayerWidget.java b/core/src/main/java/de/danoeh/antennapod/core/receiver/PlayerWidget.java
new file mode 100644
index 000000000..edc2ea3e0
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/receiver/PlayerWidget.java
@@ -0,0 +1,56 @@
+package de.danoeh.antennapod.core.receiver;
+
+import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProvider;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.util.Log;
+import de.danoeh.antennapod.core.service.PlayerWidgetJobService;
+
+import java.util.Arrays;
+
+
+public class PlayerWidget extends AppWidgetProvider {
+ private static final String TAG = "PlayerWidget";
+ private static final String PREFS_NAME = "PlayerWidgetPrefs";
+ private static final String KEY_ENABLED = "WidgetEnabled";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.d(TAG, "onReceive");
+ super.onReceive(context, intent);
+ PlayerWidgetJobService.updateWidget(context);
+ }
+
+ @Override
+ public void onEnabled(Context context) {
+ super.onEnabled(context);
+ Log.d(TAG, "Widget enabled");
+ setEnabled(context, true);
+ PlayerWidgetJobService.updateWidget(context);
+ }
+
+ @Override
+ public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
+ Log.d(TAG, "onUpdate() called with: " + "context = [" + context + "], appWidgetManager = [" + appWidgetManager + "], appWidgetIds = [" + Arrays.toString(appWidgetIds) + "]");
+ PlayerWidgetJobService.updateWidget(context);
+ }
+
+ @Override
+ public void onDisabled(Context context) {
+ super.onDisabled(context);
+ Log.d(TAG, "Widget disabled");
+ setEnabled(context, false);
+ }
+
+ public static boolean isEnabled(Context context) {
+ SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
+ return prefs.getBoolean(KEY_ENABLED, false);
+ }
+
+ private void setEnabled(Context context, boolean enabled) {
+ SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
+ prefs.edit().putBoolean(KEY_ENABLED, enabled).apply();
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/FeedUpdateJobService.java b/core/src/main/java/de/danoeh/antennapod/core/service/FeedUpdateJobService.java
new file mode 100644
index 000000000..55a8d6b86
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/FeedUpdateJobService.java
@@ -0,0 +1,34 @@
+package de.danoeh.antennapod.core.service;
+
+import android.app.job.JobParameters;
+import android.app.job.JobService;
+import android.os.Build;
+import android.support.annotation.RequiresApi;
+import android.util.Log;
+import de.danoeh.antennapod.core.ClientConfig;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+import de.danoeh.antennapod.core.util.FeedUpdateUtils;
+
+@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+public class FeedUpdateJobService extends JobService {
+ private static final String TAG = "FeedUpdateJobService";
+
+ @Override
+ public boolean onStartJob(JobParameters params) {
+ Log.d(TAG, "Job started");
+ ClientConfig.initialize(getApplicationContext());
+
+ FeedUpdateUtils.startAutoUpdate(getApplicationContext(), () -> {
+ UserPreferences.restartUpdateAlarm(false);
+ jobFinished(params, false); // needsReschedule = false
+ });
+
+ return true;
+ }
+
+ @Override
+ public boolean onStopJob(JobParameters params) {
+ return true;
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/GpodnetSyncService.java b/core/src/main/java/de/danoeh/antennapod/core/service/GpodnetSyncService.java
index a723097a2..94fadae02 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/GpodnetSyncService.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/GpodnetSyncService.java
@@ -3,11 +3,12 @@ package de.danoeh.antennapod.core.service;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
-import android.app.Service;
import android.content.Context;
import android.content.Intent;
-import android.os.IBinder;
+import android.support.annotation.NonNull;
+import android.support.v4.app.JobIntentService;
import android.support.v4.app.NotificationCompat;
+import android.support.v4.content.ContextCompat;
import android.support.v4.util.ArrayMap;
import android.util.Log;
import android.util.Pair;
@@ -15,6 +16,7 @@ import android.util.Pair;
import java.util.Collection;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
import de.danoeh.antennapod.core.ClientConfig;
import de.danoeh.antennapod.core.R;
@@ -37,12 +39,13 @@ import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.storage.DownloadRequestException;
import de.danoeh.antennapod.core.storage.DownloadRequester;
import de.danoeh.antennapod.core.util.NetworkUtils;
+import de.danoeh.antennapod.core.util.NotificationUtils;
/**
* Synchronizes local subscriptions with gpodder.net service. The service should be started with ACTION_SYNC as an action argument.
* This class also provides static methods for starting the GpodnetSyncService.
*/
-public class GpodnetSyncService extends Service {
+public class GpodnetSyncService extends JobIntentService {
private static final String TAG = "GpodnetSyncService";
private static final long WAIT_INTERVAL = 5000L;
@@ -55,12 +58,17 @@ public class GpodnetSyncService extends Service {
private GpodnetService service;
- private boolean syncSubscriptions = false;
- private boolean syncActions = false;
+ private static final AtomicInteger syncActionCount = new AtomicInteger(0);
+ private static boolean syncSubscriptions = false;
+ private static boolean syncActions = false;
+
+ private static void enqueueWork(Context context, Intent intent) {
+ enqueueWork(context, GpodnetSyncService.class, 0, intent);
+ }
@Override
- public int onStartCommand(Intent intent, int flags, int startId) {
- final String action = (intent != null) ? intent.getStringExtra(ARG_ACTION) : null;
+ protected void onHandleWork(@NonNull Intent intent) {
+ final String action = intent.getStringExtra(ARG_ACTION);
if (action != null) {
switch(action) {
case ACTION_SYNC:
@@ -78,24 +86,20 @@ public class GpodnetSyncService extends Service {
}
if(syncSubscriptions || syncActions) {
Log.d(TAG, String.format("Waiting %d milliseconds before uploading changes", WAIT_INTERVAL));
- syncWaiterThread.restart();
+ int syncActionId = syncActionCount.incrementAndGet();
+ try {
+ Thread.sleep(WAIT_INTERVAL);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ if (syncActionId == syncActionCount.get()) {
+ // onHandleWork was not called again in the meantime
+ sync();
+ }
}
} else {
Log.e(TAG, "Received invalid intent: action argument is null");
}
- return START_STICKY;
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- Log.d(TAG, "onDestroy");
- syncWaiterThread.interrupt();
- }
-
- @Override
- public IBinder onBind(Intent intent) {
- return null;
}
private synchronized GpodnetService tryLogin() throws GpodnetServiceException {
@@ -109,6 +113,7 @@ public class GpodnetSyncService extends Service {
private synchronized void sync() {
if (!GpodnetPreferences.loggedIn() || !NetworkUtils.networkAvailable()) {
+ stopForeground(true);
stopSelf();
return;
}
@@ -125,7 +130,6 @@ public class GpodnetSyncService extends Service {
}
syncActions = false;
}
- stopSelf();
}
private synchronized void syncSubscriptionChanges() {
@@ -319,7 +323,7 @@ public class GpodnetSyncService extends Service {
}
PendingIntent activityIntent = ClientConfig.gpodnetCallbacks.getGpodnetSyncServiceErrorNotificationPendingIntent(this);
- Notification notification = new NotificationCompat.Builder(this)
+ Notification notification = new NotificationCompat.Builder(this, NotificationUtils.CHANNEL_ID_ERROR)
.setContentTitle(title)
.setContentText(description)
.setContentIntent(activityIntent)
@@ -331,69 +335,11 @@ public class GpodnetSyncService extends Service {
nm.notify(id, notification);
}
- private final WaiterThread syncWaiterThread = new WaiterThread(WAIT_INTERVAL) {
- @Override
- public void onWaitCompleted() {
- sync();
- }
- };
-
- private abstract class WaiterThread {
- private final long waitInterval;
- private Thread thread;
-
- private WaiterThread(long waitInterval) {
- this.waitInterval = waitInterval;
- reinit();
- }
-
- public abstract void onWaitCompleted();
-
- public void exec() {
- if (!thread.isAlive()) {
- thread.start();
- }
- }
-
- private void reinit() {
- if (thread != null && thread.isAlive()) {
- Log.d(TAG, "Interrupting waiter thread");
- thread.interrupt();
- }
- thread = new Thread() {
- @Override
- public void run() {
- try {
- Thread.sleep(waitInterval);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- if (!isInterrupted()) {
- synchronized (this) {
- onWaitCompleted();
- }
- }
- }
- };
- }
-
- public void restart() {
- reinit();
- exec();
- }
-
- public void interrupt() {
- if (thread != null && thread.isAlive()) {
- thread.interrupt();
- }
- }
- }
-
public static void sendSyncIntent(Context context) {
if (GpodnetPreferences.loggedIn()) {
Intent intent = new Intent(context, GpodnetSyncService.class);
intent.putExtra(ARG_ACTION, ACTION_SYNC);
- context.startService(intent);
+ enqueueWork(context, intent);
}
}
@@ -401,7 +347,7 @@ public class GpodnetSyncService extends Service {
if (GpodnetPreferences.loggedIn()) {
Intent intent = new Intent(context, GpodnetSyncService.class);
intent.putExtra(ARG_ACTION, ACTION_SYNC_SUBSCRIPTIONS);
- context.startService(intent);
+ enqueueWork(context, intent);
}
}
@@ -409,7 +355,7 @@ public class GpodnetSyncService extends Service {
if (GpodnetPreferences.loggedIn()) {
Intent intent = new Intent(context, GpodnetSyncService.class);
intent.putExtra(ARG_ACTION, ACTION_SYNC_ACTIONS);
- context.startService(intent);
+ enqueueWork(context, intent);
}
}
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/PlayerWidgetJobService.java b/core/src/main/java/de/danoeh/antennapod/core/service/PlayerWidgetJobService.java
new file mode 100644
index 000000000..2fd790ac7
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/PlayerWidgetJobService.java
@@ -0,0 +1,176 @@
+package de.danoeh.antennapod.core.service;
+
+import android.app.PendingIntent;
+import android.appwidget.AppWidgetManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Build;
+import android.os.IBinder;
+import android.support.annotation.NonNull;
+import android.support.v4.app.JobIntentService;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.widget.RemoteViews;
+import de.danoeh.antennapod.core.R;
+import de.danoeh.antennapod.core.receiver.MediaButtonReceiver;
+import de.danoeh.antennapod.core.service.playback.PlaybackService;
+import de.danoeh.antennapod.core.service.playback.PlayerStatus;
+import de.danoeh.antennapod.core.util.Converter;
+import de.danoeh.antennapod.core.util.playback.Playable;
+import de.danoeh.antennapod.core.receiver.PlayerWidget;
+
+/**
+ * Updates the state of the player widget
+ */
+public class PlayerWidgetJobService extends JobIntentService {
+ private static final String TAG = "PlayerWidgetJobService";
+
+ private PlaybackService playbackService;
+ private final Object waitForService = new Object();
+
+ public static void updateWidget(Context context) {
+ enqueueWork(context, PlayerWidgetJobService.class, 0, new Intent(context, PlayerWidgetJobService.class));
+ }
+
+ @Override
+ protected void onHandleWork(@NonNull Intent intent) {
+ if (!PlayerWidget.isEnabled(getApplicationContext())) {
+ return;
+ }
+
+ if (PlaybackService.isRunning && playbackService == null) {
+ synchronized (waitForService) {
+ bindService(new Intent(this, PlaybackService.class), mConnection, 0);
+ while (playbackService == null) {
+ try {
+ waitForService.wait();
+ } catch (InterruptedException e) {
+ return;
+ }
+ }
+ }
+ }
+
+ updateViews();
+
+ if (playbackService != null) {
+ try {
+ unbindService(mConnection);
+ } catch (IllegalArgumentException e) {
+ Log.w(TAG, "IllegalArgumentException when trying to unbind service");
+ }
+ }
+ }
+
+ private void updateViews() {
+
+ ComponentName playerWidget = new ComponentName(this, PlayerWidget.class);
+ AppWidgetManager manager = AppWidgetManager.getInstance(this);
+ RemoteViews views = new RemoteViews(getPackageName(), R.layout.player_widget);
+ PendingIntent startMediaplayer = PendingIntent.getActivity(this, 0,
+ PlaybackService.getPlayerActivityIntent(this), 0);
+
+ final PendingIntent startAppPending = PendingIntent.getActivity(this, 0,
+ PlaybackService.getPlayerActivityIntent(this),
+ PendingIntent.FLAG_UPDATE_CURRENT);
+
+ boolean nothingPlaying = false;
+ Playable media;
+ PlayerStatus status;
+ if (playbackService != null) {
+ media = playbackService.getPlayable();
+ status = playbackService.getStatus();
+ } else {
+ media = Playable.PlayableUtils.createInstanceFromPreferences(getApplicationContext());
+ status = PlayerStatus.STOPPED;
+ }
+
+ if (media != null) {
+ views.setOnClickPendingIntent(R.id.layout_left, startMediaplayer);
+
+ views.setTextViewText(R.id.txtvTitle, media.getEpisodeTitle());
+
+ String progressString;
+ if (playbackService != null) {
+ progressString = getProgressString(playbackService.getCurrentPosition(), playbackService.getDuration());
+ } else {
+ progressString = getProgressString(media.getPosition(), media.getDuration());
+ }
+
+ if (progressString != null) {
+ views.setViewVisibility(R.id.txtvProgress, View.VISIBLE);
+ views.setTextViewText(R.id.txtvProgress, progressString);
+ }
+
+ if (status == PlayerStatus.PLAYING) {
+ views.setImageViewResource(R.id.butPlay, R.drawable.ic_pause_white_24dp);
+ if (Build.VERSION.SDK_INT >= 15) {
+ views.setContentDescription(R.id.butPlay, getString(R.string.pause_label));
+ }
+ } else {
+ views.setImageViewResource(R.id.butPlay, R.drawable.ic_play_arrow_white_24dp);
+ if (Build.VERSION.SDK_INT >= 15) {
+ views.setContentDescription(R.id.butPlay, getString(R.string.play_label));
+ }
+ }
+ views.setOnClickPendingIntent(R.id.butPlay, createMediaButtonIntent());
+ } else {
+ nothingPlaying = true;
+ }
+
+ if (nothingPlaying) {
+ // start the app if they click anything
+ views.setOnClickPendingIntent(R.id.layout_left, startAppPending);
+ views.setOnClickPendingIntent(R.id.butPlay, startAppPending);
+ views.setViewVisibility(R.id.txtvProgress, View.INVISIBLE);
+ views.setTextViewText(R.id.txtvTitle,
+ this.getString(R.string.no_media_playing_label));
+ views.setImageViewResource(R.id.butPlay, R.drawable.ic_play_arrow_white_24dp);
+ }
+
+ manager.updateAppWidget(playerWidget, views);
+ }
+
+ /**
+ * Creates an intent which fakes a mediabutton press
+ */
+ private PendingIntent createMediaButtonIntent() {
+ KeyEvent event = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE);
+ Intent startingIntent = new Intent(getBaseContext(), MediaButtonReceiver.class);
+ startingIntent.setAction(MediaButtonReceiver.NOTIFY_BUTTON_RECEIVER);
+ startingIntent.putExtra(Intent.EXTRA_KEY_EVENT, event);
+
+ return PendingIntent.getBroadcast(this, 0, startingIntent, 0);
+ }
+
+ private String getProgressString(int position, int duration) {
+ if (position > 0 && duration > 0) {
+ return Converter.getDurationStringLong(position) + " / "
+ + Converter.getDurationStringLong(duration);
+ } else {
+ return null;
+ }
+ }
+
+ private final ServiceConnection mConnection = new ServiceConnection() {
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ Log.d(TAG, "Connection to service established");
+ if (service instanceof PlaybackService.LocalBinder) {
+ synchronized (waitForService) {
+ playbackService = ((PlaybackService.LocalBinder) service).getService();
+ waitForService.notifyAll();
+ }
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ playbackService = null;
+ Log.d(TAG, "Disconnected from service");
+ }
+
+ };
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java
index a62c9d8bf..34cabf564 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
@@ -22,6 +22,7 @@ import android.util.Log;
import android.util.Pair;
import android.webkit.URLUtil;
+import de.danoeh.antennapod.core.util.NotificationUtils;
import org.apache.commons.io.FileUtils;
import org.xml.sax.SAXException;
@@ -295,6 +296,7 @@ public class DownloadService extends Service {
setupNotificationBuilders();
requester = DownloadRequester.getInstance();
+ startForeground(NOTIFICATION_ID, updateNotifications());
}
@Override
@@ -339,7 +341,7 @@ public class DownloadService extends Service {
}
private void setupNotificationBuilders() {
- notificationCompatBuilder = new NotificationCompat.Builder(this)
+ notificationCompatBuilder = new NotificationCompat.Builder(this, NotificationUtils.CHANNEL_ID_DOWNLOADING)
.setOngoing(true)
.setContentIntent(ClientConfig.downloadServiceCallbacks.getNotificationContentIntent(this))
.setSmallIcon(R.drawable.stat_notify_sync);
@@ -352,7 +354,7 @@ public class DownloadService extends Service {
/**
* Updates the contents of the service's notifications. Should be called
- * before setupNotificationBuilders.
+ * after setupNotificationBuilders.
*/
private Notification updateNotifications() {
if (notificationCompatBuilder == null) {
@@ -499,7 +501,7 @@ public class DownloadService extends Service {
if (createReport) {
Log.d(TAG, "Creating report");
// create notification object
- NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(this, NotificationUtils.CHANNEL_ID_ERROR)
.setTicker(getString(R.string.download_report_title))
.setContentTitle(getString(R.string.download_report_content_title))
.setContentText(
@@ -551,7 +553,7 @@ public class DownloadService extends Service {
final String resourceTitle = (downloadRequest.getTitle() != null) ?
downloadRequest.getTitle() : downloadRequest.getSource();
- NotificationCompat.Builder builder = new NotificationCompat.Builder(DownloadService.this);
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(DownloadService.this, NotificationUtils.CHANNEL_ID_USER_ACTION);
builder.setTicker(getText(R.string.authentication_notification_title))
.setContentTitle(getText(R.string.authentication_notification_title))
.setContentText(getText(R.string.authentication_notification_msg))
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 0bd516f4e..c1d23c626 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
@@ -24,6 +24,7 @@ import android.os.Vibrator;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.annotation.StringRes;
+import android.support.v4.content.ContextCompat;
import android.support.v4.media.MediaBrowserCompat;
import android.support.v4.media.MediaBrowserServiceCompat;
import android.support.v4.media.MediaDescriptionCompat;
@@ -31,7 +32,7 @@ import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.support.v4.view.InputDeviceCompat;
-import android.support.v7.app.NotificationCompat;
+import android.support.v4.app.NotificationCompat;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
@@ -49,6 +50,7 @@ import java.util.List;
import de.danoeh.antennapod.core.ClientConfig;
import de.danoeh.antennapod.core.R;
import de.danoeh.antennapod.core.event.MessageEvent;
+import de.danoeh.antennapod.core.event.ServiceEvent;
import de.danoeh.antennapod.core.feed.Chapter;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.FeedItem;
@@ -60,11 +62,13 @@ import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
import de.danoeh.antennapod.core.preferences.SleepTimerPreferences;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.receiver.MediaButtonReceiver;
+import de.danoeh.antennapod.core.service.PlayerWidgetJobService;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBTasks;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.storage.FeedSearcher;
import de.danoeh.antennapod.core.util.IntList;
+import de.danoeh.antennapod.core.util.NotificationUtils;
import de.danoeh.antennapod.core.util.QueueAccess;
import de.danoeh.antennapod.core.util.playback.ExternalMedia;
import de.danoeh.antennapod.core.util.playback.Playable;
@@ -74,8 +78,6 @@ import de.greenrobot.event.EventBus;
* Controls the MediaPlayer that plays a FeedMedia-file
*/
public class PlaybackService extends MediaBrowserServiceCompat {
- public static final String FORCE_WIDGET_UPDATE = "de.danoeh.antennapod.FORCE_WIDGET_UPDATE";
- public static final String STOP_WIDGET_UPDATE = "de.danoeh.antennapod.STOP_WIDGET_UPDATE";
/**
* Logging tag
*/
@@ -313,6 +315,31 @@ public class PlaybackService extends MediaBrowserServiceCompat {
flavorHelper.initializeMediaPlayer(PlaybackService.this);
mediaSession.setActive(true);
+
+ NotificationCompat.Builder notificationBuilder = createBasicNotification();
+ startForeground(NOTIFICATION_ID, notificationBuilder.build());
+ EventBus.getDefault().post(new ServiceEvent(ServiceEvent.Action.SERVICE_STARTED));
+
+
+ setupNotification(Playable.PlayableUtils.createInstanceFromPreferences(getApplicationContext()));
+ }
+
+ private NotificationCompat.Builder createBasicNotification() {
+ final int smallIcon = ClientConfig.playbackServiceCallbacks.getNotificationIconResource(getApplicationContext());
+
+ final PendingIntent pIntent = PendingIntent.getActivity(this, 0,
+ PlaybackService.getPlayerActivityIntent(this),
+ PendingIntent.FLAG_UPDATE_CURRENT);
+
+ return new NotificationCompat.Builder(
+ this, NotificationUtils.CHANNEL_ID_PLAYING)
+ .setContentTitle(getString(R.string.app_name))
+ .setContentText("Service is running") // Just in case the notification is not updated (should not occur)
+ .setOngoing(false)
+ .setContentIntent(pIntent)
+ .setWhen(0) // we don't need the time
+ .setSmallIcon(smallIcon)
+ .setPriority(NotificationCompat.PRIORITY_MIN);
}
@Override
@@ -487,6 +514,8 @@ public class PlaybackService extends MediaBrowserServiceCompat {
} else if (status == PlayerStatus.INITIALIZED) {
mediaPlayer.setStartWhenPrepared(true);
mediaPlayer.prepare();
+ } else if (mediaPlayer.getPlayable() == null) {
+ startPlayingFromPreferences();
}
return true;
case KeyEvent.KEYCODE_MEDIA_PLAY:
@@ -495,6 +524,8 @@ public class PlaybackService extends MediaBrowserServiceCompat {
} else if (status == PlayerStatus.INITIALIZED) {
mediaPlayer.setStartWhenPrepared(true);
mediaPlayer.prepare();
+ } else if (mediaPlayer.getPlayable() == null) {
+ startPlayingFromPreferences();
}
return true;
case KeyEvent.KEYCODE_MEDIA_PAUSE:
@@ -548,6 +579,15 @@ public class PlaybackService extends MediaBrowserServiceCompat {
return false;
}
+ private void startPlayingFromPreferences() {
+ Playable playable = Playable.PlayableUtils.createInstanceFromPreferences(getApplicationContext());
+ if (playable != null) {
+ mediaPlayer.playMediaObject(playable, false, true, true);
+ started = true;
+ PlaybackService.this.updateMediaSessionMetadata(playable);
+ }
+ }
+
/**
* Called by a mediaplayer Activity as soon as it has prepared its
* mediaplayer.
@@ -566,8 +606,10 @@ public class PlaybackService extends MediaBrowserServiceCompat {
}
public void notifyVideoSurfaceAbandoned() {
- stopForeground(!UserPreferences.isPersistNotify());
+ mediaPlayer.pause(true, false);
mediaPlayer.resetVideoSurface();
+ setupNotification(getPlayable());
+ stopForeground(!UserPreferences.isPersistNotify());
}
private final PlaybackServiceTaskManager.PSTMCallback taskManagerCallback = new PlaybackServiceTaskManager.PSTMCallback() {
@@ -601,7 +643,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
@Override
public void onWidgetUpdaterTick() {
- updateWidget();
+ PlayerWidgetJobService.updateWidget(getBaseContext());
}
@Override
@@ -663,7 +705,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
Intent statusUpdate = new Intent(ACTION_PLAYER_STATUS_CHANGED);
// statusUpdate.putExtra(EXTRA_NEW_PLAYER_STATUS, newInfo.playerStatus.ordinal());
sendBroadcast(statusUpdate);
- updateWidget();
+ PlayerWidgetJobService.updateWidget(getBaseContext());
bluetoothNotifyChange(newInfo, AVRCP_ACTION_PLAYER_STATUS_CHANGED);
bluetoothNotifyChange(newInfo, AVRCP_ACTION_META_CHANGED);
}
@@ -806,7 +848,6 @@ public class PlaybackService extends MediaBrowserServiceCompat {
if (!isCasting) {
stopForeground(true);
}
- stopWidgetUpdater();
}
if (mediaType == null) {
sendNotificationBroadcast(NOTIFICATION_TYPE_PLAYBACK_END, 0);
@@ -1170,10 +1211,10 @@ public class PlaybackService extends MediaBrowserServiceCompat {
* Prepares notification and starts the service in the foreground.
*/
private void setupNotification(final PlaybackServiceMediaPlayer.PSMPInfo info) {
- final PendingIntent pIntent = PendingIntent.getActivity(this, 0,
- PlaybackService.getPlayerActivityIntent(this),
- PendingIntent.FLAG_UPDATE_CURRENT);
+ setupNotification(info.playable);
+ }
+ private synchronized void setupNotification(final Playable playable) {
if (notificationSetupThread != null) {
notificationSetupThread.interrupt();
}
@@ -1183,12 +1224,12 @@ public class PlaybackService extends MediaBrowserServiceCompat {
@Override
public void run() {
Log.d(TAG, "Starting background work");
- if (info.playable != null) {
+ if (playable != null) {
int iconSize = getResources().getDimensionPixelSize(
android.R.dimen.notification_large_icon_width);
try {
icon = Glide.with(PlaybackService.this)
- .load(info.playable.getImageLocation())
+ .load(playable.getImageLocation())
.asBitmap()
.diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY)
.centerCrop()
@@ -1207,24 +1248,18 @@ public class PlaybackService extends MediaBrowserServiceCompat {
return;
}
PlayerStatus playerStatus = mediaPlayer.getPlayerStatus();
- final int smallIcon = ClientConfig.playbackServiceCallbacks.getNotificationIconResource(getApplicationContext());
- if (!Thread.currentThread().isInterrupted() && started && info.playable != null) {
- String contentText = info.playable.getEpisodeTitle();
- String contentTitle = info.playable.getFeedTitle();
+ if (!Thread.currentThread().isInterrupted() && started && playable != null) {
+ String contentText = playable.getEpisodeTitle();
+ String contentTitle = playable.getFeedTitle();
Notification notification;
// Builder is v7, even if some not overwritten methods return its parent's v4 interface
- NotificationCompat.Builder notificationBuilder = (NotificationCompat.Builder) new NotificationCompat.Builder(
- PlaybackService.this)
- .setContentTitle(contentTitle)
+ NotificationCompat.Builder notificationBuilder = createBasicNotification();
+ notificationBuilder.setContentTitle(contentTitle)
.setContentText(contentText)
- .setOngoing(false)
- .setContentIntent(pIntent)
- .setLargeIcon(icon)
- .setSmallIcon(smallIcon)
- .setWhen(0) // we don't need the time
- .setPriority(UserPreferences.getNotifyPriority()); // set notification priority
+ .setPriority(UserPreferences.getNotifyPriority())
+ .setLargeIcon(icon); // set notification priority
IntList compactActionList = new IntList();
int numActions = 0; // we start and 0 and then increment by 1 for each call to addAction
@@ -1292,7 +1327,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
PendingIntent stopButtonPendingIntent = getPendingIntentForMediaAction(
KeyEvent.KEYCODE_MEDIA_STOP, numActions);
- notificationBuilder.setStyle(new android.support.v7.app.NotificationCompat.MediaStyle()
+ notificationBuilder.setStyle(new android.support.v4.media.app.NotificationCompat.MediaStyle()
.setMediaSession(mediaSession.getSessionToken())
.setShowActionsInCompactView(compactActionList.toArray())
.setShowCancelButton(true)
@@ -1359,16 +1394,6 @@ public class PlaybackService extends MediaBrowserServiceCompat {
}
}
- private void stopWidgetUpdater() {
- taskManager.cancelWidgetUpdater();
- sendBroadcast(new Intent(STOP_WIDGET_UPDATE));
- }
-
- private void updateWidget() {
- PlaybackService.this.sendBroadcast(new Intent(
- FORCE_WIDGET_UPDATE));
- }
-
public boolean sleepTimerActive() {
return taskManager.isSleepTimerActive();
}
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 573954412..22a7b64fe 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java
@@ -4,6 +4,8 @@ import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
+import android.support.annotation.Nullable;
+import android.support.v4.content.ContextCompat;
import android.util.Log;
import java.util.ArrayList;
@@ -38,6 +40,7 @@ import de.danoeh.antennapod.core.util.LongList;
import de.danoeh.antennapod.core.util.comparator.FeedItemPubdateComparator;
import de.danoeh.antennapod.core.util.exception.MediaFileNotFoundException;
import de.danoeh.antennapod.core.util.flattr.FlattrUtils;
+import de.danoeh.antennapod.core.util.playback.PlaybackServiceStarter;
import static android.content.Context.MODE_PRIVATE;
@@ -123,16 +126,13 @@ public final class DBTasks {
media);
}
}
- // Start playback Service
- Intent launchIntent = new Intent(context, PlaybackService.class);
- launchIntent.putExtra(PlaybackService.EXTRA_PLAYABLE, media);
- launchIntent.putExtra(PlaybackService.EXTRA_START_WHEN_PREPARED,
- startWhenPrepared);
- launchIntent.putExtra(PlaybackService.EXTRA_SHOULD_STREAM,
- shouldStream);
- launchIntent.putExtra(PlaybackService.EXTRA_PREPARE_IMMEDIATELY,
- true);
- context.startService(launchIntent);
+
+ new PlaybackServiceStarter(context, media)
+ .callEvenIfRunning(true)
+ .startWhenPrepared(startWhenPrepared)
+ .shouldStream(shouldStream)
+ .start();
+
if (showPlayer) {
// Launch media player
context.startActivity(PlaybackService.getPlayerActivityIntent(
@@ -155,42 +155,45 @@ public final class DBTasks {
* Refreshes a given list of Feeds in a separate Thread. This method might ignore subsequent calls if it is still
* enqueuing Feeds for download from a previous call
*
- * @param context Might be used for accessing the database
- * @param feeds List of Feeds that should be refreshed.
+ * @param context Might be used for accessing the database
+ * @param feeds List of Feeds that should be refreshed.
+ * @param callback Called after everything was added enqueued for download. Might be null.
*/
- public static void refreshAllFeeds(final Context context,
- final List<Feed> feeds) {
- if (isRefreshing.compareAndSet(false, true)) {
- new Thread() {
- public void run() {
- if (feeds != null) {
- refreshFeeds(context, feeds);
- } else {
- refreshFeeds(context, DBReader.getFeedList());
- }
- isRefreshing.set(false);
+ public static void refreshAllFeeds(final Context context, final List<Feed> feeds, @Nullable Runnable callback) {
+ if (!isRefreshing.compareAndSet(false, true)) {
+ Log.d(TAG, "Ignoring request to refresh all feeds: Refresh lock is locked");
+ return;
+ }
- SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, MODE_PRIVATE);
- prefs.edit().putLong(PREF_LAST_REFRESH, System.currentTimeMillis()).apply();
+ new Thread(() -> {
+ if (feeds != null) {
+ refreshFeeds(context, feeds);
+ } else {
+ refreshFeeds(context, DBReader.getFeedList());
+ }
+ isRefreshing.set(false);
- if (FlattrUtils.hasToken()) {
- Log.d(TAG, "Flattring all pending things.");
- new FlattrClickWorker(context).executeAsync(); // flattr pending things
+ SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, MODE_PRIVATE);
+ prefs.edit().putLong(PREF_LAST_REFRESH, System.currentTimeMillis()).apply();
- Log.d(TAG, "Fetching flattr status.");
- new FlattrStatusFetcher(context).start();
+ if (FlattrUtils.hasToken()) {
+ Log.d(TAG, "Flattring all pending things.");
+ new FlattrClickWorker(context).executeAsync(); // flattr pending things
- }
- if (ClientConfig.gpodnetCallbacks.gpodnetEnabled()) {
- GpodnetSyncService.sendSyncIntent(context);
- }
- Log.d(TAG, "refreshAllFeeds autodownload");
- autodownloadUndownloadedItems(context);
- }
- }.start();
- } else {
- Log.d(TAG, "Ignoring request to refresh all feeds: Refresh lock is locked");
- }
+ Log.d(TAG, "Fetching flattr status.");
+ new FlattrStatusFetcher(context).start();
+
+ }
+ if (ClientConfig.gpodnetCallbacks.gpodnetEnabled()) {
+ GpodnetSyncService.sendSyncIntent(context);
+ }
+ Log.d(TAG, "refreshAllFeeds autodownload");
+ autodownloadUndownloadedItems(context);
+
+ if (callback != null) {
+ callback.run();
+ }
+ }).start();
}
/**
@@ -345,7 +348,7 @@ public final class DBTasks {
Log.d(TAG, "last refresh: " + Converter.getDurationStringLocalized(context,
System.currentTimeMillis() - lastRefresh) + " ago");
if(lastRefresh <= System.currentTimeMillis() - interval) {
- DBTasks.refreshAllFeeds(context, null);
+ DBTasks.refreshAllFeeds(context, null, null);
}
}
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 a8fd79fda..df618e252 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java
@@ -4,6 +4,7 @@ import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.NonNull;
+import android.support.v4.content.ContextCompat;
import android.text.TextUtils;
import android.util.Log;
import android.webkit.URLUtil;
@@ -81,7 +82,7 @@ public class DownloadRequester {
Intent launchIntent = new Intent(context, DownloadService.class);
launchIntent.putExtra(DownloadService.EXTRA_REQUEST, request);
- context.startService(launchIntent);
+ ContextCompat.startForegroundService(context, launchIntent);
return true;
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/FeedUpdateUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/FeedUpdateUtils.java
new file mode 100644
index 000000000..24e0da9ed
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/FeedUpdateUtils.java
@@ -0,0 +1,21 @@
+package de.danoeh.antennapod.core.util;
+
+import android.content.Context;
+import android.util.Log;
+import de.danoeh.antennapod.core.storage.DBTasks;
+
+public class FeedUpdateUtils {
+ private static final String TAG = "FeedUpdateUtils";
+
+ private FeedUpdateUtils() {
+
+ }
+
+ public static void startAutoUpdate(Context context, Runnable callback) {
+ if (NetworkUtils.networkAvailable() && NetworkUtils.isDownloadAllowed()) {
+ DBTasks.refreshAllFeeds(context, null, callback);
+ } else {
+ Log.d(TAG, "Blocking automatic update: no wifi available / no mobile updates allowed");
+ }
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/NotificationUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/NotificationUtils.java
new file mode 100644
index 000000000..e81b03d77
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/NotificationUtils.java
@@ -0,0 +1,72 @@
+package de.danoeh.antennapod.core.util;
+
+
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.os.Build;
+import android.support.annotation.RequiresApi;
+import de.danoeh.antennapod.core.R;
+
+public class NotificationUtils {
+ public static final String CHANNEL_ID_USER_ACTION = "user_action";
+ public static final String CHANNEL_ID_DOWNLOADING = "downloading";
+ public static final String CHANNEL_ID_PLAYING = "playing";
+ public static final String CHANNEL_ID_ERROR = "error";
+ public static final String CHANNEL_ID_GPODNET = "gpodnet";
+
+ public static void createChannels(Context context) {
+ if (android.os.Build.VERSION.SDK_INT < 26) {
+ return;
+ }
+ NotificationManager mNotificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+
+ if (mNotificationManager != null) {
+ mNotificationManager.createNotificationChannel(createChannelUserAction(context));
+ mNotificationManager.createNotificationChannel(createChannelDownloading(context));
+ mNotificationManager.createNotificationChannel(createChannelPlaying(context));
+ mNotificationManager.createNotificationChannel(createChannelError(context));
+ mNotificationManager.createNotificationChannel(createChannelGpodnet(context));
+ }
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.O)
+ private static NotificationChannel createChannelUserAction(Context c) {
+ NotificationChannel mChannel = new NotificationChannel(CHANNEL_ID_USER_ACTION,
+ c.getString(R.string.notification_channel_user_action), NotificationManager.IMPORTANCE_HIGH);
+ mChannel.setDescription(c.getString(R.string.notification_channel_user_action_description));
+ return mChannel;
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.O)
+ private static NotificationChannel createChannelDownloading(Context c) {
+ NotificationChannel mChannel = new NotificationChannel(CHANNEL_ID_DOWNLOADING,
+ c.getString(R.string.notification_channel_downloading), NotificationManager.IMPORTANCE_LOW);
+ mChannel.setDescription(c.getString(R.string.notification_channel_downloading_description));
+ return mChannel;
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.O)
+ private static NotificationChannel createChannelPlaying(Context c) {
+ NotificationChannel mChannel = new NotificationChannel(CHANNEL_ID_PLAYING,
+ c.getString(R.string.notification_channel_playing), NotificationManager.IMPORTANCE_LOW);
+ mChannel.setDescription(c.getString(R.string.notification_channel_playing_description));
+ return mChannel;
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.O)
+ private static NotificationChannel createChannelError(Context c) {
+ NotificationChannel mChannel = new NotificationChannel(CHANNEL_ID_ERROR,
+ c.getString(R.string.notification_channel_error), NotificationManager.IMPORTANCE_HIGH);
+ mChannel.setDescription(c.getString(R.string.notification_channel_error_description));
+ return mChannel;
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.O)
+ private static NotificationChannel createChannelGpodnet(Context c) {
+ NotificationChannel mChannel = new NotificationChannel(CHANNEL_ID_GPODNET,
+ c.getString(R.string.notification_channel_gpodnet), NotificationManager.IMPORTANCE_MIN);
+ mChannel.setDescription(c.getString(R.string.notification_channel_gpodnet_description));
+ return mChannel;
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/download/AutoUpdateManager.java b/core/src/main/java/de/danoeh/antennapod/core/util/download/AutoUpdateManager.java
new file mode 100644
index 000000000..ec9fcae4e
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/download/AutoUpdateManager.java
@@ -0,0 +1,113 @@
+package de.danoeh.antennapod.core.util.download;
+
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.app.job.JobInfo;
+import android.app.job.JobScheduler;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import android.os.SystemClock;
+import android.support.annotation.RequiresApi;
+import android.util.Log;
+import de.danoeh.antennapod.core.receiver.FeedUpdateReceiver;
+import de.danoeh.antennapod.core.service.FeedUpdateJobService;
+
+import java.util.Calendar;
+import java.util.concurrent.TimeUnit;
+
+public class AutoUpdateManager {
+ private static final int JOB_ID_FEED_UPDATE = 42;
+ private static final String TAG = "AutoUpdateManager";
+
+ private AutoUpdateManager() {
+
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+ private static JobInfo.Builder getFeedUpdateJobBuilder(Context context) {
+ ComponentName serviceComponent = new ComponentName(context, FeedUpdateJobService.class);
+ JobInfo.Builder builder = new JobInfo.Builder(JOB_ID_FEED_UPDATE, serviceComponent);
+ builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
+ builder.setPersisted(true);
+ return builder;
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.N)
+ public static void restartJobServiceInterval(Context context, long intervalMillis) {
+ JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
+ if (jobScheduler != null) {
+ JobInfo oldJob = jobScheduler.getPendingJob(JOB_ID_FEED_UPDATE);
+ if (oldJob == null || oldJob.getIntervalMillis() != intervalMillis) {
+ JobInfo.Builder builder = getFeedUpdateJobBuilder(context);
+ builder.setPeriodic(intervalMillis);
+ jobScheduler.cancel(JOB_ID_FEED_UPDATE);
+
+ if (intervalMillis <= 0) {
+ Log.d(TAG, "Automatic update was deactivated");
+ return;
+ }
+
+ jobScheduler.schedule(builder.build());
+ Log.d(TAG, "JobScheduler was set at interval " + intervalMillis);
+ } else {
+ Log.d(TAG, "JobScheduler was already set at interval " + intervalMillis + ", ignoring.");
+ }
+ }
+ }
+
+ public static void restartAlarmManagerInterval(Context context, long triggerAtMillis, long intervalMillis) {
+ AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+
+ if (alarmManager == null) {
+ Log.d(TAG, "AlarmManager was null");
+ return;
+ }
+
+ Intent intent = new Intent(context, FeedUpdateReceiver.class);
+ PendingIntent updateIntent = PendingIntent.getBroadcast(context, 0, intent, 0);
+ alarmManager.cancel(updateIntent);
+
+ if (intervalMillis <= 0) {
+ Log.d(TAG, "Automatic update was deactivated");
+ return;
+ }
+
+ alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
+ SystemClock.elapsedRealtime() + triggerAtMillis,
+ updateIntent);
+ Log.d(TAG, "Changed alarm to new interval " + TimeUnit.MILLISECONDS.toHours(intervalMillis) + " h");
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.N)
+ public static void restartJobServiceTriggerAt(Context context, long triggerAtMillis) {
+ JobInfo.Builder builder = getFeedUpdateJobBuilder(context);
+ builder.setMinimumLatency(triggerAtMillis);
+ JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
+ if (jobScheduler != null) {
+ jobScheduler.cancel(JOB_ID_FEED_UPDATE);
+ jobScheduler.schedule(builder.build());
+ Log.d(TAG, "JobScheduler was set for " + triggerAtMillis);
+ }
+ }
+
+ public static void restartAlarmManagerTimeOfDay(Context context, Calendar alarm) {
+ AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+ PendingIntent updateIntent = PendingIntent.getBroadcast(context, 0,
+ new Intent(context, FeedUpdateReceiver.class), 0);
+
+ if (alarmManager == null) {
+ Log.d(TAG, "AlarmManager was null");
+ return;
+ }
+
+ alarmManager.cancel(updateIntent);
+
+ Log.d(TAG, "Alarm set for: " + alarm.toString() + " : " + alarm.getTimeInMillis());
+ alarmManager.set(AlarmManager.RTC_WAKEUP,
+ alarm.getTimeInMillis(),
+ updateIntent);
+ Log.d(TAG, "Changed alarm to new time of day " + alarm.get(Calendar.HOUR_OF_DAY) + ":" + alarm.get(Calendar.MINUTE));
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java
index 279c56338..ff7f5b79d 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java
@@ -3,6 +3,8 @@ package de.danoeh.antennapod.core.util.playback;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Parcelable;
+import android.preference.PreferenceManager;
+import android.support.annotation.Nullable;
import android.util.Log;
import java.util.List;
@@ -11,6 +13,7 @@ import de.danoeh.antennapod.core.asynctask.ImageResource;
import de.danoeh.antennapod.core.feed.Chapter;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.feed.MediaType;
+import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.util.ShownotesProvider;
@@ -179,6 +182,23 @@ public interface Playable extends Parcelable,
* Restores a playable object from a sharedPreferences file. This method might load data from the database,
* depending on the type of playable that was restored.
*
+ * @return The restored Playable object
+ */
+ @Nullable
+ public static Playable createInstanceFromPreferences(Context context) {
+ long currentlyPlayingMedia = PlaybackPreferences.getCurrentlyPlayingMedia();
+ if (currentlyPlayingMedia != PlaybackPreferences.NO_MEDIA_PLAYING) {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext());
+ return PlayableUtils.createInstanceFromPreferences(context,
+ (int) currentlyPlayingMedia, prefs);
+ }
+ return null;
+ }
+
+ /**
+ * Restores a playable object from a sharedPreferences file. This method might load data from the database,
+ * depending on the type of playable that was restored.
+ *
* @param type An integer that represents the type of the Playable object
* that is restored.
* @param pref The SharedPreferences file from which the Playable object
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java
index a160b4f0a..a3f02d5cc 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java
@@ -14,6 +14,8 @@ import android.os.Build;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.content.ContextCompat;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
@@ -59,7 +61,7 @@ public abstract class PlaybackController {
private PlaybackService playbackService;
private Playable media;
- private PlayerStatus status;
+ private PlayerStatus status = PlayerStatus.STOPPED;
private final ScheduledThreadPoolExecutor schedExecutor;
private static final int SCHED_EX_POOLSIZE = 1;
@@ -69,6 +71,7 @@ public abstract class PlaybackController {
private boolean mediaInfoLoaded = false;
private boolean released = false;
+ private boolean initialized = false;
private Subscription serviceBinder;
@@ -92,10 +95,22 @@ public abstract class PlaybackController {
}
/**
- * Creates a new connection to the playbackService. Should be called in the
- * activity's onResume() method.
+ * Creates a new connection to the playbackService.
*/
public void init() {
+ if (PlaybackService.isRunning) {
+ initServiceRunning();
+ } else {
+ initServiceNotRunning();
+ }
+ }
+
+ private synchronized void initServiceRunning() {
+ if (initialized) {
+ return;
+ }
+ initialized = true;
+
activity.registerReceiver(statusUpdate, new IntentFilter(
PlaybackService.ACTION_PLAYER_STATUS_CHANGED));
@@ -167,7 +182,7 @@ public abstract class PlaybackController {
*/
private void bindToService() {
Log.d(TAG, "Trying to connect to service");
- if(serviceBinder != null) {
+ if (serviceBinder != null) {
serviceBinder.unsubscribe();
}
serviceBinder = Observable.fromCallable(this::getPlayLastPlayedMediaIntent)
@@ -178,7 +193,7 @@ public abstract class PlaybackController {
if (!PlaybackService.started) {
if (intent != null) {
Log.d(TAG, "Calling start service");
- activity.startService(intent);
+ ContextCompat.startForegroundService(activity, intent);
bound = activity.bindService(intent, mConnection, 0);
} else {
status = PlayerStatus.STOPPED;
@@ -198,31 +213,24 @@ public abstract class PlaybackController {
* Returns an intent that starts the PlaybackService and plays the last
* played media or null if no last played media could be found.
*/
- private Intent getPlayLastPlayedMediaIntent() {
+ @Nullable private Intent getPlayLastPlayedMediaIntent() {
Log.d(TAG, "Trying to restore last played media");
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
- activity.getApplicationContext());
- long currentlyPlayingMedia = PlaybackPreferences.getCurrentlyPlayingMedia();
- if (currentlyPlayingMedia != PlaybackPreferences.NO_MEDIA_PLAYING) {
- Playable media = PlayableUtils.createInstanceFromPreferences(activity,
- (int) currentlyPlayingMedia, prefs);
- if (media != null) {
- Intent serviceIntent = new Intent(activity, PlaybackService.class);
- serviceIntent.putExtra(PlaybackService.EXTRA_PLAYABLE, media);
- serviceIntent.putExtra(PlaybackService.EXTRA_START_WHEN_PREPARED, false);
- serviceIntent.putExtra(PlaybackService.EXTRA_PREPARE_IMMEDIATELY, true);
- boolean fileExists = media.localFileAvailable();
- boolean lastIsStream = PlaybackPreferences.getCurrentEpisodeIsStream();
- if (!fileExists && !lastIsStream && media instanceof FeedMedia) {
- DBTasks.notifyMissingFeedMediaFile(activity, (FeedMedia) media);
- }
- serviceIntent.putExtra(PlaybackService.EXTRA_SHOULD_STREAM,
- lastIsStream || !fileExists);
- return serviceIntent;
- }
+ Playable media = PlayableUtils.createInstanceFromPreferences(activity);
+ if (media == null) {
+ Log.d(TAG, "No last played media found");
+ return null;
}
- Log.d(TAG, "No last played media found");
- return null;
+
+ boolean fileExists = media.localFileAvailable();
+ boolean lastIsStream = PlaybackPreferences.getCurrentEpisodeIsStream();
+ if (!fileExists && !lastIsStream && media instanceof FeedMedia) {
+ DBTasks.notifyMissingFeedMediaFile(activity, (FeedMedia) media);
+ }
+
+ return new PlaybackServiceStarter(activity, media)
+ .startWhenPrepared(false)
+ .shouldStream(lastIsStream || !fileExists)
+ .getIntent();
}
@@ -511,7 +519,7 @@ public abstract class PlaybackController {
"PlaybackService has no media object. Trying to restore last played media.");
Intent serviceIntent = getPlayLastPlayedMediaIntent();
if (serviceIntent != null) {
- activity.startService(serviceIntent);
+ ContextCompat.startForegroundService(activity, serviceIntent);
}
}
*/
@@ -576,6 +584,10 @@ public abstract class PlaybackController {
public void playPause() {
if (playbackService == null) {
+ new PlaybackServiceStarter(activity, media)
+ .startWhenPrepared(true)
+ .streamIfLastWasStream()
+ .start();
Log.w(TAG, "Play/Pause button was pressed, but playbackservice was null!");
return;
}
@@ -609,6 +621,8 @@ public abstract class PlaybackController {
public int getPosition() {
if (playbackService != null) {
return playbackService.getCurrentPosition();
+ } else if (media != null) {
+ return media.getPosition();
} else {
return PlaybackService.INVALID_TIME;
}
@@ -617,12 +631,17 @@ public abstract class PlaybackController {
public int getDuration() {
if (playbackService != null) {
return playbackService.getDuration();
+ } else if (media != null) {
+ return media.getDuration();
} else {
return PlaybackService.INVALID_TIME;
}
}
public Playable getMedia() {
+ if (media == null) {
+ media = PlayableUtils.createInstanceFromPreferences(activity);
+ }
return media;
}
@@ -714,8 +733,13 @@ public abstract class PlaybackController {
}
public boolean isPlayingVideoLocally() {
- return playbackService != null && PlaybackService.getCurrentMediaType() == MediaType.VIDEO
- && !PlaybackService.isCasting();
+ if (PlaybackService.isCasting()) {
+ return false;
+ } else if (playbackService != null) {
+ return PlaybackService.getCurrentMediaType() == MediaType.VIDEO;
+ } else {
+ return getMedia() != null && getMedia().getMediaType() == MediaType.VIDEO;
+ }
}
public Pair<Integer, Integer> getVideoSize() {
@@ -755,6 +779,21 @@ public abstract class PlaybackController {
}
}
+ private void initServiceNotRunning() {
+ if (getMedia() == null) {
+ return;
+ }
+ if (getMedia().getMediaType() == MediaType.AUDIO) {
+ TypedArray res = activity.obtainStyledAttributes(new int[]{
+ de.danoeh.antennapod.core.R.attr.av_play_big});
+ getPlayButton().setImageResource(
+ res.getResourceId(0, de.danoeh.antennapod.core.R.drawable.ic_play_arrow_grey600_36dp));
+ res.recycle();
+ } else {
+ getPlayButton().setImageResource(R.drawable.ic_av_play_circle_outline_80dp);
+ }
+ }
+
/**
* Refreshes the current position of the media file that is playing.
*/
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackServiceStarter.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackServiceStarter.java
new file mode 100644
index 000000000..3ba553d12
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackServiceStarter.java
@@ -0,0 +1,76 @@
+package de.danoeh.antennapod.core.util.playback;
+
+import android.content.Context;
+import android.content.Intent;
+import android.media.MediaPlayer;
+import android.support.v4.content.ContextCompat;
+import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
+import de.danoeh.antennapod.core.service.playback.PlaybackService;
+
+public class PlaybackServiceStarter {
+ private final Context context;
+ private final Playable media;
+ private boolean startWhenPrepared = false;
+ private boolean shouldStream = false;
+ private boolean callEvenIfRunning = false;
+ private boolean prepareImmediately = true;
+
+ public PlaybackServiceStarter(Context context, Playable media) {
+ this.context = context;
+ this.media = media;
+ }
+
+ /**
+ * Default value: false
+ */
+ public PlaybackServiceStarter shouldStream(boolean shouldStream) {
+ this.shouldStream = shouldStream;
+ return this;
+ }
+
+ public PlaybackServiceStarter streamIfLastWasStream() {
+ boolean lastIsStream = PlaybackPreferences.getCurrentEpisodeIsStream();
+ return shouldStream(lastIsStream);
+ }
+
+ /**
+ * Default value: false
+ */
+ public PlaybackServiceStarter startWhenPrepared(boolean startWhenPrepared) {
+ this.startWhenPrepared = startWhenPrepared;
+ return this;
+ }
+
+ /**
+ * Default value: false
+ */
+ public PlaybackServiceStarter callEvenIfRunning(boolean callEvenIfRunning) {
+ this.callEvenIfRunning = callEvenIfRunning;
+ return this;
+ }
+
+ /**
+ * Default value: true
+ */
+ public PlaybackServiceStarter prepareImmediately(boolean prepareImmediately) {
+ this.prepareImmediately = prepareImmediately;
+ return this;
+ }
+
+ public Intent getIntent() {
+ Intent launchIntent = new Intent(context, PlaybackService.class);
+ launchIntent.putExtra(PlaybackService.EXTRA_PLAYABLE, media);
+ launchIntent.putExtra(PlaybackService.EXTRA_START_WHEN_PREPARED, startWhenPrepared);
+ launchIntent.putExtra(PlaybackService.EXTRA_SHOULD_STREAM, shouldStream);
+ launchIntent.putExtra(PlaybackService.EXTRA_PREPARE_IMMEDIATELY, prepareImmediately);
+
+ return launchIntent;
+ }
+
+ public void start() {
+ if (PlaybackService.isRunning && !callEvenIfRunning) {
+ return;
+ }
+ ContextCompat.startForegroundService(context, getIntent());
+ }
+}
diff --git a/core/src/main/res/layout/player_widget.xml b/core/src/main/res/layout/player_widget.xml
new file mode 100644
index 000000000..4c98895a0
--- /dev/null
+++ b/core/src/main/res/layout/player_widget.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:padding="@dimen/widget_margin" >
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="#262C31" >
+
+ <ImageButton
+ android:id="@+id/butPlay"
+ android:contentDescription="@string/play_label"
+ android:layout_width="56dp"
+ android:layout_height="match_parent"
+ android:layout_alignParentRight="true"
+ android:layout_margin="12dp"
+ android:background="@drawable/borderless_button_dark"
+ android:src="@drawable/ic_play_arrow_white_24dp" />
+
+ <LinearLayout
+ android:id="@+id/layout_left"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_alignParentLeft="true"
+ android:layout_toLeftOf="@id/butPlay"
+ android:background="@drawable/borderless_button_dark"
+ android:gravity="center_vertical"
+ android:orientation="vertical" >
+
+ <TextView
+ android:id="@+id/txtvTitle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="8dp"
+ android:maxLines="1"
+ android:text="@string/no_media_playing_label"
+ android:textColor="@color/white"
+ android:textSize="@dimen/text_size_medium"
+ android:textStyle="bold" />
+
+ <TextView
+ android:id="@+id/txtvProgress"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="8dp"
+ android:textColor="@color/white" />
+ </LinearLayout>
+ </RelativeLayout>
+
+</FrameLayout> \ 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 46bac68c9..5759912d1 100644
--- a/core/src/main/res/values/strings.xml
+++ b/core/src/main/res/values/strings.xml
@@ -29,6 +29,7 @@
<string name="free_space_label">%1$s free</string>
<string name="episode_cache_full_title">Episode cache full</string>
<string name="episode_cache_full_message">The episode cache limit has been reached. You can increase the cache size in the Settings.</string>
+ <string name="synchronizing">Synchronizing…</string>
<!-- Statistics fragment -->
<string name="total_time_listened_to_podcasts">Total time of podcasts played:</string>
@@ -709,4 +710,16 @@
<string name="cast_failed_seek">Failed to seek to the new position on the cast device</string>
<string name="cast_failed_receiver_player_error">Receiver player has encountered a severe error</string>
<string name="cast_failed_media_error_skipping">Error playing media. Skipping&#8230;</string>
+
+ <!-- Notification channels -->
+ <string name="notification_channel_user_action">Action required</string>
+ <string name="notification_channel_user_action_description">Shown if your action is required</string>
+ <string name="notification_channel_downloading">Downloading</string>
+ <string name="notification_channel_downloading_description">Shown while currently downloading</string>
+ <string name="notification_channel_playing">Currently playing</string>
+ <string name="notification_channel_playing_description">Allows to control playback</string>
+ <string name="notification_channel_error">Errors</string>
+ <string name="notification_channel_error_description">Shown if something went wrong</string>
+ <string name="notification_channel_gpodnet">gpodder.net</string>
+ <string name="notification_channel_gpodnet_description">Shown while currently synchronizing</string>
</resources>