summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authordaniel oeh <daniel.oeh@gmail.com>2013-08-04 15:35:18 +0200
committerdaniel oeh <daniel.oeh@gmail.com>2013-08-04 15:35:18 +0200
commite28229a29c1f60657c82f78507120b215167f039 (patch)
treeceff74c49b531dd0713758fa754db158c086b1aa /src
parent9ba3dc0d823b786700010a60d38f47c802101d27 (diff)
downloadAntennaPod-e28229a29c1f60657c82f78507120b215167f039.zip
Ported playback service to DB* classes
Diffstat (limited to 'src')
-rw-r--r--src/de/danoeh/antennapod/service/PlaybackService.java3030
-rw-r--r--src/de/danoeh/antennapod/storage/DBTasks.java6
-rw-r--r--src/de/danoeh/antennapod/util/QueueAccess.java3
3 files changed, 1560 insertions, 1479 deletions
diff --git a/src/de/danoeh/antennapod/service/PlaybackService.java b/src/de/danoeh/antennapod/service/PlaybackService.java
index 409ac6b48..edcc4157e 100644
--- a/src/de/danoeh/antennapod/service/PlaybackService.java
+++ b/src/de/danoeh/antennapod/service/PlaybackService.java
@@ -2,13 +2,8 @@ package de.danoeh.antennapod.service;
import java.io.IOException;
import java.util.Date;
-import java.util.concurrent.Future;
-import java.util.concurrent.RejectedExecutionHandler;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.ScheduledThreadPoolExecutor;
-import java.util.concurrent.ThreadFactory;
-import java.util.concurrent.ThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
+import java.util.List;
+import java.util.concurrent.*;
import android.annotation.SuppressLint;
import android.app.Notification;
@@ -40,1484 +35,1565 @@ import de.danoeh.antennapod.AppConfig;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.AudioplayerActivity;
import de.danoeh.antennapod.activity.VideoplayerActivity;
-import de.danoeh.antennapod.feed.Chapter;
-import de.danoeh.antennapod.feed.FeedComponent;
-import de.danoeh.antennapod.feed.FeedItem;
-import de.danoeh.antennapod.feed.FeedManager;
-import de.danoeh.antennapod.feed.FeedMedia;
-import de.danoeh.antennapod.feed.MediaType;
+import de.danoeh.antennapod.feed.*;
import de.danoeh.antennapod.preferences.PlaybackPreferences;
import de.danoeh.antennapod.preferences.UserPreferences;
import de.danoeh.antennapod.receiver.MediaButtonReceiver;
import de.danoeh.antennapod.receiver.PlayerWidget;
+import de.danoeh.antennapod.storage.DBReader;
+import de.danoeh.antennapod.storage.DBTasks;
+import de.danoeh.antennapod.storage.DBWriter;
import de.danoeh.antennapod.util.BitmapDecoder;
+import de.danoeh.antennapod.util.QueueAccess;
import de.danoeh.antennapod.util.flattr.FlattrUtils;
import de.danoeh.antennapod.util.playback.Playable;
import de.danoeh.antennapod.util.playback.Playable.PlayableException;
-/** Controls the MediaPlayer that plays a FeedMedia-file */
+/**
+ * Controls the MediaPlayer that plays a FeedMedia-file
+ */
public class PlaybackService extends Service {
- /** Logging tag */
- private static final String TAG = "PlaybackService";
-
- /** Parcelable of type Playable. */
- public static final String EXTRA_PLAYABLE = "PlaybackService.PlayableExtra";
- /** True if media should be streamed. */
- public static final String EXTRA_SHOULD_STREAM = "extra.de.danoeh.antennapod.service.shouldStream";
- /**
- * True if playback should be started immediately after media has been
- * prepared.
- */
- public static final String EXTRA_START_WHEN_PREPARED = "extra.de.danoeh.antennapod.service.startWhenPrepared";
-
- public static final String EXTRA_PREPARE_IMMEDIATELY = "extra.de.danoeh.antennapod.service.prepareImmediately";
-
- public static final String ACTION_PLAYER_STATUS_CHANGED = "action.de.danoeh.antennapod.service.playerStatusChanged";
- private static final String AVRCP_ACTION_PLAYER_STATUS_CHANGED = "com.android.music.playstatechanged";
-
- public static final String ACTION_PLAYER_NOTIFICATION = "action.de.danoeh.antennapod.service.playerNotification";
- public static final String EXTRA_NOTIFICATION_CODE = "extra.de.danoeh.antennapod.service.notificationCode";
- public static final String EXTRA_NOTIFICATION_TYPE = "extra.de.danoeh.antennapod.service.notificationType";
-
- /**
- * If the PlaybackService receives this action, it will stop playback and
- * try to shutdown.
- */
- public static final String ACTION_SHUTDOWN_PLAYBACK_SERVICE = "action.de.danoeh.antennapod.service.actionShutdownPlaybackService";
-
- /**
- * If the PlaybackService receives this action, it will end playback of the
- * current episode and load the next episode if there is one available.
- * */
- public static final String ACTION_SKIP_CURRENT_EPISODE = "action.de.danoeh.antennapod.service.skipCurrentEpisode";
-
- /** Used in NOTIFICATION_TYPE_RELOAD. */
- public static final int EXTRA_CODE_AUDIO = 1;
- public static final int EXTRA_CODE_VIDEO = 2;
-
- public static final int NOTIFICATION_TYPE_ERROR = 0;
- public static final int NOTIFICATION_TYPE_INFO = 1;
- public static final int NOTIFICATION_TYPE_BUFFER_UPDATE = 2;
- public static final int NOTIFICATION_TYPE_RELOAD = 3;
- /** The state of the sleeptimer changed. */
- public static final int NOTIFICATION_TYPE_SLEEPTIMER_UPDATE = 4;
- public static final int NOTIFICATION_TYPE_BUFFER_START = 5;
- public static final int NOTIFICATION_TYPE_BUFFER_END = 6;
- /** No more episodes are going to be played. */
- public static final int NOTIFICATION_TYPE_PLAYBACK_END = 7;
-
- /**
- * Returned by getPositionSafe() or getDurationSafe() if the playbackService
- * is in an invalid state.
- */
- public static final int INVALID_TIME = -1;
-
- /** Is true if service is running. */
- public static boolean isRunning = false;
-
- private static final int NOTIFICATION_ID = 1;
-
- private AudioManager audioManager;
- private ComponentName mediaButtonReceiver;
-
- private MediaPlayer player;
- private RemoteControlClient remoteControlClient;
-
- private Playable media;
-
- /** True if media should be streamed (Extracted from Intent Extra) . */
- private boolean shouldStream;
-
- /** True if service should prepare playback after it has been initialized */
- private boolean prepareImmediately;
- private boolean startWhenPrepared;
- private FeedManager manager;
- private PlayerStatus status;
-
- private PositionSaver positionSaver;
- private ScheduledFuture positionSaverFuture;
-
- private WidgetUpdateWorker widgetUpdater;
- private ScheduledFuture widgetUpdaterFuture;
-
- private SleepTimer sleepTimer;
- private Future sleepTimerFuture;
-
- private static final int SCHED_EX_POOL_SIZE = 3;
- private ScheduledThreadPoolExecutor schedExecutor;
-
- private volatile PlayerStatus statusBeforeSeek;
-
- private static boolean playingVideo;
-
- /** True if mediaplayer was paused because it lost audio focus temporarily */
- private boolean pausedBecauseOfTransientAudiofocusLoss;
-
- private Thread chapterLoader;
-
- private final IBinder mBinder = new LocalBinder();
-
- public class LocalBinder extends Binder {
- public PlaybackService getService() {
- return PlaybackService.this;
- }
- }
-
- @Override
- public boolean onUnbind(Intent intent) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Received onUnbind event");
- return super.onUnbind(intent);
- }
-
- /**
- * Returns an intent which starts an audio- or videoplayer, depending on the
- * type of media that is being played. If the playbackservice is not
- * running, the type of the last played media will be looked up.
- * */
- public static Intent getPlayerActivityIntent(Context context) {
- if (isRunning) {
- if (playingVideo) {
- return new Intent(context, VideoplayerActivity.class);
- } else {
- return new Intent(context, AudioplayerActivity.class);
- }
- } else {
- if (PlaybackPreferences.getCurrentEpisodeIsVideo()) {
- return new Intent(context, VideoplayerActivity.class);
- } else {
- return new Intent(context, AudioplayerActivity.class);
- }
- }
- }
-
- /**
- * Same as getPlayerActivityIntent(context), but here the type of activity
- * depends on the FeedMedia that is provided as an argument.
- */
- public static Intent getPlayerActivityIntent(Context context, Playable media) {
- MediaType mt = media.getMediaType();
- if (mt == MediaType.VIDEO) {
- return new Intent(context, VideoplayerActivity.class);
- } else {
- return new Intent(context, AudioplayerActivity.class);
- }
- }
-
- @SuppressLint("NewApi")
- @Override
- public void onCreate() {
- super.onCreate();
- if (AppConfig.DEBUG)
- Log.d(TAG, "Service created.");
- isRunning = true;
- pausedBecauseOfTransientAudiofocusLoss = false;
- status = PlayerStatus.STOPPED;
- audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
- manager = FeedManager.getInstance();
- 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;
- }
- }, new RejectedExecutionHandler() {
-
- @Override
- public void rejectedExecution(Runnable r,
- ThreadPoolExecutor executor) {
- Log.w(TAG, "SchedEx rejected submission of new task");
- }
- });
- player = createMediaPlayer();
-
- mediaButtonReceiver = new ComponentName(getPackageName(),
- MediaButtonReceiver.class.getName());
- audioManager.registerMediaButtonEventReceiver(mediaButtonReceiver);
- if (android.os.Build.VERSION.SDK_INT >= 14) {
- audioManager
- .registerRemoteControlClient(setupRemoteControlClient());
- }
- registerReceiver(headsetDisconnected, new IntentFilter(
- Intent.ACTION_HEADSET_PLUG));
- registerReceiver(shutdownReceiver, new IntentFilter(
- ACTION_SHUTDOWN_PLAYBACK_SERVICE));
- registerReceiver(audioBecomingNoisy, new IntentFilter(
- AudioManager.ACTION_AUDIO_BECOMING_NOISY));
- registerReceiver(skipCurrentEpisodeReceiver, new IntentFilter(
- ACTION_SKIP_CURRENT_EPISODE));
-
- }
-
- private MediaPlayer createMediaPlayer() {
- return createMediaPlayer(new MediaPlayer());
- }
-
- private MediaPlayer createMediaPlayer(MediaPlayer mp) {
- if (mp != null) {
- mp.setOnPreparedListener(preparedListener);
- mp.setOnCompletionListener(completionListener);
- mp.setOnSeekCompleteListener(onSeekCompleteListener);
- mp.setOnErrorListener(onErrorListener);
- mp.setOnBufferingUpdateListener(onBufferingUpdateListener);
- mp.setOnInfoListener(onInfoListener);
- }
- return mp;
- }
-
- @SuppressLint("NewApi")
- @Override
- public void onDestroy() {
- super.onDestroy();
- if (AppConfig.DEBUG)
- Log.d(TAG, "Service is about to be destroyed");
- isRunning = false;
- if (chapterLoader != null) {
- chapterLoader.interrupt();
- }
- disableSleepTimer();
- unregisterReceiver(headsetDisconnected);
- unregisterReceiver(shutdownReceiver);
- unregisterReceiver(audioBecomingNoisy);
- unregisterReceiver(skipCurrentEpisodeReceiver);
- if (android.os.Build.VERSION.SDK_INT >= 14) {
- audioManager.unregisterRemoteControlClient(remoteControlClient);
- }
- audioManager.unregisterMediaButtonEventReceiver(mediaButtonReceiver);
- audioManager.abandonAudioFocus(audioFocusChangeListener);
- player.release();
- stopWidgetUpdater();
- updateWidget();
- }
-
- @Override
- public IBinder onBind(Intent intent) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Received onBind event");
- return mBinder;
- }
-
- private final OnAudioFocusChangeListener audioFocusChangeListener = new OnAudioFocusChangeListener() {
-
- @Override
- public void onAudioFocusChange(int focusChange) {
- switch (focusChange) {
- case AudioManager.AUDIOFOCUS_LOSS:
- if (AppConfig.DEBUG)
- Log.d(TAG, "Lost audio focus");
- pause(true, false);
- stopSelf();
- break;
- case AudioManager.AUDIOFOCUS_GAIN:
- if (AppConfig.DEBUG)
- Log.d(TAG, "Gained audio focus");
- if (pausedBecauseOfTransientAudiofocusLoss) {
- audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC,
- AudioManager.ADJUST_RAISE, 0);
- play();
- }
- break;
- case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
- if (status == PlayerStatus.PLAYING) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Lost audio focus temporarily. Ducking...");
- audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC,
- AudioManager.ADJUST_LOWER, 0);
- pausedBecauseOfTransientAudiofocusLoss = true;
- }
- break;
- case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
- if (status == PlayerStatus.PLAYING) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Lost audio focus temporarily. Pausing...");
- pause(false, false);
- pausedBecauseOfTransientAudiofocusLoss = true;
- }
- }
- }
- };
-
- @Override
- public int onStartCommand(Intent intent, int flags, int startId) {
- super.onStartCommand(intent, flags, startId);
-
- if (AppConfig.DEBUG)
- Log.d(TAG, "OnStartCommand called");
- int keycode = intent.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1);
- if (keycode != -1) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Received media button event");
- handleKeycode(keycode);
- } else {
-
- Playable playable = intent.getParcelableExtra(EXTRA_PLAYABLE);
- boolean playbackType = intent.getBooleanExtra(EXTRA_SHOULD_STREAM,
- true);
- if (playable == null) {
- Log.e(TAG, "Playable extra wasn't sent to the service");
- if (media == null) {
- stopSelf();
- }
- // Intent values appear to be valid
- // check if already playing and playbackType is the same
- } else if (media == null
- || !playable.getIdentifier().equals(media.getIdentifier())
- || playbackType != shouldStream) {
- pause(true, false);
- player.reset();
- sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0);
- if (media == null
- || !playable.getIdentifier().equals(
- media.getIdentifier())) {
- media = playable;
- }
-
- if (media != null) {
- shouldStream = playbackType;
- startWhenPrepared = intent.getBooleanExtra(
- EXTRA_START_WHEN_PREPARED, false);
- prepareImmediately = intent.getBooleanExtra(
- EXTRA_PREPARE_IMMEDIATELY, false);
- initMediaplayer();
-
- } else {
- Log.e(TAG, "Media is null");
- stopSelf();
- }
-
- } else if (media != null) {
- if (status == PlayerStatus.PAUSED) {
- play();
- }
-
- } else {
- Log.w(TAG, "Something went wrong. Shutting down...");
- stopSelf();
- }
- }
- return Service.START_NOT_STICKY;
- }
-
- /** Handles media button events */
- private void handleKeycode(int keycode) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Handling keycode: " + keycode);
- switch (keycode) {
- case KeyEvent.KEYCODE_HEADSETHOOK:
- case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
- if (status == PlayerStatus.PLAYING) {
- pause(true, true);
- } else if (status == PlayerStatus.PAUSED) {
- play();
- } else if (status == PlayerStatus.PREPARING) {
- setStartWhenPrepared(!startWhenPrepared);
- } else if (status == PlayerStatus.INITIALIZED) {
- startWhenPrepared = true;
- prepare();
- }
- break;
- case KeyEvent.KEYCODE_MEDIA_PLAY:
- if (status == PlayerStatus.PAUSED) {
- play();
- } else if (status == PlayerStatus.INITIALIZED) {
- startWhenPrepared = true;
- prepare();
- }
- break;
- case KeyEvent.KEYCODE_MEDIA_PAUSE:
- if (status == PlayerStatus.PLAYING) {
- pause(true, true);
- }
- break;
- }
- }
-
- /**
- * Called by a mediaplayer Activity as soon as it has prepared its
- * mediaplayer.
- */
- public void setVideoSurface(SurfaceHolder sh) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Setting display");
- player.setDisplay(null);
- player.setDisplay(sh);
- if (status == PlayerStatus.STOPPED
- || status == PlayerStatus.AWAITING_VIDEO_SURFACE) {
- try {
- InitTask initTask = new InitTask() {
-
- @Override
- protected void onPostExecute(Playable result) {
- if (status == PlayerStatus.INITIALIZING) {
- if (result != null) {
- try {
- if (shouldStream) {
- player.setDataSource(media
- .getStreamUrl());
- setStatus(PlayerStatus.PREPARING);
- player.prepareAsync();
- } else {
- player.setDataSource(media
- .getLocalMediaUrl());
- setStatus(PlayerStatus.PREPARING);
- player.prepareAsync();
- }
- } catch (IOException e) {
- e.printStackTrace();
- }
- } else {
- setStatus(PlayerStatus.ERROR);
- sendBroadcast(new Intent(
- ACTION_SHUTDOWN_PLAYBACK_SERVICE));
- }
- }
- }
-
- @Override
- protected void onPreExecute() {
- setStatus(PlayerStatus.INITIALIZING);
- }
-
- };
- initTask.executeAsync(media);
- } catch (IllegalArgumentException e) {
- e.printStackTrace();
- } catch (SecurityException e) {
- e.printStackTrace();
- } catch (IllegalStateException e) {
- e.printStackTrace();
- }
- }
-
- }
-
- /** Called when the surface holder of the mediaplayer has to be changed. */
- private void resetVideoSurface() {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Resetting video surface");
- cancelPositionSaver();
- player.setDisplay(null);
- player.reset();
- player.release();
- player = createMediaPlayer();
- status = PlayerStatus.STOPPED;
- if (media != null) {
- initMediaplayer();
- }
- }
-
- public void notifyVideoSurfaceAbandoned() {
- resetVideoSurface();
- }
-
- /** Called after service has extracted the media it is supposed to play. */
- private void initMediaplayer() {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Setting up media player");
- try {
- MediaType mediaType = media.getMediaType();
- if (mediaType == MediaType.AUDIO) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Mime type is audio");
-
- InitTask initTask = new InitTask() {
-
- @Override
- protected void onPostExecute(Playable result) {
- // check if state of service has changed. If it has
- // changed, assume that loaded metadata is not needed
- // anymore.
- if (status == PlayerStatus.INITIALIZING) {
- if (result != null) {
- playingVideo = false;
- try {
- if (shouldStream) {
- player.setDataSource(media
- .getStreamUrl());
- } else if (media.localFileAvailable()) {
- player.setDataSource(media
- .getLocalMediaUrl());
- }
-
- if (prepareImmediately) {
- setStatus(PlayerStatus.PREPARING);
- player.prepareAsync();
- } else {
- setStatus(PlayerStatus.INITIALIZED);
- }
- } catch (IOException e) {
- e.printStackTrace();
- media = null;
- setStatus(PlayerStatus.ERROR);
- sendBroadcast(new Intent(
- ACTION_SHUTDOWN_PLAYBACK_SERVICE));
- }
- } else {
- Log.e(TAG, "InitTask could not load metadata");
- media = null;
- setStatus(PlayerStatus.ERROR);
- sendBroadcast(new Intent(
- ACTION_SHUTDOWN_PLAYBACK_SERVICE));
- }
- } else {
- if (AppConfig.DEBUG)
- Log.d(TAG,
- "Status of player has changed during initialization. Stopping init process.");
- }
- }
-
- @Override
- protected void onPreExecute() {
- setStatus(PlayerStatus.INITIALIZING);
- }
-
- };
- initTask.executeAsync(media);
- } else if (mediaType == MediaType.VIDEO) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Mime type is video");
- playingVideo = true;
- setStatus(PlayerStatus.AWAITING_VIDEO_SURFACE);
- player.setScreenOnWhilePlaying(true);
- }
-
- } catch (IllegalArgumentException e) {
- e.printStackTrace();
- } catch (SecurityException e) {
- e.printStackTrace();
- } catch (IllegalStateException e) {
- e.printStackTrace();
- }
- }
-
- private void setupPositionSaver() {
- if (positionSaverFuture == null
- || (positionSaverFuture.isCancelled() || positionSaverFuture
- .isDone())) {
-
- positionSaver = new PositionSaver();
- positionSaverFuture = schedExecutor.scheduleAtFixedRate(
- positionSaver, PositionSaver.WAITING_INTERVALL,
- PositionSaver.WAITING_INTERVALL, TimeUnit.MILLISECONDS);
- }
- }
-
- private void cancelPositionSaver() {
- if (positionSaverFuture != null) {
- boolean result = positionSaverFuture.cancel(true);
- if (AppConfig.DEBUG)
- Log.d(TAG, "PositionSaver cancelled. Result: " + result);
- }
- }
-
- private MediaPlayer.OnPreparedListener preparedListener = new MediaPlayer.OnPreparedListener() {
- @Override
- public void onPrepared(MediaPlayer mp) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Resource prepared");
- mp.seekTo(media.getPosition());
- if (media.getDuration() == 0) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Setting duration of media");
- media.setDuration(mp.getDuration());
- }
- setStatus(PlayerStatus.PREPARED);
- if (chapterLoader != null) {
- chapterLoader.interrupt();
- }
- chapterLoader = new Thread() {
- @Override
- public void run() {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Chapter loader started");
- if (media != null && media.getChapters() == null) {
- media.loadChapterMarks();
- if (!isInterrupted() && media.getChapters() != null) {
- sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD,
- 0);
- }
- }
- if (AppConfig.DEBUG)
- Log.d(TAG, "Chapter loader stopped");
- }
- };
- chapterLoader.start();
-
- if (startWhenPrepared) {
- play();
- }
- }
- };
-
- private MediaPlayer.OnSeekCompleteListener onSeekCompleteListener = new MediaPlayer.OnSeekCompleteListener() {
-
- @Override
- public void onSeekComplete(MediaPlayer mp) {
- if (status == PlayerStatus.SEEKING) {
- setStatus(statusBeforeSeek);
- }
-
- }
- };
-
- private MediaPlayer.OnInfoListener onInfoListener = new MediaPlayer.OnInfoListener() {
-
- @Override
- public boolean onInfo(MediaPlayer mp, int what, int extra) {
- switch (what) {
- case MediaPlayer.MEDIA_INFO_BUFFERING_START:
- sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_START, 0);
- return true;
- case MediaPlayer.MEDIA_INFO_BUFFERING_END:
- sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_END, 0);
- return true;
- default:
- return false;
- }
- }
- };
-
- private MediaPlayer.OnErrorListener onErrorListener = new MediaPlayer.OnErrorListener() {
- private static final String TAG = "PlaybackService.onErrorListener";
-
- @Override
- public boolean onError(MediaPlayer mp, int what, int extra) {
- Log.w(TAG, "An error has occured: " + what);
- if (mp.isPlaying()) {
- pause(true, true);
- }
- sendNotificationBroadcast(NOTIFICATION_TYPE_ERROR, what);
- setCurrentlyPlayingMedia(PlaybackPreferences.NO_MEDIA_PLAYING);
- stopSelf();
- return true;
- }
- };
-
- private MediaPlayer.OnCompletionListener completionListener = new MediaPlayer.OnCompletionListener() {
-
- @Override
- public void onCompletion(MediaPlayer mp) {
- endPlayback(true);
- }
- };
-
- private MediaPlayer.OnBufferingUpdateListener onBufferingUpdateListener = new MediaPlayer.OnBufferingUpdateListener() {
-
- @Override
- public void onBufferingUpdate(MediaPlayer mp, int percent) {
- sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_UPDATE, percent);
-
- }
- };
-
- private void endPlayback(boolean playNextEpisode) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Playback ended");
- audioManager.abandonAudioFocus(audioFocusChangeListener);
-
- // Save state
- cancelPositionSaver();
-
- boolean isInQueue = false;
- FeedItem nextItem = null;
-
- if (media instanceof FeedMedia) {
- FeedItem item = ((FeedMedia) media).getItem();
- ((FeedMedia) media).setPlaybackCompletionDate(new Date());
- manager.markItemRead(PlaybackService.this, item, true, true);
- nextItem = manager.getQueueSuccessorOfItem(item);
- isInQueue = media instanceof FeedMedia
- && manager.isInQueue(((FeedMedia) media).getItem());
- if (isInQueue) {
- manager.removeQueueItem(PlaybackService.this, item, true);
- }
- manager.addItemToPlaybackHistory(PlaybackService.this, item);
- manager.setFeedMedia(PlaybackService.this, (FeedMedia) media);
- long autoDeleteMediaId = ((FeedComponent) media).getId();
- if (shouldStream) {
- autoDeleteMediaId = -1;
- }
- }
-
- // Load next episode if previous episode was in the queue and if there
- // is an episode in the queue left.
- // Start playback immediately if continuous playback is enabled
- boolean loadNextItem = isInQueue && nextItem != null;
- playNextEpisode = playNextEpisode && loadNextItem
- && UserPreferences.isFollowQueue();
- if (loadNextItem) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Loading next item in queue");
- media = nextItem.getMedia();
- }
-
- if (playNextEpisode) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Playback of next episode will start immediately.");
- prepareImmediately = startWhenPrepared = true;
- } else {
- if (AppConfig.DEBUG)
- Log.d(TAG, "No more episodes available to play");
- media = null;
- prepareImmediately = startWhenPrepared = false;
- stopForeground(true);
- stopWidgetUpdater();
- }
-
- int notificationCode = 0;
- if (media != null) {
- shouldStream = !media.localFileAvailable();
- if (media.getMediaType() == MediaType.AUDIO) {
- notificationCode = EXTRA_CODE_AUDIO;
- playingVideo = false;
- } else if (media.getMediaType() == MediaType.VIDEO) {
- notificationCode = EXTRA_CODE_VIDEO;
- }
- }
- writePlaybackPreferences();
- if (media != null) {
- resetVideoSurface();
- refreshRemoteControlClientState();
- sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD,
- notificationCode);
- } else {
- sendNotificationBroadcast(NOTIFICATION_TYPE_PLAYBACK_END, 0);
- stopSelf();
- }
- }
-
- public void setSleepTimer(long waitingTime) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Setting sleep timer to " + Long.toString(waitingTime)
- + " milliseconds");
- if (sleepTimerFuture != null) {
- sleepTimerFuture.cancel(true);
- }
- sleepTimer = new SleepTimer(waitingTime);
- sleepTimerFuture = schedExecutor.submit(sleepTimer);
- sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0);
- }
-
- public void disableSleepTimer() {
- if (sleepTimerFuture != null) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Disabling sleep timer");
- sleepTimerFuture.cancel(true);
- sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0);
- }
- }
-
- /**
- * Saves the current position and pauses playback. Note that, if audiofocus
- * is abandoned, the lockscreen controls will also disapear.
- *
- * @param abandonFocus
- * is true if the service should release audio focus
- * @param reset
- * is true if service should reinit after pausing if the media
- * file is being streamed
- */
- public void pause(boolean abandonFocus, boolean reinit) {
- if (player.isPlaying()) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Pausing playback.");
- player.pause();
- if (abandonFocus) {
- audioManager.abandonAudioFocus(audioFocusChangeListener);
- pausedBecauseOfTransientAudiofocusLoss = false;
- disableSleepTimer();
- }
- cancelPositionSaver();
- saveCurrentPosition();
- setStatus(PlayerStatus.PAUSED);
- stopWidgetUpdater();
- stopForeground(true);
- if (shouldStream && reinit) {
- reinit();
- }
- }
- }
-
- /** Pauses playback and destroys service. Recommended for video playback. */
- public void stop() {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Stopping playback");
- if (status == PlayerStatus.PREPARED || status == PlayerStatus.PAUSED
- || status == PlayerStatus.STOPPED
- || status == PlayerStatus.PLAYING) {
- player.stop();
- }
- setCurrentlyPlayingMedia(PlaybackPreferences.NO_MEDIA_PLAYING);
- stopSelf();
- }
-
- /**
- * Prepared media player for playback if the service is in the INITALIZED
- * state.
- */
- public void prepare() {
- if (status == PlayerStatus.INITIALIZED) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Preparing media player");
- setStatus(PlayerStatus.PREPARING);
- player.prepareAsync();
- }
- }
-
- /** Resets the media player and moves into INITIALIZED state. */
- public void reinit() {
- player.reset();
- player = createMediaPlayer(player);
- prepareImmediately = false;
- initMediaplayer();
- }
-
- @SuppressLint("NewApi")
- public void play() {
- if (status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED
- || status == PlayerStatus.STOPPED) {
- int focusGained = audioManager.requestAudioFocus(
- audioFocusChangeListener, AudioManager.STREAM_MUSIC,
- AudioManager.AUDIOFOCUS_GAIN);
-
- if (focusGained == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Audiofocus successfully requested");
- if (AppConfig.DEBUG)
- Log.d(TAG, "Resuming/Starting playback");
- writePlaybackPreferences();
-
- player.start();
- if (status != PlayerStatus.PAUSED) {
- player.seekTo((int) media.getPosition());
- }
- setStatus(PlayerStatus.PLAYING);
- setupPositionSaver();
- setupWidgetUpdater();
- setupNotification();
- pausedBecauseOfTransientAudiofocusLoss = false;
- if (android.os.Build.VERSION.SDK_INT >= 14) {
- audioManager
- .registerRemoteControlClient(remoteControlClient);
- }
- audioManager
- .registerMediaButtonEventReceiver(mediaButtonReceiver);
- media.onPlaybackStart();
- } else {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Failed to request Audiofocus");
- }
- }
- }
-
- private void writePlaybackPreferences() {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Writing playback preferences");
-
- SharedPreferences.Editor editor = PreferenceManager
- .getDefaultSharedPreferences(getApplicationContext()).edit();
- if (media != null) {
- editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA,
- media.getPlayableType());
- editor.putBoolean(
- PlaybackPreferences.PREF_CURRENT_EPISODE_IS_STREAM,
- shouldStream);
- editor.putBoolean(
- PlaybackPreferences.PREF_CURRENT_EPISODE_IS_VIDEO,
- playingVideo);
- if (media instanceof FeedMedia) {
- FeedMedia fMedia = (FeedMedia) media;
- editor.putLong(
- PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID,
- fMedia.getItem().getFeed().getId());
- editor.putLong(
- PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID,
- fMedia.getId());
- } else {
- editor.putLong(
- PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID,
- PlaybackPreferences.NO_MEDIA_PLAYING);
- editor.putLong(
- PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID,
- PlaybackPreferences.NO_MEDIA_PLAYING);
- }
- media.writeToPreferences(editor);
- } else {
- editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA,
- PlaybackPreferences.NO_MEDIA_PLAYING);
- editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID,
- PlaybackPreferences.NO_MEDIA_PLAYING);
- editor.putLong(
- PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID,
- PlaybackPreferences.NO_MEDIA_PLAYING);
- }
-
- editor.commit();
- }
-
- private void setStatus(PlayerStatus newStatus) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Setting status to " + newStatus);
- status = newStatus;
- sendBroadcast(new Intent(ACTION_PLAYER_STATUS_CHANGED));
- updateWidget();
- refreshRemoteControlClientState();
- bluetoothNotifyChange();
- }
-
- /** Send ACTION_PLAYER_STATUS_CHANGED without changing the status attribute. */
- private void postStatusUpdateIntent() {
- setStatus(status);
- }
-
- private void sendNotificationBroadcast(int type, int code) {
- Intent intent = new Intent(ACTION_PLAYER_NOTIFICATION);
- intent.putExtra(EXTRA_NOTIFICATION_TYPE, type);
- intent.putExtra(EXTRA_NOTIFICATION_CODE, code);
- sendBroadcast(intent);
- }
-
- /** Used by setupNotification to load notification data in another thread. */
- private AsyncTask<Void, Void, Void> notificationSetupTask;
-
- /** Prepares notification and starts the service in the foreground. */
- @SuppressLint("NewApi")
- private void setupNotification() {
- final PendingIntent pIntent = PendingIntent.getActivity(this, 0,
- PlaybackService.getPlayerActivityIntent(this),
- PendingIntent.FLAG_UPDATE_CURRENT);
-
- if (notificationSetupTask != null) {
- notificationSetupTask.cancel(true);
- }
- notificationSetupTask = new AsyncTask<Void, Void, Void>() {
- Bitmap icon = null;
-
- @Override
- protected Void doInBackground(Void... params) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Starting background work");
- if (android.os.Build.VERSION.SDK_INT >= 11) {
- if (media != null && media != null) {
- int iconSize = getResources().getDimensionPixelSize(
- android.R.dimen.notification_large_icon_width);
- icon = BitmapDecoder
- .decodeBitmapFromWorkerTaskResource(iconSize,
- media);
- }
-
- }
- if (icon == null) {
- icon = BitmapFactory.decodeResource(getResources(),
- R.drawable.ic_stat_antenna);
- }
-
- return null;
- }
-
- @Override
- protected void onPostExecute(Void result) {
- super.onPostExecute(result);
- if (!isCancelled() && status == PlayerStatus.PLAYING
- && media != null) {
- String contentText = media.getFeedTitle();
- String contentTitle = media.getEpisodeTitle();
- Notification notification = null;
- if (android.os.Build.VERSION.SDK_INT >= 16) {
- Intent pauseButtonIntent = new Intent(
- PlaybackService.this, PlaybackService.class);
- pauseButtonIntent.putExtra(
- MediaButtonReceiver.EXTRA_KEYCODE,
- KeyEvent.KEYCODE_MEDIA_PAUSE);
- PendingIntent pauseButtonPendingIntent = PendingIntent
- .getService(PlaybackService.this, 0,
- pauseButtonIntent,
- PendingIntent.FLAG_UPDATE_CURRENT);
- Notification.Builder notificationBuilder = new Notification.Builder(
- PlaybackService.this)
- .setContentTitle(contentTitle)
- .setContentText(contentText)
- .setOngoing(true)
- .setContentIntent(pIntent)
- .setLargeIcon(icon)
- .setSmallIcon(R.drawable.ic_stat_antenna)
- .addAction(android.R.drawable.ic_media_pause,
- getString(R.string.pause_label),
- pauseButtonPendingIntent);
- notification = notificationBuilder.build();
- } else {
- NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(
- PlaybackService.this)
- .setContentTitle(contentTitle)
- .setContentText(contentText).setOngoing(true)
- .setContentIntent(pIntent).setLargeIcon(icon)
- .setSmallIcon(R.drawable.ic_stat_antenna);
- notification = notificationBuilder.getNotification();
- }
- startForeground(NOTIFICATION_ID, notification);
- if (AppConfig.DEBUG)
- Log.d(TAG, "Notification set up");
- }
- }
-
- };
- if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) {
- notificationSetupTask
- .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
- } else {
- notificationSetupTask.execute();
- }
-
- }
-
- /**
- * Seek a specific position from the current position
- *
- * @param delta
- * offset from current position (positive or negative)
- * */
- public void seekDelta(int delta) {
- int position = getCurrentPositionSafe();
- if (position != INVALID_TIME) {
- seek(player.getCurrentPosition() + delta);
- }
- }
-
- public void seek(int i) {
- saveCurrentPosition();
- if (status == PlayerStatus.INITIALIZED
- || status == PlayerStatus.INITIALIZING
- || status == PlayerStatus.PREPARING) {
- media.setPosition(i);
- setStartWhenPrepared(true);
- prepare();
- } else {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Seeking position " + i);
- if (shouldStream) {
- if (status != PlayerStatus.SEEKING) {
- statusBeforeSeek = status;
- }
- setStatus(PlayerStatus.SEEKING);
- }
- player.seekTo(i);
- }
- }
-
- public void seekToChapter(Chapter chapter) {
- seek((int) chapter.getStart());
- }
-
- /** Saves the current position of the media file to the DB */
- private synchronized void saveCurrentPosition() {
- int position = getCurrentPositionSafe();
- if (position != INVALID_TIME) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Saving current position to " + position);
- media.saveCurrentPosition(PreferenceManager
- .getDefaultSharedPreferences(getApplicationContext()),
- position);
- }
- }
-
- private void stopWidgetUpdater() {
- if (widgetUpdaterFuture != null) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Stopping widgetUpdateWorker");
- widgetUpdaterFuture.cancel(true);
- }
- sendBroadcast(new Intent(PlayerWidget.STOP_WIDGET_UPDATE));
- }
-
- @SuppressLint("NewApi")
- private void setupWidgetUpdater() {
- if (widgetUpdaterFuture == null
- || (widgetUpdaterFuture.isCancelled() || widgetUpdaterFuture
- .isDone())) {
- widgetUpdater = new WidgetUpdateWorker();
- widgetUpdaterFuture = schedExecutor.scheduleAtFixedRate(
- widgetUpdater, WidgetUpdateWorker.NOTIFICATION_INTERVALL,
- WidgetUpdateWorker.NOTIFICATION_INTERVALL,
- TimeUnit.MILLISECONDS);
- }
- }
-
- private void updateWidget() {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Sending widget update request");
- PlaybackService.this.sendBroadcast(new Intent(
- PlayerWidget.FORCE_WIDGET_UPDATE));
- }
-
- public boolean sleepTimerActive() {
- return sleepTimer != null && sleepTimer.isWaiting();
- }
-
- public long getSleepTimerTimeLeft() {
- if (sleepTimerActive()) {
- return sleepTimer.getWaitingTime();
- } else {
- return 0;
- }
- }
-
- @SuppressLint("NewApi")
- private RemoteControlClient setupRemoteControlClient() {
- Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
- mediaButtonIntent.setComponent(mediaButtonReceiver);
- PendingIntent mediaPendingIntent = PendingIntent.getBroadcast(
- getApplicationContext(), 0, mediaButtonIntent, 0);
- remoteControlClient = new RemoteControlClient(mediaPendingIntent);
- int controlFlags;
- if (android.os.Build.VERSION.SDK_INT < 16) {
- controlFlags = RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE
- | RemoteControlClient.FLAG_KEY_MEDIA_NEXT;
- } else {
- controlFlags = RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE;
- }
- remoteControlClient.setTransportControlFlags(controlFlags);
- return remoteControlClient;
- }
-
- /** Refresh player status and metadata. */
- @SuppressLint("NewApi")
- private void refreshRemoteControlClientState() {
- if (android.os.Build.VERSION.SDK_INT >= 14) {
- if (remoteControlClient != null) {
- switch (status) {
- case PLAYING:
- remoteControlClient
- .setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING);
- break;
- case PAUSED:
- case INITIALIZED:
- remoteControlClient
- .setPlaybackState(RemoteControlClient.PLAYSTATE_PAUSED);
- break;
- case STOPPED:
- remoteControlClient
- .setPlaybackState(RemoteControlClient.PLAYSTATE_STOPPED);
- break;
- case ERROR:
- remoteControlClient
- .setPlaybackState(RemoteControlClient.PLAYSTATE_ERROR);
- break;
- default:
- remoteControlClient
- .setPlaybackState(RemoteControlClient.PLAYSTATE_BUFFERING);
- }
- if (media != null) {
- MetadataEditor editor = remoteControlClient
- .editMetadata(false);
- editor.putString(MediaMetadataRetriever.METADATA_KEY_TITLE,
- media.getEpisodeTitle());
-
- editor.putString(MediaMetadataRetriever.METADATA_KEY_ALBUM,
- media.getFeedTitle());
-
- editor.apply();
- }
- if (AppConfig.DEBUG)
- Log.d(TAG, "RemoteControlClient state was refreshed");
- }
- }
- }
-
- private void bluetoothNotifyChange() {
- boolean isPlaying = false;
-
- if (status == PlayerStatus.PLAYING) {
- isPlaying = true;
- }
-
- Intent i = new Intent(AVRCP_ACTION_PLAYER_STATUS_CHANGED);
- i.putExtra("id", 1);
- i.putExtra("artist", "");
- i.putExtra("album", media.getFeedTitle());
- i.putExtra("track", media.getEpisodeTitle());
- i.putExtra("playing", isPlaying);
- i.putExtra("ListSize", manager.getQueueSize(false));
- i.putExtra("duration", media.getDuration());
- i.putExtra("position", media.getPosition());
- sendBroadcast(i);
- }
-
- /**
- * Pauses playback when the headset is disconnected and the preference is
- * set
- */
- private BroadcastReceiver headsetDisconnected = new BroadcastReceiver() {
- private static final String TAG = "headsetDisconnected";
- private static final int UNPLUGGED = 0;
-
- @Override
- public void onReceive(Context context, Intent intent) {
- if (intent.getAction().equals(Intent.ACTION_HEADSET_PLUG)) {
- int state = intent.getIntExtra("state", -1);
- if (state != -1) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Headset plug event. State is " + state);
- if (state == UNPLUGGED && status == PlayerStatus.PLAYING) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Headset was unplugged during playback.");
- pauseIfPauseOnDisconnect();
- }
- } else {
- Log.e(TAG, "Received invalid ACTION_HEADSET_PLUG intent");
- }
- }
- }
- };
-
- private BroadcastReceiver audioBecomingNoisy = new BroadcastReceiver() {
-
- @Override
- public void onReceive(Context context, Intent intent) {
- // sound is about to change, eg. bluetooth -> speaker
- if (AppConfig.DEBUG)
- Log.d(TAG, "Pausing playback because audio is becoming noisy");
- pauseIfPauseOnDisconnect();
- }
- // android.media.AUDIO_BECOMING_NOISY
- };
-
- /** Pauses playback if PREF_PAUSE_ON_HEADSET_DISCONNECT was set to true. */
- private void pauseIfPauseOnDisconnect() {
- if (UserPreferences.isPauseOnHeadsetDisconnect()
- && status == PlayerStatus.PLAYING) {
- pause(true, true);
- }
- }
-
- private BroadcastReceiver shutdownReceiver = new BroadcastReceiver() {
-
- @Override
- public void onReceive(Context context, Intent intent) {
- if (intent.getAction().equals(ACTION_SHUTDOWN_PLAYBACK_SERVICE)) {
- schedExecutor.shutdownNow();
- stop();
- media = null;
- }
- }
-
- };
-
- private BroadcastReceiver skipCurrentEpisodeReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- if (intent.getAction().equals(ACTION_SKIP_CURRENT_EPISODE)) {
-
- if (AppConfig.DEBUG)
- Log.d(TAG, "Received SKIP_CURRENT_EPISODE intent");
- if (media != null) {
- setStatus(PlayerStatus.STOPPED);
- player.reset();
- endPlayback(false);
- }
- }
- }
- };
-
- /** Periodically saves the position of the media file */
- class PositionSaver implements Runnable {
- public static final int WAITING_INTERVALL = 5000;
-
- @Override
- public void run() {
- if (player != null && player.isPlaying()) {
- try {
- saveCurrentPosition();
- } catch (IllegalStateException e) {
- Log.w(TAG,
- "saveCurrentPosition was called in illegal state");
- }
- }
- }
- }
-
- /** Notifies the player widget in the specified intervall */
- class WidgetUpdateWorker implements Runnable {
- private static final int NOTIFICATION_INTERVALL = 1000;
-
- @Override
- public void run() {
- if (PlaybackService.isRunning) {
- updateWidget();
- }
- }
- }
-
- /** Sleeps for a given time and then pauses playback. */
- 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");
- if (status == PlayerStatus.PLAYING) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Pausing playback");
- pause(true, true);
- }
- postExecute();
- }
- } catch (InterruptedException e) {
- Log.d(TAG, "Thread was interrupted while waiting");
- break;
- }
- }
- postExecute();
- }
-
- protected void postExecute() {
- isWaiting = false;
- sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0);
- }
-
- public long getWaitingTime() {
- return waitingTime;
- }
-
- public boolean isWaiting() {
- return isWaiting;
- }
-
- }
-
- public static boolean isPlayingVideo() {
- return playingVideo;
- }
-
- public boolean isShouldStream() {
- return shouldStream;
- }
-
- public PlayerStatus getStatus() {
- return status;
- }
-
- public Playable getMedia() {
- return media;
- }
-
- public MediaPlayer getPlayer() {
- return player;
- }
-
- public boolean isStartWhenPrepared() {
- return startWhenPrepared;
- }
-
- public void setStartWhenPrepared(boolean startWhenPrepared) {
- this.startWhenPrepared = startWhenPrepared;
- postStatusUpdateIntent();
- }
-
- /**
- * call getDuration() on mediaplayer or return INVALID_TIME if player is in
- * an invalid state. This method should be used instead of calling
- * getDuration() directly to avoid an error.
- */
- public int getDurationSafe() {
- if (status != null && player != null) {
- switch (status) {
- case PREPARED:
- case PLAYING:
- case PAUSED:
- case SEEKING:
- try {
- return player.getDuration();
- } catch (IllegalStateException e) {
- e.printStackTrace();
- return INVALID_TIME;
- }
- default:
- return INVALID_TIME;
- }
- } else {
- return INVALID_TIME;
- }
- }
-
- /**
- * call getCurrentPosition() on mediaplayer or return INVALID_TIME if player
- * is in an invalid state. This method should be used instead of calling
- * getCurrentPosition() directly to avoid an error.
- */
- public int getCurrentPositionSafe() {
- if (status != null && player != null) {
- switch (status) {
- case PREPARED:
- case PLAYING:
- case PAUSED:
- case SEEKING:
- return player.getCurrentPosition();
- default:
- return INVALID_TIME;
- }
- } else {
- return INVALID_TIME;
- }
- }
-
- private void setCurrentlyPlayingMedia(long id) {
- SharedPreferences.Editor editor = PreferenceManager
- .getDefaultSharedPreferences(getApplicationContext()).edit();
- editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA, id);
- editor.commit();
- }
-
- private static class InitTask extends AsyncTask<Playable, Void, Playable> {
- private Playable playable;
- public PlayableException exception;
-
- @Override
- protected Playable doInBackground(Playable... params) {
- if (params[0] == null) {
- throw new IllegalArgumentException("Playable must not be null");
- }
- playable = params[0];
-
- try {
- playable.loadMetadata();
- } catch (PlayableException e) {
- e.printStackTrace();
- exception = e;
- return null;
- }
- return playable;
- }
-
- @SuppressLint("NewApi")
- public void executeAsync(Playable playable) {
- FlattrUtils.hasToken();
- if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) {
- executeOnExecutor(THREAD_POOL_EXECUTOR, playable);
- } else {
- execute(playable);
- }
- }
-
- }
+ /**
+ * Logging tag
+ */
+ private static final String TAG = "PlaybackService";
+
+ /**
+ * Parcelable of type Playable.
+ */
+ public static final String EXTRA_PLAYABLE = "PlaybackService.PlayableExtra";
+ /**
+ * True if media should be streamed.
+ */
+ public static final String EXTRA_SHOULD_STREAM = "extra.de.danoeh.antennapod.service.shouldStream";
+ /**
+ * True if playback should be started immediately after media has been
+ * prepared.
+ */
+ public static final String EXTRA_START_WHEN_PREPARED = "extra.de.danoeh.antennapod.service.startWhenPrepared";
+
+ public static final String EXTRA_PREPARE_IMMEDIATELY = "extra.de.danoeh.antennapod.service.prepareImmediately";
+
+ public static final String ACTION_PLAYER_STATUS_CHANGED = "action.de.danoeh.antennapod.service.playerStatusChanged";
+ private static final String AVRCP_ACTION_PLAYER_STATUS_CHANGED = "com.android.music.playstatechanged";
+
+ public static final String ACTION_PLAYER_NOTIFICATION = "action.de.danoeh.antennapod.service.playerNotification";
+ public static final String EXTRA_NOTIFICATION_CODE = "extra.de.danoeh.antennapod.service.notificationCode";
+ public static final String EXTRA_NOTIFICATION_TYPE = "extra.de.danoeh.antennapod.service.notificationType";
+
+ /**
+ * If the PlaybackService receives this action, it will stop playback and
+ * try to shutdown.
+ */
+ public static final String ACTION_SHUTDOWN_PLAYBACK_SERVICE = "action.de.danoeh.antennapod.service.actionShutdownPlaybackService";
+
+ /**
+ * If the PlaybackService receives this action, it will end playback of the
+ * current episode and load the next episode if there is one available.
+ */
+ public static final String ACTION_SKIP_CURRENT_EPISODE = "action.de.danoeh.antennapod.service.skipCurrentEpisode";
+
+ /**
+ * Used in NOTIFICATION_TYPE_RELOAD.
+ */
+ public static final int EXTRA_CODE_AUDIO = 1;
+ public static final int EXTRA_CODE_VIDEO = 2;
+
+ public static final int NOTIFICATION_TYPE_ERROR = 0;
+ public static final int NOTIFICATION_TYPE_INFO = 1;
+ public static final int NOTIFICATION_TYPE_BUFFER_UPDATE = 2;
+
+ /**
+ * Receivers of this intent should update their information about the curently playing media
+ */
+ public static final int NOTIFICATION_TYPE_RELOAD = 3;
+ /**
+ * The state of the sleeptimer changed.
+ */
+ public static final int NOTIFICATION_TYPE_SLEEPTIMER_UPDATE = 4;
+ public static final int NOTIFICATION_TYPE_BUFFER_START = 5;
+ public static final int NOTIFICATION_TYPE_BUFFER_END = 6;
+ /**
+ * No more episodes are going to be played.
+ */
+ public static final int NOTIFICATION_TYPE_PLAYBACK_END = 7;
+
+ /**
+ * Returned by getPositionSafe() or getDurationSafe() if the playbackService
+ * is in an invalid state.
+ */
+ public static final int INVALID_TIME = -1;
+
+ /**
+ * Is true if service is running.
+ */
+ public static boolean isRunning = false;
+
+ private static final int NOTIFICATION_ID = 1;
+
+ private AudioManager audioManager;
+ private ComponentName mediaButtonReceiver;
+
+ private MediaPlayer player;
+ private RemoteControlClient remoteControlClient;
+
+ private Playable media;
+
+ /**
+ * True if media should be streamed (Extracted from Intent Extra) .
+ */
+ private boolean shouldStream;
+
+ private boolean startWhenPrepared;
+ private PlayerStatus status;
+
+ private PositionSaver positionSaver;
+ private ScheduledFuture positionSaverFuture;
+
+ private WidgetUpdateWorker widgetUpdater;
+ private ScheduledFuture widgetUpdaterFuture;
+
+ private SleepTimer sleepTimer;
+ private Future sleepTimerFuture;
+
+ private static final int SCHED_EX_POOL_SIZE = 3;
+ private ScheduledThreadPoolExecutor schedExecutor;
+ private ExecutorService dbLoaderExecutor;
+
+ private volatile PlayerStatus statusBeforeSeek;
+
+ private static boolean playingVideo;
+
+ /**
+ * True if mediaplayer was paused because it lost audio focus temporarily
+ */
+ private boolean pausedBecauseOfTransientAudiofocusLoss;
+
+ private Thread chapterLoader;
+
+ private final IBinder mBinder = new LocalBinder();
+
+ private volatile List<FeedItem> queue;
+
+ public class LocalBinder extends Binder {
+ public PlaybackService getService() {
+ return PlaybackService.this;
+ }
+ }
+
+ @Override
+ public boolean onUnbind(Intent intent) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Received onUnbind event");
+ return super.onUnbind(intent);
+ }
+
+ /**
+ * Returns an intent which starts an audio- or videoplayer, depending on the
+ * type of media that is being played. If the playbackservice is not
+ * running, the type of the last played media will be looked up.
+ */
+ public static Intent getPlayerActivityIntent(Context context) {
+ if (isRunning) {
+ if (playingVideo) {
+ return new Intent(context, VideoplayerActivity.class);
+ } else {
+ return new Intent(context, AudioplayerActivity.class);
+ }
+ } else {
+ if (PlaybackPreferences.getCurrentEpisodeIsVideo()) {
+ return new Intent(context, VideoplayerActivity.class);
+ } else {
+ return new Intent(context, AudioplayerActivity.class);
+ }
+ }
+ }
+
+ /**
+ * Same as getPlayerActivityIntent(context), but here the type of activity
+ * depends on the FeedMedia that is provided as an argument.
+ */
+ public static Intent getPlayerActivityIntent(Context context, Playable media) {
+ MediaType mt = media.getMediaType();
+ if (mt == MediaType.VIDEO) {
+ return new Intent(context, VideoplayerActivity.class);
+ } else {
+ return new Intent(context, AudioplayerActivity.class);
+ }
+ }
+
+ @SuppressLint("NewApi")
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Service created.");
+ isRunning = true;
+ pausedBecauseOfTransientAudiofocusLoss = false;
+ status = PlayerStatus.STOPPED;
+ audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
+ 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;
+ }
+ }, new RejectedExecutionHandler() {
+
+ @Override
+ public void rejectedExecution(Runnable r,
+ ThreadPoolExecutor executor) {
+ Log.w(TAG, "SchedEx rejected submission of new task");
+ }
+ }
+ );
+ dbLoaderExecutor = Executors.newSingleThreadExecutor();
+ player = createMediaPlayer();
+
+ mediaButtonReceiver = new ComponentName(getPackageName(),
+ MediaButtonReceiver.class.getName());
+ audioManager.registerMediaButtonEventReceiver(mediaButtonReceiver);
+ if (android.os.Build.VERSION.SDK_INT >= 14) {
+ audioManager
+ .registerRemoteControlClient(setupRemoteControlClient());
+ }
+ registerReceiver(headsetDisconnected, new IntentFilter(
+ Intent.ACTION_HEADSET_PLUG));
+ registerReceiver(shutdownReceiver, new IntentFilter(
+ ACTION_SHUTDOWN_PLAYBACK_SERVICE));
+ registerReceiver(audioBecomingNoisy, new IntentFilter(
+ AudioManager.ACTION_AUDIO_BECOMING_NOISY));
+ registerReceiver(skipCurrentEpisodeReceiver, new IntentFilter(
+ ACTION_SKIP_CURRENT_EPISODE));
+ EventDistributor.getInstance().register(eventDistributorListener);
+ loadQueue();
+ }
+
+ private MediaPlayer createMediaPlayer() {
+ return createMediaPlayer(new MediaPlayer());
+ }
+
+ private MediaPlayer createMediaPlayer(MediaPlayer mp) {
+ if (mp != null) {
+ mp.setOnPreparedListener(preparedListener);
+ mp.setOnCompletionListener(completionListener);
+ mp.setOnSeekCompleteListener(onSeekCompleteListener);
+ mp.setOnErrorListener(onErrorListener);
+ mp.setOnBufferingUpdateListener(onBufferingUpdateListener);
+ mp.setOnInfoListener(onInfoListener);
+ }
+ return mp;
+ }
+
+ @SuppressLint("NewApi")
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Service is about to be destroyed");
+ isRunning = false;
+ if (chapterLoader != null) {
+ chapterLoader.interrupt();
+ }
+ disableSleepTimer();
+ unregisterReceiver(headsetDisconnected);
+ unregisterReceiver(shutdownReceiver);
+ unregisterReceiver(audioBecomingNoisy);
+ unregisterReceiver(skipCurrentEpisodeReceiver);
+ EventDistributor.getInstance().unregister(eventDistributorListener);
+ if (android.os.Build.VERSION.SDK_INT >= 14) {
+ audioManager.unregisterRemoteControlClient(remoteControlClient);
+ }
+ audioManager.unregisterMediaButtonEventReceiver(mediaButtonReceiver);
+ audioManager.abandonAudioFocus(audioFocusChangeListener);
+ player.release();
+ stopWidgetUpdater();
+ updateWidget();
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Received onBind event");
+ return mBinder;
+ }
+
+ private final EventDistributor.EventListener eventDistributorListener = new EventDistributor.EventListener() {
+ @Override
+ public void update(EventDistributor eventDistributor, Integer arg) {
+ if ((EventDistributor.QUEUE_UPDATE & arg) != 0) {
+ loadQueue();
+ }
+ }
+ };
+
+ private final OnAudioFocusChangeListener audioFocusChangeListener = new OnAudioFocusChangeListener() {
+
+ @Override
+ public void onAudioFocusChange(int focusChange) {
+ switch (focusChange) {
+ case AudioManager.AUDIOFOCUS_LOSS:
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Lost audio focus");
+ pause(true, false);
+ stopSelf();
+ break;
+ case AudioManager.AUDIOFOCUS_GAIN:
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Gained audio focus");
+ if (pausedBecauseOfTransientAudiofocusLoss) {
+ audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC,
+ AudioManager.ADJUST_RAISE, 0);
+ play();
+ }
+ break;
+ case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
+ if (status == PlayerStatus.PLAYING) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Lost audio focus temporarily. Ducking...");
+ audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC,
+ AudioManager.ADJUST_LOWER, 0);
+ pausedBecauseOfTransientAudiofocusLoss = true;
+ }
+ break;
+ case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
+ if (status == PlayerStatus.PLAYING) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Lost audio focus temporarily. Pausing...");
+ pause(false, false);
+ pausedBecauseOfTransientAudiofocusLoss = true;
+ }
+ }
+ }
+ };
+
+ /**
+ * 1. Check type of intent
+ * 1.1 Keycode -> handle keycode -> done
+ * 1.2 Playable -> Step 2
+ * 2. Handle playable
+ * 2.1 Check current status
+ * 2.1.1 Not playing -> play new playable
+ * 2.1.2 Playing, new playable is the same -> play if playback is currently paused
+ * 2.1.3 Playing, new playable different -> Stop playback of old media
+ *
+ * @param intent
+ * @param flags
+ * @param startId
+ * @return
+ */
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ super.onStartCommand(intent, flags, startId);
+
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "OnStartCommand called");
+ final int keycode = intent.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1);
+ final Playable playable = intent.getParcelableExtra(EXTRA_PLAYABLE);
+ if (keycode == -1 && playable == null) {
+ Log.e(TAG, "PlaybackService was started with no arguments");
+ stopSelf();
+ }
+
+ if (keycode != -1) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Received media button event");
+ handleKeycode(keycode);
+ } else {
+ boolean playbackType = intent.getBooleanExtra(EXTRA_SHOULD_STREAM,
+ true);
+ if (media == null) {
+ media = playable;
+ shouldStream = playbackType;
+ startWhenPrepared = intent.getBooleanExtra(
+ EXTRA_START_WHEN_PREPARED, false);
+ initMediaplayer(intent.getBooleanExtra(EXTRA_PREPARE_IMMEDIATELY, false));
+ sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0);
+ }
+ if (media != null) {
+ if (!playable.getIdentifier().equals(media.getIdentifier())) {
+ // different media or different playback type
+ pause(true, false);
+ player.reset();
+ media = playable;
+ shouldStream = playbackType;
+ startWhenPrepared = intent.getBooleanExtra(EXTRA_START_WHEN_PREPARED, false);
+ initMediaplayer(intent.getBooleanExtra(EXTRA_PREPARE_IMMEDIATELY, false));
+ sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0);
+ } else {
+ // same media and same playback type
+ if (status == PlayerStatus.PAUSED) {
+ play();
+ }
+ }
+ }
+ }
+
+ return Service.START_NOT_STICKY;
+ }
+
+ /**
+ * Handles media button events
+ */
+ private void handleKeycode(int keycode) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Handling keycode: " + keycode);
+ switch (keycode) {
+ case KeyEvent.KEYCODE_HEADSETHOOK:
+ case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
+ if (status == PlayerStatus.PLAYING) {
+ pause(true, true);
+ } else if (status == PlayerStatus.PAUSED) {
+ play();
+ } else if (status == PlayerStatus.PREPARING) {
+ setStartWhenPrepared(!startWhenPrepared);
+ } else if (status == PlayerStatus.INITIALIZED) {
+ startWhenPrepared = true;
+ prepare();
+ }
+ break;
+ case KeyEvent.KEYCODE_MEDIA_PLAY:
+ if (status == PlayerStatus.PAUSED) {
+ play();
+ } else if (status == PlayerStatus.INITIALIZED) {
+ startWhenPrepared = true;
+ prepare();
+ }
+ break;
+ case KeyEvent.KEYCODE_MEDIA_PAUSE:
+ if (status == PlayerStatus.PLAYING) {
+ pause(true, true);
+ }
+ break;
+ }
+ }
+
+ /**
+ * Called by a mediaplayer Activity as soon as it has prepared its
+ * mediaplayer.
+ */
+ public void setVideoSurface(SurfaceHolder sh) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Setting display");
+ player.setDisplay(null);
+ player.setDisplay(sh);
+ if (status == PlayerStatus.STOPPED
+ || status == PlayerStatus.AWAITING_VIDEO_SURFACE) {
+ try {
+ InitTask initTask = new InitTask() {
+
+ @Override
+ protected void onPostExecute(Playable result) {
+ if (status == PlayerStatus.INITIALIZING) {
+ if (result != null) {
+ try {
+ if (shouldStream) {
+ player.setDataSource(media
+ .getStreamUrl());
+ setStatus(PlayerStatus.PREPARING);
+ player.prepareAsync();
+ } else {
+ player.setDataSource(media
+ .getLocalMediaUrl());
+ setStatus(PlayerStatus.PREPARING);
+ player.prepareAsync();
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ } else {
+ setStatus(PlayerStatus.ERROR);
+ sendBroadcast(new Intent(
+ ACTION_SHUTDOWN_PLAYBACK_SERVICE));
+ }
+ }
+ }
+
+ @Override
+ protected void onPreExecute() {
+ setStatus(PlayerStatus.INITIALIZING);
+ }
+
+ };
+ initTask.executeAsync(media);
+ } catch (IllegalArgumentException e) {
+ e.printStackTrace();
+ } catch (SecurityException e) {
+ e.printStackTrace();
+ } catch (IllegalStateException e) {
+ e.printStackTrace();
+ }
+ }
+
+ }
+
+ /**
+ * Called when the surface holder of the mediaplayer has to be changed.
+ */
+ private void resetVideoSurface() {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Resetting video surface");
+ cancelPositionSaver();
+ player.setDisplay(null);
+ player.reset();
+ player.release();
+ player = createMediaPlayer();
+ status = PlayerStatus.STOPPED;
+ }
+
+ public void notifyVideoSurfaceAbandoned() {
+ resetVideoSurface();
+ if (media != null) {
+ initMediaplayer(true);
+ }
+ }
+
+ /**
+ * Called after service has extracted the media it is supposed to play.
+ *
+ * @param prepareImmediately True if service should prepare playback after it has been initialized
+ */
+ private void initMediaplayer(final boolean prepareImmediately) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Setting up media player");
+ try {
+ MediaType mediaType = media.getMediaType();
+ if (mediaType == MediaType.AUDIO) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Mime type is audio");
+
+ InitTask initTask = new InitTask() {
+
+ @Override
+ protected void onPostExecute(Playable result) {
+ // check if state of service has changed. If it has
+ // changed, assume that loaded metadata is not needed
+ // anymore.
+ if (status == PlayerStatus.INITIALIZING) {
+ if (result != null) {
+ playingVideo = false;
+ try {
+ if (shouldStream) {
+ player.setDataSource(media
+ .getStreamUrl());
+ } else if (media.localFileAvailable()) {
+ player.setDataSource(media
+ .getLocalMediaUrl());
+ }
+
+ if (prepareImmediately) {
+ setStatus(PlayerStatus.PREPARING);
+ player.prepareAsync();
+ } else {
+ setStatus(PlayerStatus.INITIALIZED);
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ media = null;
+ setStatus(PlayerStatus.ERROR);
+ sendBroadcast(new Intent(
+ ACTION_SHUTDOWN_PLAYBACK_SERVICE));
+ }
+ } else {
+ Log.e(TAG, "InitTask could not load metadata");
+ media = null;
+ setStatus(PlayerStatus.ERROR);
+ sendBroadcast(new Intent(
+ ACTION_SHUTDOWN_PLAYBACK_SERVICE));
+ }
+ } else {
+ if (AppConfig.DEBUG)
+ Log.d(TAG,
+ "Status of player has changed during initialization. Stopping init process.");
+ }
+ }
+
+ @Override
+ protected void onPreExecute() {
+ setStatus(PlayerStatus.INITIALIZING);
+ }
+
+ };
+ initTask.executeAsync(media);
+ } else if (mediaType == MediaType.VIDEO) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Mime type is video");
+ playingVideo = true;
+ setStatus(PlayerStatus.AWAITING_VIDEO_SURFACE);
+ player.setScreenOnWhilePlaying(true);
+ }
+
+ } catch (IllegalArgumentException e) {
+ e.printStackTrace();
+ } catch (SecurityException e) {
+ e.printStackTrace();
+ } catch (IllegalStateException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void setupPositionSaver() {
+ if (positionSaverFuture == null
+ || (positionSaverFuture.isCancelled() || positionSaverFuture
+ .isDone())) {
+
+ positionSaver = new PositionSaver();
+ positionSaverFuture = schedExecutor.scheduleAtFixedRate(
+ positionSaver, PositionSaver.WAITING_INTERVALL,
+ PositionSaver.WAITING_INTERVALL, TimeUnit.MILLISECONDS);
+ }
+ }
+
+ private void cancelPositionSaver() {
+ if (positionSaverFuture != null) {
+ boolean result = positionSaverFuture.cancel(true);
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "PositionSaver cancelled. Result: " + result);
+ }
+ }
+
+ private MediaPlayer.OnPreparedListener preparedListener = new MediaPlayer.OnPreparedListener() {
+ @Override
+ public void onPrepared(MediaPlayer mp) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Resource prepared");
+ mp.seekTo(media.getPosition());
+ if (media.getDuration() == 0) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Setting duration of media");
+ media.setDuration(mp.getDuration());
+ }
+ setStatus(PlayerStatus.PREPARED);
+ if (chapterLoader != null) {
+ chapterLoader.interrupt();
+ }
+ chapterLoader = new Thread() {
+ @Override
+ public void run() {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Chapter loader started");
+ if (media != null && media.getChapters() == null) {
+ media.loadChapterMarks();
+ if (!isInterrupted() && media.getChapters() != null) {
+ sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD,
+ 0);
+ }
+ }
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Chapter loader stopped");
+ }
+ };
+ chapterLoader.start();
+
+ if (startWhenPrepared) {
+ play();
+ }
+ }
+ };
+
+ private MediaPlayer.OnSeekCompleteListener onSeekCompleteListener = new MediaPlayer.OnSeekCompleteListener() {
+
+ @Override
+ public void onSeekComplete(MediaPlayer mp) {
+ if (status == PlayerStatus.SEEKING) {
+ setStatus(statusBeforeSeek);
+ }
+
+ }
+ };
+
+ private MediaPlayer.OnInfoListener onInfoListener = new MediaPlayer.OnInfoListener() {
+
+ @Override
+ public boolean onInfo(MediaPlayer mp, int what, int extra) {
+ switch (what) {
+ case MediaPlayer.MEDIA_INFO_BUFFERING_START:
+ sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_START, 0);
+ return true;
+ case MediaPlayer.MEDIA_INFO_BUFFERING_END:
+ sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_END, 0);
+ return true;
+ default:
+ return false;
+ }
+ }
+ };
+
+ private MediaPlayer.OnErrorListener onErrorListener = new MediaPlayer.OnErrorListener() {
+ private static final String TAG = "PlaybackService.onErrorListener";
+
+ @Override
+ public boolean onError(MediaPlayer mp, int what, int extra) {
+ Log.w(TAG, "An error has occured: " + what);
+ if (mp.isPlaying()) {
+ pause(true, true);
+ }
+ sendNotificationBroadcast(NOTIFICATION_TYPE_ERROR, what);
+ setCurrentlyPlayingMedia(PlaybackPreferences.NO_MEDIA_PLAYING);
+ stopSelf();
+ return true;
+ }
+ };
+
+ private MediaPlayer.OnCompletionListener completionListener = new MediaPlayer.OnCompletionListener() {
+
+ @Override
+ public void onCompletion(MediaPlayer mp) {
+ endPlayback(true);
+ }
+ };
+
+ private MediaPlayer.OnBufferingUpdateListener onBufferingUpdateListener = new MediaPlayer.OnBufferingUpdateListener() {
+
+ @Override
+ public void onBufferingUpdate(MediaPlayer mp, int percent) {
+ sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_UPDATE, percent);
+
+ }
+ };
+
+ private void endPlayback(boolean playNextEpisode) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Playback ended");
+ audioManager.abandonAudioFocus(audioFocusChangeListener);
+
+ // Save state
+ cancelPositionSaver();
+
+ boolean isInQueue = false;
+ FeedItem nextItem = null;
+
+ if (media instanceof FeedMedia) {
+ FeedItem item = ((FeedMedia) media).getItem();
+ ((FeedMedia) media).setPlaybackCompletionDate(new Date());
+ DBWriter.markItemRead(PlaybackService.this, item, true, true);
+ nextItem = DBTasks.getQueueSuccessorOfItem(this, item.getId(), queue);
+ isInQueue = media instanceof FeedMedia
+ && QueueAccess.ItemListAccess(queue).contains(((FeedMedia) media).getItem().getId());
+ if (isInQueue) {
+ DBWriter.removeQueueItem(PlaybackService.this, item.getId(), true);
+ }
+ DBWriter.addItemToPlaybackHistory(PlaybackService.this, item);
+ DBWriter.setFeedMedia(PlaybackService.this, (FeedMedia) media);
+ long autoDeleteMediaId = ((FeedComponent) media).getId();
+ if (shouldStream) {
+ autoDeleteMediaId = -1;
+ }
+ }
+
+ // Load next episode if previous episode was in the queue and if there
+ // is an episode in the queue left.
+ // Start playback immediately if continuous playback is enabled
+ boolean loadNextItem = isInQueue && nextItem != null;
+ playNextEpisode = playNextEpisode && loadNextItem
+ && UserPreferences.isFollowQueue();
+ if (loadNextItem) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Loading next item in queue");
+ media = nextItem.getMedia();
+ }
+ final boolean prepareImmediately;
+ if (playNextEpisode) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Playback of next episode will start immediately.");
+ prepareImmediately = startWhenPrepared = true;
+ } else {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "No more episodes available to play");
+ media = null;
+ prepareImmediately = startWhenPrepared = false;
+ stopForeground(true);
+ stopWidgetUpdater();
+ }
+
+ int notificationCode = 0;
+ if (media != null) {
+ shouldStream = !media.localFileAvailable();
+ if (media.getMediaType() == MediaType.AUDIO) {
+ notificationCode = EXTRA_CODE_AUDIO;
+ playingVideo = false;
+ } else if (media.getMediaType() == MediaType.VIDEO) {
+ notificationCode = EXTRA_CODE_VIDEO;
+ }
+ }
+ writePlaybackPreferences();
+ if (media != null) {
+ resetVideoSurface();
+ refreshRemoteControlClientState();
+ initMediaplayer(prepareImmediately);
+
+ sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD,
+ notificationCode);
+ } else {
+ sendNotificationBroadcast(NOTIFICATION_TYPE_PLAYBACK_END, 0);
+ stopSelf();
+ }
+ }
+
+ public void setSleepTimer(long waitingTime) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Setting sleep timer to " + Long.toString(waitingTime)
+ + " milliseconds");
+ if (sleepTimerFuture != null) {
+ sleepTimerFuture.cancel(true);
+ }
+ sleepTimer = new SleepTimer(waitingTime);
+ sleepTimerFuture = schedExecutor.submit(sleepTimer);
+ sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0);
+ }
+
+ public void disableSleepTimer() {
+ if (sleepTimerFuture != null) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Disabling sleep timer");
+ sleepTimerFuture.cancel(true);
+ sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0);
+ }
+ }
+
+ /**
+ * Saves the current position and pauses playback. Note that, if audiofocus
+ * is abandoned, the lockscreen controls will also disapear.
+ *
+ * @param abandonFocus is true if the service should release audio focus
+ * @param reinit is true if service should reinit after pausing if the media
+ * file is being streamed
+ */
+ public void pause(boolean abandonFocus, boolean reinit) {
+ if (player.isPlaying()) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Pausing playback.");
+ player.pause();
+ if (abandonFocus) {
+ audioManager.abandonAudioFocus(audioFocusChangeListener);
+ pausedBecauseOfTransientAudiofocusLoss = false;
+ disableSleepTimer();
+ }
+ cancelPositionSaver();
+ saveCurrentPosition();
+ setStatus(PlayerStatus.PAUSED);
+ stopWidgetUpdater();
+ stopForeground(true);
+ if (shouldStream && reinit) {
+ reinit();
+ }
+ }
+ }
+
+ /**
+ * Pauses playback and destroys service. Recommended for video playback.
+ */
+ public void stop() {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Stopping playback");
+ if (status == PlayerStatus.PREPARED || status == PlayerStatus.PAUSED
+ || status == PlayerStatus.STOPPED
+ || status == PlayerStatus.PLAYING) {
+ player.stop();
+ }
+ setCurrentlyPlayingMedia(PlaybackPreferences.NO_MEDIA_PLAYING);
+ stopSelf();
+ }
+
+ /**
+ * Prepared media player for playback if the service is in the INITALIZED
+ * state.
+ */
+ public void prepare() {
+ if (status == PlayerStatus.INITIALIZED) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Preparing media player");
+ setStatus(PlayerStatus.PREPARING);
+ player.prepareAsync();
+ }
+ }
+
+ /**
+ * Resets the media player and moves into INITIALIZED state.
+ */
+ public void reinit() {
+ player.reset();
+ player = createMediaPlayer(player);
+ initMediaplayer(false);
+ }
+
+ @SuppressLint("NewApi")
+ public void play() {
+ if (status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED
+ || status == PlayerStatus.STOPPED) {
+ int focusGained = audioManager.requestAudioFocus(
+ audioFocusChangeListener, AudioManager.STREAM_MUSIC,
+ AudioManager.AUDIOFOCUS_GAIN);
+
+ if (focusGained == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Audiofocus successfully requested");
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Resuming/Starting playback");
+ writePlaybackPreferences();
+
+ player.start();
+ if (status != PlayerStatus.PAUSED) {
+ player.seekTo((int) media.getPosition());
+ }
+ setStatus(PlayerStatus.PLAYING);
+ setupPositionSaver();
+ setupWidgetUpdater();
+ setupNotification();
+ pausedBecauseOfTransientAudiofocusLoss = false;
+ if (android.os.Build.VERSION.SDK_INT >= 14) {
+ audioManager
+ .registerRemoteControlClient(remoteControlClient);
+ }
+ audioManager
+ .registerMediaButtonEventReceiver(mediaButtonReceiver);
+ media.onPlaybackStart();
+ } else {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Failed to request Audiofocus");
+ }
+ }
+ }
+
+ private void writePlaybackPreferences() {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Writing playback preferences");
+
+ SharedPreferences.Editor editor = PreferenceManager
+ .getDefaultSharedPreferences(getApplicationContext()).edit();
+ if (media != null) {
+ editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA,
+ media.getPlayableType());
+ editor.putBoolean(
+ PlaybackPreferences.PREF_CURRENT_EPISODE_IS_STREAM,
+ shouldStream);
+ editor.putBoolean(
+ PlaybackPreferences.PREF_CURRENT_EPISODE_IS_VIDEO,
+ playingVideo);
+ if (media instanceof FeedMedia) {
+ FeedMedia fMedia = (FeedMedia) media;
+ editor.putLong(
+ PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID,
+ fMedia.getItem().getFeed().getId());
+ editor.putLong(
+ PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID,
+ fMedia.getId());
+ } else {
+ editor.putLong(
+ PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID,
+ PlaybackPreferences.NO_MEDIA_PLAYING);
+ editor.putLong(
+ PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID,
+ PlaybackPreferences.NO_MEDIA_PLAYING);
+ }
+ media.writeToPreferences(editor);
+ } else {
+ editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA,
+ PlaybackPreferences.NO_MEDIA_PLAYING);
+ editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID,
+ PlaybackPreferences.NO_MEDIA_PLAYING);
+ editor.putLong(
+ PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID,
+ PlaybackPreferences.NO_MEDIA_PLAYING);
+ }
+
+ editor.commit();
+ }
+
+ private void setStatus(PlayerStatus newStatus) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Setting status to " + newStatus);
+ status = newStatus;
+ sendBroadcast(new Intent(ACTION_PLAYER_STATUS_CHANGED));
+ updateWidget();
+ refreshRemoteControlClientState();
+ bluetoothNotifyChange();
+ }
+
+ /**
+ * Send ACTION_PLAYER_STATUS_CHANGED without changing the status attribute.
+ */
+ private void postStatusUpdateIntent() {
+ setStatus(status);
+ }
+
+ private void sendNotificationBroadcast(int type, int code) {
+ Intent intent = new Intent(ACTION_PLAYER_NOTIFICATION);
+ intent.putExtra(EXTRA_NOTIFICATION_TYPE, type);
+ intent.putExtra(EXTRA_NOTIFICATION_CODE, code);
+ sendBroadcast(intent);
+ }
+
+ /**
+ * Used by setupNotification to load notification data in another thread.
+ */
+ private AsyncTask<Void, Void, Void> notificationSetupTask;
+
+ /**
+ * Prepares notification and starts the service in the foreground.
+ */
+ @SuppressLint("NewApi")
+ private void setupNotification() {
+ final PendingIntent pIntent = PendingIntent.getActivity(this, 0,
+ PlaybackService.getPlayerActivityIntent(this),
+ PendingIntent.FLAG_UPDATE_CURRENT);
+
+ if (notificationSetupTask != null) {
+ notificationSetupTask.cancel(true);
+ }
+ notificationSetupTask = new AsyncTask<Void, Void, Void>() {
+ Bitmap icon = null;
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Starting background work");
+ if (android.os.Build.VERSION.SDK_INT >= 11) {
+ if (media != null && media != null) {
+ int iconSize = getResources().getDimensionPixelSize(
+ android.R.dimen.notification_large_icon_width);
+ icon = BitmapDecoder
+ .decodeBitmapFromWorkerTaskResource(iconSize,
+ media);
+ }
+
+ }
+ if (icon == null) {
+ icon = BitmapFactory.decodeResource(getResources(),
+ R.drawable.ic_stat_antenna);
+ }
+
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void result) {
+ super.onPostExecute(result);
+ if (!isCancelled() && status == PlayerStatus.PLAYING
+ && media != null) {
+ String contentText = media.getFeedTitle();
+ String contentTitle = media.getEpisodeTitle();
+ Notification notification = null;
+ if (android.os.Build.VERSION.SDK_INT >= 16) {
+ Intent pauseButtonIntent = new Intent(
+ PlaybackService.this, PlaybackService.class);
+ pauseButtonIntent.putExtra(
+ MediaButtonReceiver.EXTRA_KEYCODE,
+ KeyEvent.KEYCODE_MEDIA_PAUSE);
+ PendingIntent pauseButtonPendingIntent = PendingIntent
+ .getService(PlaybackService.this, 0,
+ pauseButtonIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT);
+ Notification.Builder notificationBuilder = new Notification.Builder(
+ PlaybackService.this)
+ .setContentTitle(contentTitle)
+ .setContentText(contentText)
+ .setOngoing(true)
+ .setContentIntent(pIntent)
+ .setLargeIcon(icon)
+ .setSmallIcon(R.drawable.ic_stat_antenna)
+ .addAction(android.R.drawable.ic_media_pause,
+ getString(R.string.pause_label),
+ pauseButtonPendingIntent);
+ notification = notificationBuilder.build();
+ } else {
+ NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(
+ PlaybackService.this)
+ .setContentTitle(contentTitle)
+ .setContentText(contentText).setOngoing(true)
+ .setContentIntent(pIntent).setLargeIcon(icon)
+ .setSmallIcon(R.drawable.ic_stat_antenna);
+ notification = notificationBuilder.getNotification();
+ }
+ startForeground(NOTIFICATION_ID, notification);
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Notification set up");
+ }
+ }
+
+ };
+ if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) {
+ notificationSetupTask
+ .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ } else {
+ notificationSetupTask.execute();
+ }
+
+ }
+
+ /**
+ * Seek a specific position from the current position
+ *
+ * @param delta offset from current position (positive or negative)
+ */
+ public void seekDelta(int delta) {
+ int position = getCurrentPositionSafe();
+ if (position != INVALID_TIME) {
+ seek(player.getCurrentPosition() + delta);
+ }
+ }
+
+ public void seek(int i) {
+ saveCurrentPosition();
+ if (status == PlayerStatus.INITIALIZED
+ || status == PlayerStatus.INITIALIZING
+ || status == PlayerStatus.PREPARING) {
+ media.setPosition(i);
+ setStartWhenPrepared(true);
+ prepare();
+ } else {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Seeking position " + i);
+ if (shouldStream) {
+ if (status != PlayerStatus.SEEKING) {
+ statusBeforeSeek = status;
+ }
+ setStatus(PlayerStatus.SEEKING);
+ }
+ player.seekTo(i);
+ }
+ }
+
+ public void seekToChapter(Chapter chapter) {
+ seek((int) chapter.getStart());
+ }
+
+ /**
+ * Saves the current position of the media file to the DB
+ */
+ private synchronized void saveCurrentPosition() {
+ int position = getCurrentPositionSafe();
+ if (position != INVALID_TIME) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Saving current position to " + position);
+ media.saveCurrentPosition(PreferenceManager
+ .getDefaultSharedPreferences(getApplicationContext()),
+ position);
+ }
+ }
+
+ private void stopWidgetUpdater() {
+ if (widgetUpdaterFuture != null) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Stopping widgetUpdateWorker");
+ widgetUpdaterFuture.cancel(true);
+ }
+ sendBroadcast(new Intent(PlayerWidget.STOP_WIDGET_UPDATE));
+ }
+
+ @SuppressLint("NewApi")
+ private void setupWidgetUpdater() {
+ if (widgetUpdaterFuture == null
+ || (widgetUpdaterFuture.isCancelled() || widgetUpdaterFuture
+ .isDone())) {
+ widgetUpdater = new WidgetUpdateWorker();
+ widgetUpdaterFuture = schedExecutor.scheduleAtFixedRate(
+ widgetUpdater, WidgetUpdateWorker.NOTIFICATION_INTERVALL,
+ WidgetUpdateWorker.NOTIFICATION_INTERVALL,
+ TimeUnit.MILLISECONDS);
+ }
+ }
+
+ private void updateWidget() {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Sending widget update request");
+ PlaybackService.this.sendBroadcast(new Intent(
+ PlayerWidget.FORCE_WIDGET_UPDATE));
+ }
+
+ public boolean sleepTimerActive() {
+ return sleepTimer != null && sleepTimer.isWaiting();
+ }
+
+ public long getSleepTimerTimeLeft() {
+ if (sleepTimerActive()) {
+ return sleepTimer.getWaitingTime();
+ } else {
+ return 0;
+ }
+ }
+
+ @SuppressLint("NewApi")
+ private RemoteControlClient setupRemoteControlClient() {
+ Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
+ mediaButtonIntent.setComponent(mediaButtonReceiver);
+ PendingIntent mediaPendingIntent = PendingIntent.getBroadcast(
+ getApplicationContext(), 0, mediaButtonIntent, 0);
+ remoteControlClient = new RemoteControlClient(mediaPendingIntent);
+ int controlFlags;
+ if (android.os.Build.VERSION.SDK_INT < 16) {
+ controlFlags = RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE
+ | RemoteControlClient.FLAG_KEY_MEDIA_NEXT;
+ } else {
+ controlFlags = RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE;
+ }
+ remoteControlClient.setTransportControlFlags(controlFlags);
+ return remoteControlClient;
+ }
+
+ /**
+ * Refresh player status and metadata.
+ */
+ @SuppressLint("NewApi")
+ private void refreshRemoteControlClientState() {
+ if (android.os.Build.VERSION.SDK_INT >= 14) {
+ if (remoteControlClient != null) {
+ switch (status) {
+ case PLAYING:
+ remoteControlClient
+ .setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING);
+ break;
+ case PAUSED:
+ case INITIALIZED:
+ remoteControlClient
+ .setPlaybackState(RemoteControlClient.PLAYSTATE_PAUSED);
+ break;
+ case STOPPED:
+ remoteControlClient
+ .setPlaybackState(RemoteControlClient.PLAYSTATE_STOPPED);
+ break;
+ case ERROR:
+ remoteControlClient
+ .setPlaybackState(RemoteControlClient.PLAYSTATE_ERROR);
+ break;
+ default:
+ remoteControlClient
+ .setPlaybackState(RemoteControlClient.PLAYSTATE_BUFFERING);
+ }
+ if (media != null) {
+ MetadataEditor editor = remoteControlClient
+ .editMetadata(false);
+ editor.putString(MediaMetadataRetriever.METADATA_KEY_TITLE,
+ media.getEpisodeTitle());
+
+ editor.putString(MediaMetadataRetriever.METADATA_KEY_ALBUM,
+ media.getFeedTitle());
+
+ editor.apply();
+ }
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "RemoteControlClient state was refreshed");
+ }
+ }
+ }
+
+ private void bluetoothNotifyChange() {
+ boolean isPlaying = false;
+
+ if (status == PlayerStatus.PLAYING) {
+ isPlaying = true;
+ }
+
+ Intent i = new Intent(AVRCP_ACTION_PLAYER_STATUS_CHANGED);
+ i.putExtra("id", 1);
+ i.putExtra("artist", "");
+ i.putExtra("album", media.getFeedTitle());
+ i.putExtra("track", media.getEpisodeTitle());
+ i.putExtra("playing", isPlaying);
+ if (queue != null) {
+ i.putExtra("ListSize", queue.size());
+ }
+ i.putExtra("duration", media.getDuration());
+ i.putExtra("position", media.getPosition());
+ sendBroadcast(i);
+ }
+
+ /**
+ * Pauses playback when the headset is disconnected and the preference is
+ * set
+ */
+ private BroadcastReceiver headsetDisconnected = new BroadcastReceiver() {
+ private static final String TAG = "headsetDisconnected";
+ private static final int UNPLUGGED = 0;
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction().equals(Intent.ACTION_HEADSET_PLUG)) {
+ int state = intent.getIntExtra("state", -1);
+ if (state != -1) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Headset plug event. State is " + state);
+ if (state == UNPLUGGED && status == PlayerStatus.PLAYING) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Headset was unplugged during playback.");
+ pauseIfPauseOnDisconnect();
+ }
+ } else {
+ Log.e(TAG, "Received invalid ACTION_HEADSET_PLUG intent");
+ }
+ }
+ }
+ };
+
+ private BroadcastReceiver audioBecomingNoisy = new BroadcastReceiver() {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ // sound is about to change, eg. bluetooth -> speaker
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Pausing playback because audio is becoming noisy");
+ pauseIfPauseOnDisconnect();
+ }
+ // android.media.AUDIO_BECOMING_NOISY
+ };
+
+ /**
+ * Pauses playback if PREF_PAUSE_ON_HEADSET_DISCONNECT was set to true.
+ */
+ private void pauseIfPauseOnDisconnect() {
+ if (UserPreferences.isPauseOnHeadsetDisconnect()
+ && status == PlayerStatus.PLAYING) {
+ pause(true, true);
+ }
+ }
+
+ private BroadcastReceiver shutdownReceiver = new BroadcastReceiver() {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction().equals(ACTION_SHUTDOWN_PLAYBACK_SERVICE)) {
+ schedExecutor.shutdownNow();
+ stop();
+ media = null;
+ }
+ }
+
+ };
+
+ private BroadcastReceiver skipCurrentEpisodeReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction().equals(ACTION_SKIP_CURRENT_EPISODE)) {
+
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Received SKIP_CURRENT_EPISODE intent");
+ if (media != null) {
+ setStatus(PlayerStatus.STOPPED);
+ player.reset();
+ endPlayback(false);
+ }
+ }
+ }
+ };
+
+ /**
+ * Periodically saves the position of the media file
+ */
+ class PositionSaver implements Runnable {
+ public static final int WAITING_INTERVALL = 5000;
+
+ @Override
+ public void run() {
+ if (player != null && player.isPlaying()) {
+ try {
+ saveCurrentPosition();
+ } catch (IllegalStateException e) {
+ Log.w(TAG,
+ "saveCurrentPosition was called in illegal state");
+ }
+ }
+ }
+ }
+
+ /**
+ * Notifies the player widget in the specified intervall
+ */
+ class WidgetUpdateWorker implements Runnable {
+ private static final int NOTIFICATION_INTERVALL = 1000;
+
+ @Override
+ public void run() {
+ if (PlaybackService.isRunning) {
+ updateWidget();
+ }
+ }
+ }
+
+ /**
+ * Sleeps for a given time and then pauses playback.
+ */
+ 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");
+ if (status == PlayerStatus.PLAYING) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Pausing playback");
+ pause(true, true);
+ }
+ postExecute();
+ }
+ } catch (InterruptedException e) {
+ Log.d(TAG, "Thread was interrupted while waiting");
+ break;
+ }
+ }
+ postExecute();
+ }
+
+ protected void postExecute() {
+ isWaiting = false;
+ sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0);
+ }
+
+ public long getWaitingTime() {
+ return waitingTime;
+ }
+
+ public boolean isWaiting() {
+ return isWaiting;
+ }
+
+ }
+
+ public static boolean isPlayingVideo() {
+ return playingVideo;
+ }
+
+ public boolean isShouldStream() {
+ return shouldStream;
+ }
+
+ public PlayerStatus getStatus() {
+ return status;
+ }
+
+ public Playable getMedia() {
+ return media;
+ }
+
+ public MediaPlayer getPlayer() {
+ return player;
+ }
+
+ public boolean isStartWhenPrepared() {
+ return startWhenPrepared;
+ }
+
+ public void setStartWhenPrepared(boolean startWhenPrepared) {
+ this.startWhenPrepared = startWhenPrepared;
+ postStatusUpdateIntent();
+ }
+
+ /**
+ * call getDuration() on mediaplayer or return INVALID_TIME if player is in
+ * an invalid state. This method should be used instead of calling
+ * getDuration() directly to avoid an error.
+ */
+ public int getDurationSafe() {
+ if (status != null && player != null) {
+ switch (status) {
+ case PREPARED:
+ case PLAYING:
+ case PAUSED:
+ case SEEKING:
+ try {
+ return player.getDuration();
+ } catch (IllegalStateException e) {
+ e.printStackTrace();
+ return INVALID_TIME;
+ }
+ default:
+ return INVALID_TIME;
+ }
+ } else {
+ return INVALID_TIME;
+ }
+ }
+
+ /**
+ * call getCurrentPosition() on mediaplayer or return INVALID_TIME if player
+ * is in an invalid state. This method should be used instead of calling
+ * getCurrentPosition() directly to avoid an error.
+ */
+ public int getCurrentPositionSafe() {
+ if (status != null && player != null) {
+ switch (status) {
+ case PREPARED:
+ case PLAYING:
+ case PAUSED:
+ case SEEKING:
+ return player.getCurrentPosition();
+ default:
+ return INVALID_TIME;
+ }
+ } else {
+ return INVALID_TIME;
+ }
+ }
+
+ private void setCurrentlyPlayingMedia(long id) {
+ SharedPreferences.Editor editor = PreferenceManager
+ .getDefaultSharedPreferences(getApplicationContext()).edit();
+ editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA, id);
+ editor.commit();
+ }
+
+ private static class InitTask extends AsyncTask<Playable, Void, Playable> {
+ private Playable playable;
+ public PlayableException exception;
+
+ @Override
+ protected Playable doInBackground(Playable... params) {
+ if (params[0] == null) {
+ throw new IllegalArgumentException("Playable must not be null");
+ }
+ playable = params[0];
+
+ try {
+ playable.loadMetadata();
+ } catch (PlayableException e) {
+ e.printStackTrace();
+ exception = e;
+ return null;
+ }
+ return playable;
+ }
+
+ @SuppressLint("NewApi")
+ public void executeAsync(Playable playable) {
+ FlattrUtils.hasToken();
+ if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) {
+ executeOnExecutor(THREAD_POOL_EXECUTOR, playable);
+ } else {
+ execute(playable);
+ }
+ }
+
+ }
+
+ private void loadQueue() {
+ dbLoaderExecutor.submit(new QueueLoaderTask());
+ }
+
+ private class QueueLoaderTask implements Runnable {
+ @Override
+ public void run() {
+ List<FeedItem> queueRef = DBReader.getQueue(PlaybackService.this);
+ queue = queueRef;
+ }
+ }
}
diff --git a/src/de/danoeh/antennapod/storage/DBTasks.java b/src/de/danoeh/antennapod/storage/DBTasks.java
index 45ce4298a..5853bbb02 100644
--- a/src/de/danoeh/antennapod/storage/DBTasks.java
+++ b/src/de/danoeh/antennapod/storage/DBTasks.java
@@ -381,9 +381,11 @@ public final class DBTasks {
}
public static FeedItem getQueueSuccessorOfItem(Context context,
- final long itemId) {
+ final long itemId, List<FeedItem> queue) {
FeedItem result = null;
- List<FeedItem> queue = DBReader.getQueue(context);
+ if (queue == null) {
+ queue = DBReader.getQueue(context);
+ }
if (queue != null) {
Iterator<FeedItem> iterator = queue.iterator();
while (iterator.hasNext()) {
diff --git a/src/de/danoeh/antennapod/util/QueueAccess.java b/src/de/danoeh/antennapod/util/QueueAccess.java
index 4c70d74e2..ce9d11429 100644
--- a/src/de/danoeh/antennapod/util/QueueAccess.java
+++ b/src/de/danoeh/antennapod/util/QueueAccess.java
@@ -48,6 +48,9 @@ public abstract class QueueAccess {
return new QueueAccess() {
@Override
public boolean contains(long id) {
+ if (items == null) {
+ return false;
+ }
Iterator<FeedItem> it = items.iterator();
for (FeedItem i = it.next(); it.hasNext(); i = it.next()) {
if (i.getId() == id) {