package de.danoeh.antennapod.core.service.playback;
import android.content.Context;
import android.os.Vibrator;
import android.support.annotation.NonNull;
import android.util.Log;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import de.danoeh.antennapod.core.event.QueueEvent;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.util.playback.Playable;
import de.greenrobot.event.EventBus;
/**
* Manages the background tasks of PlaybackSerivce, i.e.
* the sleep timer, the position saver, the widget updater and
* the queue loader.
* <p/>
* The PlaybackServiceTaskManager(PSTM) uses a callback object (PSTMCallback)
* to notify the PlaybackService about updates from the running tasks.
*/
public class PlaybackServiceTaskManager {
private static final String TAG = "PlaybackServiceTaskMgr";
/**
* Update interval of position saver in milliseconds.
*/
public static final int POSITION_SAVER_WAITING_INTERVAL = 5000;
/**
* Notification interval of widget updater in milliseconds.
*/
public static final int WIDGET_UPDATER_NOTIFICATION_INTERVAL = 1000;
private static final int SCHED_EX_POOL_SIZE = 2;
private final ScheduledThreadPoolExecutor schedExecutor;
private ScheduledFuture positionSaverFuture;
private ScheduledFuture widgetUpdaterFuture;
private ScheduledFuture sleepTimerFuture;
private volatile Future<List<FeedItem>> queueFuture;
private volatile Future chapterLoaderFuture;
private SleepTimer sleepTimer;
private final Context context;
private final PSTMCallback callback;
/**
* Sets up a new PSTM. This method will also start the queue loader task.
*
* @param context
* @param callback A PSTMCallback object for notifying the user about updates. Must not be null.
*/
public PlaybackServiceTaskManager(@NonNull Context context,
@NonNull PSTMCallback callback) {
this.context = context;
this.callback = callback;
schedExecutor = new ScheduledThreadPoolExecutor(SCHED_EX_POOL_SIZE, r -> {
Thread t = new Thread(r);
t.setPriority(Thread.MIN_PRIORITY);
return t;
});
loadQueue();
EventBus.getDefault().register(this);
}
public void onEvent(QueueEvent event) {
Log.d(TAG, "onEvent(QueueEvent " + event +")");
cancelQueueLoader();
loadQueue();
}
private synchronized boolean isQueueLoaderActive() {
return queueFuture != null && !queueFuture.isDone();
}
private synchronized void cancelQueueLoader() {
if (isQueueLoaderActive()) {
queueFuture.cancel(true);
}
}
private synchronized void loadQueue() {
if (!isQueueLoaderActive()) {
queueFuture = schedExecutor.submit(DBReader::getQueue);
}
}
/**
* Returns the queue if it is already loaded or null if it hasn't been loaded yet.
* In order to wait until the queue has been loaded, use getQueue()
*/
public synchronized List<FeedItem> getQueueIfLoaded() {
if (queueFuture.isDone()) {
try {
return queueFuture.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
return null;
}
/**
* Returns the queue or waits until the PSTM has loaded the queue from the database.
*/
public synchronized List<FeedItem> getQueue() throws InterruptedException {
try {
return queueFuture.get();
} catch (ExecutionException e) {
throw new IllegalArgumentException(e);
}
}
/**
* Starts the position saver task. If the position saver is already active, nothing will happen.
*/
public synchronized void startPositionSaver() {
if (!isPositionSaverActive()) {
Runnable positionSaver = callback::positionSaverTick;
positionSaverFuture = schedExecutor.scheduleWithFixedDelay(positionSaver, POSITION_SAVER_WAITING_INTERVAL,
POSITION_SAVER_WAITING_INTERVAL, TimeUnit.MILLISECONDS);
Log.d(TAG, "Started PositionSaver");
} else {
Log.d(TAG, "Call to startPositionSaver was ignored.");
}
}
/**
* Returns true if the position saver is currently running.
*/
public synchronized boolean isPositionSaverActive() {
return positionSaverFuture != null && !positionSaverFuture.isCancelled() && !positionSaverFuture.isDone();
}
/**
* Cancels the position saver. If the position saver is not running, nothing will happen.
*/
public synchronized void cancelPositionSaver() {
if (isPositionSaverActive()) {
positionSaverFuture.cancel(false);
Log.d(TAG, "Cancelled PositionSaver");
}
}
/**
* Starts the widget updater task. If the widget updater is already active, nothing will happen.
*/
public synchronized void startWidgetUpdater() {
if (!isWidgetUpdaterActive()) {
Runnable widgetUpdater = callback::onWidgetUpdaterTick;
widgetUpdaterFuture = schedExecutor.scheduleWithFixedDelay(widgetUpdater, WIDGET_UPDATER_NOTIFICATION_INTERVAL,
WIDGET_UPDATER_NOTIFICATION_INTERVAL, TimeUnit.MILLISECONDS);
Log.d(TAG, "Started WidgetUpdater");
} else {
Log.d(TAG, "Call to startWidgetUpdater was ignored.");
}
}
/**
* Starts a new sleep timer with the given waiting time. If another sleep timer is already active, it will be
* cancelled first.
* After waitingTime has elapsed, onSleepTimerExpired() will be called.
*
* @throws java.lang.IllegalArgumentException if waitingTime <= 0
*/
public synchronized void setSleepTimer(long waitingTime, boolean shakeToReset, boolean vibrate) {
if(waitingTime <= 0) {
throw new IllegalArgumentException("Waiting time <= 0");
}
Log.d(TAG, "Setting sleep timer to " + Long.toString(waitingTime) + " milliseconds");
if (isSleepTimerActive()) {
sleepTimerFuture.cancel(true);
}
sleepTimer = new SleepTimer(waitingTime, shakeToReset, vibrate);
sleepTimerFuture = schedExecutor.schedule(sleepTimer, 0, TimeUnit.MILLISECONDS);
}
/**
* Returns true if the sleep timer is currently active.
*/
public synchronized boolean isSleepTimerActive() {
return sleepTimer != null
&& sleepTimerFuture != null
&& !sleepTimerFuture.isCancelled()
&& !sleepTimerFuture.isDone()
&& sleepTimer.getWaitingTime() > 0;
}
/**
* Disables the sleep timer. If the sleep timer is not active, nothing will happen.
*/
public synchronized void disableSleepTimer() {
if (isSleepTimerActive()) {
Log.d(TAG, "Disabling sleep timer");
sleepTimerFuture.cancel(true);
}
}
/**
* Returns the current sleep timer time or 0 if the sleep timer is not active.
*/
public synchronized long getSleepTimerTimeLeft() {
if (isSleepTimerActive()) {
return sleepTimer.getWaitingTime();
} else {
return 0;
}
}
/**
* Returns true if the widget updater is currently running.
*/
public synchronized boolean isWidgetUpdaterActive() {
return widgetUpdaterFuture != null && !widgetUpdaterFuture.isCancelled() && !widgetUpdaterFuture.isDone();
}
/**
* Cancels the widget updater. If the widget updater is not running, nothing will happen.
*/
public synchronized void cancelWidgetUpdater() {
if (isWidgetUpdaterActive()) {
widgetUpdaterFuture.cancel(false);
Log.d(TAG, "Cancelled WidgetUpdater");
}
}
private synchronized void cancelChapterLoader() {
if (isChapterLoaderActive()) {
chapterLoaderFuture.cancel(true);
}
}
private synchronized boolean isChapterLoaderActive() {
return chapterLoaderFuture != null && !chapterLoaderFuture.isDone();
}
/**
* Starts a new thread that loads the chapter marks from a playable object. If another chapter loader is already active,
* it will be cancelled first.
* On completion, the callback's onChapterLoaded method will be called.
*/
public synchronized void startChapterLoader(@NonNull final Playable media) {
if (isChapterLoaderActive()) {
cancelChapterLoader();
}
Runnable chapterLoader = () -> {
Log.d(TAG, "Chapter loader started");
if (media.getChapters() == null) {
media.loadChapterMarks();
if (!Thread.currentThread().isInterrupted() && media.getChapters() != null) {
callback.onChapterLoaded(media);
}
}
Log.d(TAG, "Chapter loader stopped");
};
chapterLoaderFuture = schedExecutor.submit(chapterLoader);
}
/**
* Cancels all tasks. The PSTM will be in the initial state after execution of this method.
*/
public synchronized void cancelAllTasks() {
cancelPositionSaver();
cancelWidgetUpdater();
disableSleepTimer();
cancelQueueLoader();
cancelChapterLoader();
}
/**
* Cancels all tasks and shuts down the internal executor service of the PSTM. The object should not be used after
* execution of this method.
*/
public synchronized void shutdown() {
EventBus.getDefault().unregister(this);
cancelAllTasks();
schedExecutor.shutdown();
}
/**
* Sleeps for a given time and then pauses playback.
*/
protected class SleepTimer implements Runnable {
private static final String TAG = "SleepTimer";
private static final long UPDATE_INTERVAL = 1000L;
private static final long NOTIFICATION_THRESHOLD = 10000;
private long waitingTime;
private final boolean shakeToReset;
private final boolean vibrate;
private ShakeListener shakeListener;
public SleepTimer(long waitingTime, boolean shakeToReset, boolean vibrate) {
super();
this.waitingTime = waitingTime;
this.shakeToReset = shakeToReset;
this.vibrate = vibrate;
}
@Override
public void run() {
Log.d(TAG, "Starting");
boolean notifiedAlmostExpired = false;
long lastTick = System.currentTimeMillis();
while (waitingTime > 0) {
try {
Thread.sleep(UPDATE_INTERVAL);
long now = System.currentTimeMillis();
waitingTime -= now - lastTick;
lastTick = now;
if(waitingTime < NOTIFICATION_THRESHOLD && !notifiedAlmostExpired) {
Log.d(TAG, "Sleep timer is about to expire");
if(vibrate) {
Vibrator v = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
if(v != null) {
v.vibrate(500);
}
}
if(shakeListener == null && shakeToReset) {
shakeListener = new ShakeListener(context, this);
}
callback.onSleepTimerAlmostExpired();
notifiedAlmostExpired = true;
}
if (waitingTime <= 0) {
Log.d(TAG, "Sleep timer expired");
if(shakeListener != null) {
shakeListener.pause();
shakeListener = null;
}
if (!Thread.currentThread().isInterrupted()) {
callback.onSleepTimerExpired();
} else {
Log.d(TAG, "Sleep timer interrupted");
}
}
} catch (InterruptedException e) {
Log.d(TAG, "Thread was interrupted while waiting");
e.printStackTrace();
break;
}
}
}
public long getWaitingTime() {
return waitingTime;
}
public void onShake() {
setSleepTimer(15 * 60 * 1000, shakeToReset, vibrate);
callback.onSleepTimerReset();
shakeListener.pause();
shakeListener = null;
}
}
public interface PSTMCallback {
void positionSaverTick();
void onSleepTimerAlmostExpired();
void onSleepTimerExpired();
void onSleepTimerReset();
void onWidgetUpdaterTick();
void onChapterLoaded(Playable media);
}
}