From 513183bc84d2bc7d4f9223f7f3e26d9297badd12 Mon Sep 17 00:00:00 2001
From: daniel oeh <daniel.oeh@gmail.com>
Date: Sun, 15 Dec 2013 02:33:23 +0100
Subject: [PATCH] Moved background tasks of PlaybackService into
 PlaybackServiceTaskManager

---
 .../playback/PlaybackServiceTaskManager.java  | 364 ++++++++++++++++++
 1 file changed, 364 insertions(+)
 create mode 100644 src/de/danoeh/antennapod/service/playback/PlaybackServiceTaskManager.java

diff --git a/src/de/danoeh/antennapod/service/playback/PlaybackServiceTaskManager.java b/src/de/danoeh/antennapod/service/playback/PlaybackServiceTaskManager.java
new file mode 100644
index 000000000..136fc3aa3
--- /dev/null
+++ b/src/de/danoeh/antennapod/service/playback/PlaybackServiceTaskManager.java
@@ -0,0 +1,364 @@
+package de.danoeh.antennapod.service.playback;
+
+import android.content.Context;
+import android.util.Log;
+import de.danoeh.antennapod.AppConfig;
+import de.danoeh.antennapod.feed.EventDistributor;
+import de.danoeh.antennapod.feed.FeedItem;
+import de.danoeh.antennapod.storage.DBReader;
+import de.danoeh.antennapod.util.playback.Playable;
+
+import java.util.List;
+import java.util.concurrent.*;
+
+/**
+ * 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 = "PlaybackServiceTaskManager";
+
+    /**
+     * 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 = 1500;
+
+    private static final int SCHED_EX_POOL_SIZE = 2;
+    private final ScheduledThreadPoolExecutor schedExecutor;
+
+    private ScheduledFuture positionSaverFuture;
+    private ScheduledFuture widgetUpdaterFuture;
+    private Future 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(Context context, PSTMCallback callback) {
+        if (context == null)
+            throw new IllegalArgumentException("context must not be null");
+        if (callback == null)
+            throw new IllegalArgumentException("callback must not be null");
+
+        this.context = context;
+        this.callback = callback;
+        schedExecutor = new ScheduledThreadPoolExecutor(SCHED_EX_POOL_SIZE, new ThreadFactory() {
+            @Override
+            public Thread newThread(Runnable r) {
+                Thread t = new Thread(r);
+                t.setPriority(Thread.MIN_PRIORITY);
+                return t;
+            }
+        });
+        loadQueue();
+        EventDistributor.getInstance().register(eventDistributorListener);
+    }
+
+    private final EventDistributor.EventListener eventDistributorListener = new EventDistributor.EventListener() {
+        @Override
+        public void update(EventDistributor eventDistributor, Integer arg) {
+            if ((EventDistributor.QUEUE_UPDATE & arg) != 0) {
+                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(new Callable<List<FeedItem>>() {
+                @Override
+                public List<FeedItem> call() throws Exception {
+                    return DBReader.getQueue(context);
+                }
+            });
+        }
+    }
+
+    /**
+     * Returns the queue or waits until the PSTM hasloaded 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 = new Runnable() {
+                @Override
+                public void run() {
+                    callback.positionSaverTick();
+                }
+            };
+            positionSaverFuture = schedExecutor.scheduleWithFixedDelay(positionSaver, POSITION_SAVER_WAITING_INTERVAL,
+                    POSITION_SAVER_WAITING_INTERVAL, TimeUnit.MILLISECONDS);
+
+            if (AppConfig.DEBUG) Log.d(TAG, "Started PositionSaver");
+        } else {
+            if (AppConfig.DEBUG) 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);
+            if (AppConfig.DEBUG) 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 = new Runnable() {
+                @Override
+                public void run() {
+                    callback.onWidgetUpdaterTick();
+                }
+            };
+            widgetUpdaterFuture = schedExecutor.scheduleWithFixedDelay(widgetUpdater, WIDGET_UPDATER_NOTIFICATION_INTERVAL,
+                    WIDGET_UPDATER_NOTIFICATION_INTERVAL, TimeUnit.MILLISECONDS);
+
+            if (AppConfig.DEBUG) Log.d(TAG, "Started WidgetUpdater");
+        } else {
+            if (AppConfig.DEBUG) 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) {
+        if (waitingTime <= 0)
+            throw new IllegalArgumentException("waitingTime <= 0");
+
+        if (AppConfig.DEBUG)
+            Log.d(TAG, "Setting sleep timer to " + Long.toString(waitingTime)
+                    + " milliseconds");
+        if (isSleepTimerActive()) {
+            sleepTimerFuture.cancel(true);
+        }
+        sleepTimer = new SleepTimer(waitingTime);
+        sleepTimerFuture = schedExecutor.submit(sleepTimer);
+    }
+
+    /**
+     * Returns true if the sleep timer is currently active.
+     */
+    public synchronized boolean isSleepTimerActive() {
+        return sleepTimer != null && sleepTimer.isWaiting() && sleepTimerFuture != null;
+    }
+
+    /**
+     * Disables the sleep timer. If the sleep timer is not active, nothing will happen.
+     */
+    public synchronized void disableSleepTimer() {
+        if (isSleepTimerActive()) {
+            if (AppConfig.DEBUG)
+                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);
+            if (AppConfig.DEBUG) 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(final Playable media) {
+        if (media == null)
+            throw new IllegalArgumentException("media = null");
+
+        if (isChapterLoaderActive()) {
+            cancelChapterLoader();
+        }
+
+        Runnable chapterLoader = new Runnable() {
+            @Override
+            public void run() {
+                if (AppConfig.DEBUG)
+                    Log.d(TAG, "Chapter loader started");
+                if (media.getChapters() == null) {
+                    media.loadChapterMarks();
+                    if (!Thread.currentThread().isInterrupted() && media.getChapters() != null) {
+                        callback.onChapterLoaded(media);
+                    }
+                }
+                if (AppConfig.DEBUG)
+                    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() {
+        EventDistributor.getInstance().unregister(eventDistributorListener);
+        cancelAllTasks();
+        schedExecutor.shutdown();
+    }
+
+    /**
+     * Sleeps for a given time and then pauses playback.
+     */
+    private class SleepTimer implements Runnable {
+        private static final String TAG = "SleepTimer";
+        private static final long UPDATE_INTERVALL = 1000L;
+        private volatile long waitingTime;
+        private boolean isWaiting;
+
+        public SleepTimer(long waitingTime) {
+            super();
+            this.waitingTime = waitingTime;
+        }
+
+        @Override
+        public void run() {
+            isWaiting = true;
+            if (AppConfig.DEBUG)
+                Log.d(TAG, "Starting");
+            while (waitingTime > 0) {
+                try {
+                    Thread.sleep(UPDATE_INTERVALL);
+                    waitingTime -= UPDATE_INTERVALL;
+
+                    if (waitingTime <= 0) {
+                        if (AppConfig.DEBUG)
+                            Log.d(TAG, "Waiting completed");
+                        callback.onSleepTimerExpired();
+                        postExecute();
+                    }
+                } catch (InterruptedException e) {
+                    Log.d(TAG, "Thread was interrupted while waiting");
+                    break;
+                }
+            }
+            postExecute();
+        }
+
+        protected void postExecute() {
+            isWaiting = false;
+        }
+
+        public long getWaitingTime() {
+            return waitingTime;
+        }
+
+        public boolean isWaiting() {
+            return isWaiting;
+        }
+
+    }
+
+    public static interface PSTMCallback {
+        void positionSaverTick();
+
+        void onSleepTimerExpired();
+
+        void onWidgetUpdaterTick();
+
+        void onChapterLoaded(Playable media);
+    }
+}