diff --git a/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java b/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java index 6f8dbcb1a..8305eb68e 100644 --- a/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java +++ b/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java @@ -20,6 +20,7 @@ import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.FeedPreferences; +import de.danoeh.antennapod.core.feed.MediaType; import de.danoeh.antennapod.core.service.playback.PlaybackServiceMediaPlayer; import de.danoeh.antennapod.core.service.playback.LocalPSMP; import de.danoeh.antennapod.core.service.playback.PlayerStatus; @@ -196,10 +197,19 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase { } @Override - public boolean endPlayback(Playable p, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) { - return false; + public void onPostPlayback(Playable media, boolean ended, boolean playingNext) { + } + @Override + public Playable getNextInQueue(Playable currentMedia) { + return null; + } + + @Override + public void onPlaybackEnded(MediaType mediaType, boolean stopPlaying) { + + } }; PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, callback); Playable p = writeTestPlayable(PLAYABLE_FILE_URL, null); @@ -275,8 +285,18 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase { } @Override - public boolean endPlayback(Playable p, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) { - return false; + public void onPostPlayback(Playable media, boolean ended, boolean playingNext) { + + } + + @Override + public Playable getNextInQueue(Playable currentMedia) { + return null; + } + + @Override + public void onPlaybackEnded(MediaType mediaType, boolean stopPlaying) { + } }; PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, callback); @@ -357,8 +377,18 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase { } @Override - public boolean endPlayback(Playable p, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) { - return false; + public void onPostPlayback(Playable media, boolean ended, boolean playingNext) { + + } + + @Override + public Playable getNextInQueue(Playable currentMedia) { + return null; + } + + @Override + public void onPlaybackEnded(MediaType mediaType, boolean stopPlaying) { + } }; PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, callback); @@ -440,8 +470,18 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase { } @Override - public boolean endPlayback(Playable p, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) { - return false; + public void onPostPlayback(Playable media, boolean ended, boolean playingNext) { + + } + + @Override + public Playable getNextInQueue(Playable currentMedia) { + return null; + } + + @Override + public void onPlaybackEnded(MediaType mediaType, boolean stopPlaying) { + } }; @@ -517,8 +557,18 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase { } @Override - public boolean endPlayback(Playable p, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) { - return false; + public void onPostPlayback(Playable media, boolean ended, boolean playingNext) { + + } + + @Override + public Playable getNextInQueue(Playable currentMedia) { + return null; + } + + @Override + public void onPlaybackEnded(MediaType mediaType, boolean stopPlaying) { + } }; @@ -595,8 +645,18 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase { } @Override - public boolean endPlayback(Playable p, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) { - return false; + public void onPostPlayback(Playable media, boolean ended, boolean playingNext) { + + } + + @Override + public Playable getNextInQueue(Playable currentMedia) { + return null; + } + + @Override + public void onPlaybackEnded(MediaType mediaType, boolean stopPlaying) { + } }; PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, callback); @@ -675,8 +735,18 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase { } @Override - public boolean endPlayback(Playable p, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) { - return false; + public void onPostPlayback(Playable media, boolean ended, boolean playingNext) { + + } + + @Override + public Playable getNextInQueue(Playable currentMedia) { + return null; + } + + @Override + public void onPlaybackEnded(MediaType mediaType, boolean stopPlaying) { + } }; PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, callback); @@ -758,8 +828,18 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase { } @Override - public boolean endPlayback(Playable p, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) { - return false; + public void onPostPlayback(Playable media, boolean ended, boolean playingNext) { + + } + + @Override + public Playable getNextInQueue(Playable currentMedia) { + return null; + } + + @Override + public void onPlaybackEnded(MediaType mediaType, boolean stopPlaying) { + } }; PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, callback); @@ -814,8 +894,18 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase { } @Override - public boolean endPlayback(Playable p, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) { - return false; + public void onPostPlayback(Playable media, boolean ended, boolean playingNext) { + + } + + @Override + public Playable getNextInQueue(Playable currentMedia) { + return null; + } + + @Override + public void onPlaybackEnded(MediaType mediaType, boolean stopPlaying) { + } }; @@ -896,8 +986,18 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase { } @Override - public boolean endPlayback(Playable p, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) { - return false; + public void onPostPlayback(Playable media, boolean ended, boolean playingNext) { + + } + + @Override + public Playable getNextInQueue(Playable currentMedia) { + return null; + } + + @Override + public void onPlaybackEnded(MediaType mediaType, boolean stopPlaying) { + } }; PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, callback); @@ -1012,8 +1112,18 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase { } @Override - public boolean endPlayback(Playable p, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) { - return false; + public void onPostPlayback(Playable media, boolean ended, boolean playingNext) { + + } + + @Override + public Playable getNextInQueue(Playable currentMedia) { + return null; + } + + @Override + public void onPlaybackEnded(MediaType mediaType, boolean stopPlaying) { + } }; PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, callback); @@ -1103,8 +1213,18 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase { } @Override - public boolean endPlayback(Playable p, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) { - return false; + public void onPostPlayback(Playable media, boolean ended, boolean playingNext) { + + } + + @Override + public Playable getNextInQueue(Playable currentMedia) { + return null; + } + + @Override + public void onPlaybackEnded(MediaType mediaType, boolean stopPlaying) { + } }; PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, callback); @@ -1207,8 +1327,18 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase { } @Override - public boolean endPlayback(Playable p, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) { - return false; + public void onPostPlayback(Playable media, boolean ended, boolean playingNext) { + + } + + @Override + public Playable getNextInQueue(Playable currentMedia) { + return null; + } + + @Override + public void onPlaybackEnded(MediaType mediaType, boolean stopPlaying) { + } }; PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, callback); diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java index 8bacac1ef..6fbc722b6 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java @@ -143,7 +143,10 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { setPlayerStatus(PlayerStatus.PAUSED, media); } - smartMarkAsPlayed(media); + if (!media.getIdentifier().equals(playable.getIdentifier())) { + final Playable oldMedia = media; + executor.submit(() -> callback.onPostPlayback(oldMedia, false, true)); + } setPlayerStatus(PlayerStatus.INDETERMINATE, null); } @@ -762,13 +765,47 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { if (playerStatus != PlayerStatus.INDETERMINATE) { setPlayerStatus(PlayerStatus.INDETERMINATE, media); } + // we're relying on the position stored in the Playable object for post-playback processing + if (media != null) { + int position = getPosition(); + if (position >= 0) { + media.setPosition(position); + } + } + if (mediaPlayer != null) { mediaPlayer.reset(); - } audioManager.abandonAudioFocus(audioFocusChangeListener); - callback.endPlayback(media, isPlaying, wasSkipped, switchingPlayers); + // 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 + final Playable currentMedia = media; + Playable nextMedia = callback.getNextInQueue(currentMedia); + boolean playNextEpisode = isPlaying && + nextMedia != null && + UserPreferences.isFollowQueue(); + + if (playNextEpisode) { + Log.d(TAG, "Playback of next episode will start immediately."); + } else if (nextMedia == null){ + Log.d(TAG, "No more episodes available to play"); + } else { + Log.d(TAG, "Loading next episode, but not playing automatically."); + } + + if (nextMedia != null) { + callback.onPlaybackEnded(nextMedia.getMediaType(), !playNextEpisode); + // setting media to null signals to playMediaObject() that we're taking care of post-playback processing + media = null; + playMediaObject(nextMedia, false, !nextMedia.localFileAvailable(), playNextEpisode, playNextEpisode); + } else { + callback.onPlaybackEnded(null, true); + stop(); + } + + executor.submit(() -> callback.onPostPlayback(currentMedia, !wasSkipped, nextMedia != null)); playerLock.unlock(); }); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java index e3557f5f8..14fa28eed 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java @@ -700,121 +700,138 @@ public class PlaybackService extends MediaBrowserServiceCompat { } @Override - public boolean endPlayback(Playable media, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) { - PlaybackService.this.endPlayback(media, playNextEpisode, wasSkipped, switchingPlayers); - return true; + public void onPostPlayback(Playable media, boolean ended, boolean playingNext) { + PlaybackService.this.onPostPlayback(media, ended, playingNext); + } + + @Override + public Playable getNextInQueue(Playable currentMedia) { + return PlaybackService.this.getNextInQueue(currentMedia); + } + + @Override + public void onPlaybackEnded(MediaType mediaType, boolean stopPlaying) { + PlaybackService.this.onPlaybackEnded(mediaType, stopPlaying); } }; - private void endPlayback(final Playable playable, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) { - Log.d(TAG, "Playback ended" + (switchingPlayers ? " from switching players": "")); + private Playable getNextInQueue(final Playable currentMedia) { + if (!(currentMedia instanceof FeedMedia)) { + Log.d(TAG, "getNextInQueue(), but playable not an instance of FeedMedia, so not proceeding"); + return null; + } + if (!ClientConfig.playbackServiceCallbacks.useQueue()) { + Log.d(TAG, "getNextInQueue(), but queue not in use by this app"); + return null; + } + Log.d(TAG, "getNextInQueue()"); + FeedMedia media = (FeedMedia) currentMedia; + try { + media.loadMetadata(); + } catch (Playable.PlayableException e) { + Log.e(TAG, "Unable to load metadata to get next in queue", e); + return null; + } + FeedItem item = media.getItem(); + if (item == null) { + Log.w(TAG, "getNextInQueue() with FeedMedia object whose FeedItem is null"); + return null; + } + FeedItem nextItem; + try { + final List queue = taskManager.getQueue(); + nextItem = DBTasks.getQueueSuccessorOfItem(item.getId(), queue); + } catch (InterruptedException e) { + Log.e(TAG, "Error handling the queue in order to retrieve the next item", e); + return null; + } + return (nextItem != null)? nextItem.getMedia() : null; + } + + /** + * Set of instructions to be performed when playback ends. + */ + private void onPlaybackEnded(MediaType mediaType, boolean stopPlaying) { + Log.d(TAG, "Playback ended"); + if (stopPlaying) { + taskManager.cancelPositionSaver(); + writePlaybackPreferencesNoMediaPlaying(); + if (!isCasting) { + stopForeground(true); + } + stopWidgetUpdater(); + } + if (mediaType == null) { + sendNotificationBroadcast(NOTIFICATION_TYPE_PLAYBACK_END, 0); + } else { + sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, + isCasting ? EXTRA_CODE_CAST : + (mediaType == MediaType.VIDEO) ? EXTRA_CODE_VIDEO : EXTRA_CODE_AUDIO); + } + } + + //TODO add javadoc, that is is assumed that the playable object contains info about its position and duration + private void onPostPlayback(final Playable playable, boolean ended, boolean playingNext) { if (playable == null) { - Log.e(TAG, "Cannot end playback: media was null"); + Log.e(TAG, "Cannot do post-playback processing: media was null"); return; } + Log.d(TAG, "onPostPlayback(): media=" + playable.getEpisodeTitle()); - taskManager.cancelPositionSaver(); + if (!(playable instanceof FeedMedia)) { + Log.d(TAG, "Not doing post-playback processing: media not of type FeedMedia"); + return; + } + FeedMedia media = (FeedMedia) playable; + FeedItem item = media.getItem(); + boolean smartMarkAsPlayed = playingNext && media.hasAlmostEnded(); + if (!ended && smartMarkAsPlayed) { + Log.d(TAG, "smart mark as played"); + } - boolean isInQueue = false; - FeedItem nextItem = null; + // auto-flattr if enabled + if (isAutoFlattrable(media) && UserPreferences.getAutoFlattrPlayedDurationThreshold() == 1.0f) { + DBTasks.flattrItemIfLoggedIn(PlaybackService.this, item); + } + + // gpodder play action + if (GpodnetPreferences.loggedIn()) { + GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, Action.PLAY) + .currentDeviceId() + .currentTimestamp() + .started(startPosition / 1000) + .position(((ended || smartMarkAsPlayed) ? media.getDuration() : media.getPosition()) / 1000) + .total(media.getDuration() / 1000) + .build(); + GpodnetPreferences.enqueueEpisodeAction(action); + } - if (playable instanceof FeedMedia && ((FeedMedia) playable).getItem() != null) { - FeedMedia media = (FeedMedia) playable; - FeedItem item = media.getItem(); - - if (!switchingPlayers) { + if (item != null) { + if (ended || smartMarkAsPlayed || + !UserPreferences.shouldSkipKeepEpisode()) { + // only mark the item as played if we're not keeping it anyways + DBWriter.markItemPlayed(item, FeedItem.PLAYED, ended); try { final List queue = taskManager.getQueue(); - isInQueue = QueueAccess.ItemListAccess(queue).contains(item.getId()); - nextItem = DBTasks.getQueueSuccessorOfItem(item.getId(), queue); + if (QueueAccess.ItemListAccess(queue).contains(item.getId())) { + // don't know if it actually matters to not autodownload when smart mark as played is triggered + DBWriter.removeQueueItem(PlaybackService.this, item, ended); + } } catch (InterruptedException e) { e.printStackTrace(); // isInQueue remains false } - - boolean shouldKeep = wasSkipped && UserPreferences.shouldSkipKeepEpisode(); - - if (!shouldKeep) { - // only mark the item as played if we're not keeping it anyways - DBWriter.markItemPlayed(item, FeedItem.PLAYED, true); - - if (isInQueue) { - DBWriter.removeQueueItem(PlaybackService.this, item, true); - } - - // Delete episode if enabled - if (item.getFeed().getPreferences().getCurrentAutoDelete()) { - DBWriter.deleteFeedMediaOfItem(PlaybackService.this, media.getId()); - Log.d(TAG, "Episode Deleted"); - } + // Delete episode if enabled + if (item.getFeed().getPreferences().getCurrentAutoDelete()) { + DBWriter.deleteFeedMediaOfItem(PlaybackService.this, media.getId()); + Log.d(TAG, "Episode Deleted"); } } - - - DBWriter.addItemToPlaybackHistory(media); - - // auto-flattr if enabled - if (isAutoFlattrable(media) && UserPreferences.getAutoFlattrPlayedDurationThreshold() == 1.0f) { - DBTasks.flattrItemIfLoggedIn(PlaybackService.this, item); - } - - // gpodder play action - if(GpodnetPreferences.loggedIn()) { - GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, Action.PLAY) - .currentDeviceId() - .currentTimestamp() - .started(startPosition / 1000) - .position(getDuration() / 1000) - .total(getDuration() / 1000) - .build(); - GpodnetPreferences.enqueueEpisodeAction(action); - } } - if (!switchingPlayers) { - // 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 - Playable nextMedia = null; - boolean loadNextItem = ClientConfig.playbackServiceCallbacks.useQueue() && - isInQueue && - nextItem != null; - - playNextEpisode = playNextEpisode && - loadNextItem && - UserPreferences.isFollowQueue(); - - if (loadNextItem) { - Log.d(TAG, "Loading next item in queue"); - nextMedia = nextItem.getMedia(); - } - final boolean prepareImmediately; - final boolean startWhenPrepared; - final boolean stream; - - if (playNextEpisode) { - Log.d(TAG, "Playback of next episode will start immediately."); - prepareImmediately = startWhenPrepared = true; - } else { - Log.d(TAG, "No more episodes available to play"); - prepareImmediately = startWhenPrepared = false; - stopForeground(true); - stopWidgetUpdater(); - } - - writePlaybackPreferencesNoMediaPlaying(); - if (nextMedia != null) { - stream = !nextMedia.localFileAvailable(); - mediaPlayer.playMediaObject(nextMedia, stream, startWhenPrepared, prepareImmediately); - sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, - isCasting ? EXTRA_CODE_CAST : - (nextMedia.getMediaType() == MediaType.VIDEO) ? EXTRA_CODE_VIDEO : EXTRA_CODE_AUDIO); - } else { - sendNotificationBroadcast(NOTIFICATION_TYPE_PLAYBACK_END, 0); - mediaPlayer.stop(); - //stopSelf(); - } + if (ended || playingNext) { + DBWriter.addItemToPlaybackHistory(media); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java index e05733135..51742b27e 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java @@ -9,10 +9,7 @@ import android.util.Pair; import android.view.SurfaceHolder; import de.danoeh.antennapod.core.feed.Chapter; -import de.danoeh.antennapod.core.feed.FeedItem; -import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.MediaType; -import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.util.playback.Playable; @@ -291,26 +288,6 @@ public abstract class PlaybackServiceMediaPlayer { callback.statusChanged(new PSMPInfo(playerStatus, getPlayable())); } - protected void smartMarkAsPlayed(Playable media) { - if(media != null && media instanceof FeedMedia) { - FeedMedia oldMedia = (FeedMedia) media; - if(oldMedia.hasAlmostEnded()) { - Log.d(TAG, "smart mark as read"); - FeedItem item = oldMedia.getItem(); - if (item == null) { - return; - } - DBWriter.markItemPlayed(item, FeedItem.PLAYED, false); - DBWriter.removeQueueItem(context, item, false); - DBWriter.addItemToPlaybackHistory(oldMedia); - if (item.getFeed().getPreferences().getCurrentAutoDelete()) { - Log.d(TAG, "Delete " + oldMedia.toString()); - DBWriter.deleteFeedMediaOfItem(context, oldMedia.getId()); - } - } - } - } - public interface PSMPCallback { void statusChanged(PSMPInfo newInfo); @@ -328,7 +305,11 @@ public abstract class PlaybackServiceMediaPlayer { boolean onMediaPlayerError(Object inObj, int what, int extra); - boolean endPlayback(Playable media, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers); + void onPostPlayback(Playable media, boolean ended, boolean playingNext); + + Playable getNextInQueue(Playable currentMedia); + + void onPlaybackEnded(MediaType mediaType, boolean stopPlaying); } /**