diff options
Diffstat (limited to 'core/src/main/java')
8 files changed, 439 insertions, 272 deletions
diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java index 64d4d14fd..d4414227c 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java @@ -1,5 +1,6 @@ package de.danoeh.antennapod.core.feed; +import android.content.Context; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.database.Cursor; @@ -14,12 +15,16 @@ import java.util.Date; import java.util.List; import java.util.concurrent.Callable; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction; +import de.danoeh.antennapod.core.preferences.GpodnetPreferences; import de.danoeh.antennapod.core.preferences.PlaybackPreferences; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.storage.PodDBAdapter; import de.danoeh.antennapod.core.util.ChapterUtils; +import de.danoeh.antennapod.core.util.flattr.FlattrUtils; import de.danoeh.antennapod.core.util.playback.Playable; public class FeedMedia extends FeedFile implements Playable { @@ -48,6 +53,8 @@ public class FeedMedia extends FeedFile implements Playable { private String mime_type; @Nullable private volatile FeedItem item; private Date playbackCompletionDate; + private int startPosition = -1; + private int playedDurationWhenStarted; // if null: unknown, will be checked private Boolean hasEmbeddedPicture; @@ -73,6 +80,7 @@ public class FeedMedia extends FeedFile implements Playable { this.duration = duration; this.position = position; this.played_duration = played_duration; + this.playedDurationWhenStarted = played_duration; this.size = size; this.mime_type = mime_type; this.playbackCompletionDate = playbackCompletionDate == null @@ -472,15 +480,59 @@ public class FeedMedia extends FeedFile implements Playable { } setPosition(newPosition); setLastPlayedTime(timeStamp); + if(startPosition>=0 && position > startPosition) { + setPlayedDuration(playedDurationWhenStarted + position - startPosition); + } DBWriter.setFeedMediaPlaybackInformation(this); } @Override public void onPlaybackStart() { + startPosition = (position > 0) ? position : 0; + playedDurationWhenStarted = played_duration; } + @Override - public void onPlaybackCompleted() { + public void onPlaybackPause(Context context) { + if (position > startPosition) { + played_duration = playedDurationWhenStarted + position - startPosition; + playedDurationWhenStarted = played_duration; + } + postPlaybackTasks(context, false); + startPosition = position; + } + @Override + public void onPlaybackCompleted(Context context) { + postPlaybackTasks(context, true); + startPosition = -1; + } + + private void postPlaybackTasks(Context context, boolean completed) { + if (item != null) { + // gpodder play action + if (startPosition >= 0 && (completed || startPosition < position) && + GpodnetPreferences.loggedIn()) { + GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, GpodnetEpisodeAction.Action.PLAY) + .currentDeviceId() + .currentTimestamp() + .started(startPosition / 1000) + .position((completed ? duration : position) / 1000) + .total(duration / 1000) + .build(); + GpodnetPreferences.enqueueEpisodeAction(action); + } + // Auto flattr + float autoFlattrThreshold = UserPreferences.getAutoFlattrPlayedDurationThreshold(); + if (FlattrUtils.hasToken() && + UserPreferences.isAutoFlattr() && + item.getPaymentLink() != null && + item.getFlattrStatus().getUnflattred() && + (completed && autoFlattrThreshold <= 1.0f || + played_duration >= autoFlattrThreshold * duration)) { + DBTasks.flattrItemIfLoggedIn(context, item); + } + } } @Override 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..0871758d0 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 @@ -13,6 +13,7 @@ import org.antennapod.audio.MediaPlayer; import java.io.IOException; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; @@ -140,10 +141,13 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { } // set temporarily to pause in order to update list with current position if (playerStatus == PlayerStatus.PLAYING) { - setPlayerStatus(PlayerStatus.PAUSED, media); + callback.onPlaybackPause(media, getPosition()); } - smartMarkAsPlayed(media); + if (!media.getIdentifier().equals(playable.getIdentifier())) { + final Playable oldMedia = media; + executor.submit(() -> callback.onPostPlayback(oldMedia, false, true)); + } setPlayerStatus(PlayerStatus.INDETERMINATE, null); } @@ -199,6 +203,8 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { audioFocusChangeListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); if (focusGained == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { + Log.d(TAG, "Audiofocus successfully requested"); + Log.d(TAG, "Resuming/Starting playback"); acquireWifiLockIfNecessary(); float speed = 1.0f; try { @@ -220,8 +226,6 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { setPlayerStatus(PlayerStatus.PLAYING, media); pausedBecauseOfTransientAudiofocusLoss = false; - media.onPlaybackStart(); - } else { Log.e(TAG, "Failed to request audio focus"); } @@ -249,7 +253,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { if (playerStatus == PlayerStatus.PLAYING) { Log.d(TAG, "Pausing playback."); mediaPlayer.pause(); - setPlayerStatus(PlayerStatus.PAUSED, media); + setPlayerStatus(PlayerStatus.PAUSED, media, getPosition()); if (abandonFocus) { audioManager.abandonAudioFocus(audioFocusChangeListener); @@ -311,11 +315,12 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { videoSize = new Pair<>(vp.getVideoWidth(), vp.getVideoHeight()); } + // TODO this call has no effect! if (media.getPosition() > 0) { seekToSync(media.getPosition()); } - if (media.getDuration() == 0) { + if (media.getDuration() <= 0) { Log.d(TAG, "Setting duration of media"); media.setDuration(mediaPlayer.getDuration()); } @@ -367,10 +372,6 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { if (playerStatus == PlayerStatus.PLAYING || playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PREPARED) { - if (!stream) { - statusBeforeSeeking = playerStatus; - setPlayerStatus(PlayerStatus.SEEKING, media); - } if(seekLatch != null && seekLatch.getCount() > 0) { try { seekLatch.await(3, TimeUnit.SECONDS); @@ -379,6 +380,8 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { } } seekLatch = new CountDownLatch(1); + statusBeforeSeeking = playerStatus; + setPlayerStatus(PlayerStatus.SEEKING, media, getPosition()); mediaPlayer.seekTo(t); try { seekLatch.await(3, TimeUnit.SECONDS); @@ -752,8 +755,8 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { @Override - public void endPlayback(final boolean wasSkipped, boolean switchingPlayers) { - executor.submit(() -> { + protected Future<?> endPlayback(final boolean wasSkipped, final boolean shouldContinue, final boolean toStoppedState) { + return executor.submit(() -> { playerLock.lock(); releaseWifiLockIfNecessary(); @@ -762,13 +765,58 @@ 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); + final Playable currentMedia = media; + Playable nextMedia = null; + + if (shouldContinue) { + // 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 + 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); + } + } + if (shouldContinue || toStoppedState) { + if (nextMedia == null) { + callback.onPlaybackEnded(null, true); + stop(); + } + final boolean hasNext = nextMedia != null; + + executor.submit(() -> callback.onPostPlayback(currentMedia, !wasSkipped, hasNext)); + } else if (isPlaying) { + callback.onPlaybackPause(currentMedia, currentMedia.getPosition()); + } playerLock.unlock(); }); } @@ -779,8 +827,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { * This method will only take care of changing the PlayerStatus of this object! Other tasks like * abandoning audio focus have to be done with other methods. */ - @Override - public void stop() { + private void stop() { executor.submit(() -> { playerLock.lock(); releaseWifiLockIfNecessary(); @@ -833,7 +880,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { mp -> genericOnCompletion(); private void genericOnCompletion() { - endPlayback(false, false); + endPlayback(false, true, true); } private final MediaPlayer.OnBufferingUpdateListener audioBufferingUpdateListener = @@ -889,8 +936,11 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { seekLatch.countDown(); } playerLock.lock(); + if (playerStatus == PlayerStatus.PLAYING) { + callback.onPlaybackStart(media, getPosition()); + } if (playerStatus == PlayerStatus.SEEKING) { - setPlayerStatus(statusBeforeSeeking, media); + setPlayerStatus(statusBeforeSeeking, media, getPosition()); } 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..04b5b676d 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 @@ -52,9 +52,6 @@ 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.glide.ApGlideSettings; -import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction; -import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction.Action; -import de.danoeh.antennapod.core.preferences.GpodnetPreferences; import de.danoeh.antennapod.core.preferences.PlaybackPreferences; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.receiver.MediaButtonReceiver; @@ -63,7 +60,6 @@ import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.util.IntList; import de.danoeh.antennapod.core.util.QueueAccess; -import de.danoeh.antennapod.core.util.flattr.FlattrUtils; import de.danoeh.antennapod.core.util.playback.ExternalMedia; import de.danoeh.antennapod.core.util.playback.Playable; @@ -206,8 +202,6 @@ public class PlaybackService extends MediaBrowserServiceCompat { */ private MediaSessionCompat mediaSession; - private int startPosition; - private static volatile MediaType currentMediaType = MediaType.UNKNOWN; private final IBinder mBinder = new LocalBinder(); @@ -473,7 +467,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { UserPreferences.shouldHardwareButtonSkip()) { // assume the skip command comes from a notification or the lockscreen // a >| skip button should actually skip - mediaPlayer.endPlayback(true, false); + mediaPlayer.skip(); } else { // assume skip command comes from a (bluetooth) media button // user actually wants to fast-forward @@ -530,7 +524,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { private final PlaybackServiceTaskManager.PSTMCallback taskManagerCallback = new PlaybackServiceTaskManager.PSTMCallback() { @Override public void positionSaverTick() { - saveCurrentPosition(true, PlaybackServiceTaskManager.POSITION_SAVER_WAITING_INTERVAL); + saveCurrentPosition(true, null, PlaybackServiceMediaPlayer.INVALID_TIME); } @Override @@ -582,9 +576,6 @@ public class PlaybackService extends MediaBrowserServiceCompat { break; case PAUSED: - taskManager.cancelPositionSaver(); - saveCurrentPosition(false, 0); - taskManager.cancelWidgetUpdater(); if ((UserPreferences.isPersistNotify() || isCasting) && android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { // do not remove notification on pause based on user pref and whether android version supports expanded notifications @@ -595,22 +586,6 @@ public class PlaybackService extends MediaBrowserServiceCompat { stopForeground(true); } writePlayerStatusPlaybackPreferences(); - - final Playable playable = newInfo.playable; - - // Gpodder: send play action - if(GpodnetPreferences.loggedIn() && playable instanceof FeedMedia) { - FeedMedia media = (FeedMedia) playable; - FeedItem item = media.getItem(); - GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, Action.PLAY) - .currentDeviceId() - .currentTimestamp() - .started(startPosition / 1000) - .position(getCurrentPosition() / 1000) - .total(getDuration() / 1000) - .build(); - GpodnetPreferences.enqueueEpisodeAction(action); - } break; case STOPPED: @@ -619,15 +594,9 @@ public class PlaybackService extends MediaBrowserServiceCompat { break; case PLAYING: - Log.d(TAG, "Audiofocus successfully requested"); - Log.d(TAG, "Resuming/Starting playback"); - - taskManager.startPositionSaver(); - taskManager.startWidgetUpdater(); writePlayerStatusPlaybackPreferences(); setupNotification(newInfo); started = true; - startPosition = mediaPlayer.getPosition(); break; case ERROR: @@ -700,121 +669,168 @@ 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(@NonNull Playable media, boolean ended, boolean playingNext) { + PlaybackService.this.onPostPlayback(media, ended, playingNext); + } + + @Override + public void onPlaybackStart(@NonNull Playable playable, int position) { + taskManager.startWidgetUpdater(); + if (position != PlaybackServiceMediaPlayer.INVALID_TIME) { + playable.setPosition(position); + } + playable.onPlaybackStart(); + taskManager.startPositionSaver(); + } + + @Override + public void onPlaybackPause(Playable playable, int position) { + taskManager.cancelPositionSaver(); + saveCurrentPosition(position == PlaybackServiceMediaPlayer.INVALID_TIME || playable == null, + playable, position); + taskManager.cancelWidgetUpdater(); + if (playable != null) { + playable.onPlaybackPause(getApplicationContext()); + } + } + + @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<FeedItem> 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); + } + } + + /** + * This method processes the media object after its playback ended, either because it completed + * or because a different media object was selected for playback. + * + * Even though these tasks aren't supposed to be resource intensive, a good practice is to + * usually call this method on a background thread. + * + * @param playable the media object that was playing. It is assumed that its position property + * was updated before this method was called. + * @param ended if true, it signals that {@param playable} was played until its end. + * In such case, the position property of the media becomes irrelevant for most of + * the tasks (although it's still a good practice to keep it accurate). + * @param playingNext if true, it means another media object is being loaded in place of this one. + * Instances when we'd set it to false would be when we're not following the + * queue or when the queue has ended. + */ + 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(); - - boolean isInQueue = false; - FeedItem nextItem = null; + if (!(playable instanceof FeedMedia)) { + Log.d(TAG, "Not doing post-playback processing: media not of type FeedMedia"); + if (ended) { + playable.onPlaybackCompleted(getApplicationContext()); + } else { + playable.onPlaybackPause(getApplicationContext()); + } + return; + } + FeedMedia media = (FeedMedia) playable; + FeedItem item = media.getItem(); + boolean smartMarkAsPlayed = playingNext && media.hasAlmostEnded(); + if (!ended && smartMarkAsPlayed) { + Log.d(TAG, "smart mark as played"); + } - if (playable instanceof FeedMedia && ((FeedMedia) playable).getItem() != null) { - FeedMedia media = (FeedMedia) playable; - FeedItem item = media.getItem(); + if (ended || smartMarkAsPlayed) { + media.onPlaybackCompleted(getApplicationContext()); + } else { + media.onPlaybackPause(getApplicationContext()); + } - 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<FeedItem> 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); } } @@ -1218,28 +1234,23 @@ public class PlaybackService extends MediaBrowserServiceCompat { /** * Persists the current position and last played time of the media file. * - * @param updatePlayedDuration true if played_duration should be updated. This applies only to FeedMedia objects - * @param deltaPlayedDuration value by which played_duration should be increased. + * @param fromMediaPlayer if true, the information is gathered from the current Media Player + * and {@param playable} and {@param position} become irrelevant. + * @param playable the playable for which the current position should be saved, unless + * {@param fromMediaPlayer} is true. + * @param position the position that should be saved, unless {@param fromMediaPlayer} is true. */ - private synchronized void saveCurrentPosition(boolean updatePlayedDuration, int deltaPlayedDuration) { - int position = getCurrentPosition(); - int duration = getDuration(); - float playbackSpeed = getCurrentPlaybackSpeed(); - final Playable playable = mediaPlayer.getPlayable(); + private synchronized void saveCurrentPosition(boolean fromMediaPlayer, Playable playable, int position) { + int duration; + if (fromMediaPlayer) { + position = getCurrentPosition(); + duration = getDuration(); + playable = mediaPlayer.getPlayable(); + } else { + duration = playable.getDuration(); + } if (position != INVALID_TIME && duration != INVALID_TIME && playable != null) { Log.d(TAG, "Saving current position to " + position); - if (updatePlayedDuration && playable instanceof FeedMedia) { - FeedMedia media = (FeedMedia) playable; - FeedItem item = media.getItem(); - media.setPlayedDuration(media.getPlayedDuration() + ((int) (deltaPlayedDuration * playbackSpeed))); - // Auto flattr - if (isAutoFlattrable(media) && - (media.getPlayedDuration() > UserPreferences.getAutoFlattrPlayedDurationThreshold() * duration)) { - Log.d(TAG, "saveCurrentPosition: performing auto flattr since played duration " + Integer.toString(media.getPlayedDuration()) - + " is " + UserPreferences.getAutoFlattrPlayedDurationThreshold() * 100 + "% of file duration " + Integer.toString(duration)); - DBTasks.flattrItemIfLoggedIn(this, item); - } - } playable.saveCurrentPosition( PreferenceManager.getDefaultSharedPreferences(getApplicationContext()), position, @@ -1407,7 +1418,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { public void onReceive(Context context, Intent intent) { if (TextUtils.equals(intent.getAction(), ACTION_SKIP_CURRENT_EPISODE)) { Log.d(TAG, "Received SKIP_CURRENT_EPISODE intent"); - mediaPlayer.endPlayback(true, false); + mediaPlayer.skip(); } } }; @@ -1500,26 +1511,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { public void seekTo(final int t) { - if(mediaPlayer.getPlayerStatus() == PlayerStatus.PLAYING - && GpodnetPreferences.loggedIn()) { - final Playable playable = mediaPlayer.getPlayable(); - if (playable instanceof FeedMedia) { - FeedMedia media = (FeedMedia) playable; - FeedItem item = media.getItem(); - GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, Action.PLAY) - .currentDeviceId() - .currentTimestamp() - .started(startPosition / 1000) - .position(getCurrentPosition() / 1000) - .total(getDuration() / 1000) - .build(); - GpodnetPreferences.enqueueEpisodeAction(action); - } - } mediaPlayer.seekTo(t); - if(mediaPlayer.getPlayerStatus() == PlayerStatus.PLAYING ) { - startPosition = t; - } } @@ -1528,10 +1520,10 @@ public class PlaybackService extends MediaBrowserServiceCompat { } /** - * @see LocalPSMP#seekToChapter(de.danoeh.antennapod.core.feed.Chapter) + * Seek to the start of the specified chapter. */ public void seekToChapter(Chapter c) { - mediaPlayer.seekToChapter(c); + seekTo((int) c.getStart()); } /** @@ -1558,15 +1550,6 @@ public class PlaybackService extends MediaBrowserServiceCompat { return mediaPlayer.getVideoSize(); } - private boolean isAutoFlattrable(FeedMedia media) { - if (media != null) { - FeedItem item = media.getItem(); - return item != null && FlattrUtils.hasToken() && UserPreferences.isAutoFlattr() && item.getPaymentLink() != null && item.getFlattrStatus().getUnflattred(); - } else { - return false; - } - } - private final MediaSessionCompat.Callback sessionCallback = new MediaSessionCompat.Callback() { private static final String TAG = "MediaSessionCompat"; @@ -1602,19 +1585,14 @@ public class PlaybackService extends MediaBrowserServiceCompat { public void onPause() { Log.d(TAG, "onPause()"); if (getStatus() == PlayerStatus.PLAYING) { - pause(false, true); - } - if (UserPreferences.isPersistNotify()) { - pause(false, true); - } else { - pause(true, true); + pause(!UserPreferences.isPersistNotify(), true); } } @Override public void onStop() { Log.d(TAG, "onStop()"); - mediaPlayer.stop(); + mediaPlayer.stopPlayback(true); } @Override @@ -1639,7 +1617,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { public void onSkipToNext() { Log.d(TAG, "onSkipToNext()"); if(UserPreferences.shouldHardwareButtonSkip()) { - mediaPlayer.endPlayback(true, false); + mediaPlayer.skip(); } else { seekDelta(UserPreferences.getFastFowardSecs() * 1000); } @@ -1682,7 +1660,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { PlaybackServiceMediaPlayer getMediaPlayer(); void setIsCasting(boolean isCasting); void sendNotificationBroadcast(int type, int code); - void saveCurrentPosition(boolean updatePlayedDuration, int deltaPlayedDuration); + void saveCurrentPosition(boolean fromMediaPlayer, Playable playable, int position); void setupNotification(boolean connected, PlaybackServiceMediaPlayer.PSMPInfo info); MediaSessionCompat getMediaSession(); Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter); @@ -1716,8 +1694,8 @@ public class PlaybackService extends MediaBrowserServiceCompat { } @Override - public void saveCurrentPosition(boolean updatePlayedDuration, int deltaPlayedDuration) { - PlaybackService.this.saveCurrentPosition(updatePlayedDuration, deltaPlayedDuration); + public void saveCurrentPosition(boolean fromMediaPlayer, Playable playable, int position) { + PlaybackService.this.saveCurrentPosition(fromMediaPlayer, playable, position); } @Override 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..aec059ca0 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 @@ -8,11 +8,9 @@ import android.util.Log; 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 java.util.concurrent.Future; + import de.danoeh.antennapod.core.feed.MediaType; -import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.util.playback.Playable; @@ -130,13 +128,6 @@ public abstract class PlaybackServiceMediaPlayer { public abstract void seekDelta(int d); /** - * Seek to the start of the specified chapter. - */ - public void seekToChapter(@NonNull Chapter c) { - seekTo((int) c.getStart()); - } - - /** * Returns the duration of the current media object or INVALID_TIME if the duration could not be retrieved. */ public abstract int getDuration(); @@ -236,15 +227,44 @@ public abstract class PlaybackServiceMediaPlayer { protected abstract void setPlayable(Playable playable); - public abstract void endPlayback(boolean wasSkipped, boolean switchingPlayers); + public void skip() { + endPlayback(true, true, true); + } /** - * Moves the PSMP into STOPPED state. This call is only valid if the player is currently in - * INDETERMINATE state, for example after a call to endPlayback. - * This method will only take care of changing the PlayerStatus of this object! Other tasks like - * abandoning audio focus have to be done with other methods. + * Ends playback of current media (if any) and moves into INDETERMINATE state, unless + * {@param toStoppedState} is set to true, in which case it moves into STOPPED state. + * + * @see #endPlayback(boolean, boolean, boolean) */ - public abstract void stop(); + public Future<?> stopPlayback(boolean toStoppedState) { + return endPlayback(true, false, toStoppedState); + } + + /** + * Internal method that handles end of playback. + * + * Currently, it has 4 use cases: + * <ul> + * <li>Media playback has completed: call with (false, true, true)</li> + * <li>User asks to skip to next episode: call with (true, true, true)</li> + * <li>Stopping the media player: call with (true, false, true)</li> + * <li>We want to change the media player implementation: call with (true, false, false)</li> + * </ul> + * + * @param wasSkipped If true, we assume the current media's playback has ended, for + * purposes of post playback processing. + * @param shouldContinue If true, the media player should try to load, and possibly play, + * the next item, based on the user preferences and whether such item + * exists. + * @param toStoppedState If true, the playback state gets set to STOPPED if the media player + * is not loading/playing after this call, and the UI will reflect that. + * Only relevant if {@param shouldContinue} is set to false, otherwise + * this method's behavior defaults as if this parameter was true. + * + * @return a Future, just for the purpose of tracking its execution. + */ + protected abstract Future<?> endPlayback(boolean wasSkipped, boolean shouldContinue, boolean toStoppedState); /** * @return {@code true} if the WifiLock feature should be used, {@code false} otherwise. @@ -274,41 +294,39 @@ public abstract class PlaybackServiceMediaPlayer { * <p/> * This method will notify the callback about the change of the player status (even if the new status is the same * as the old one). + * <p/> + * It will also call {@link PSMPCallback#onPlaybackPause(Playable, int)} or {@link PSMPCallback#onPlaybackStart(Playable, int)} + * depending on the status change. * * @param newStatus The new PlayerStatus. This must not be null. * @param newMedia The new playable object of the PSMP object. This can be null. + * @param position The position to be set to the current Playable object in case playback started or paused. + * Will be ignored if given the value of {@link #INVALID_TIME}. */ - protected final synchronized void setPlayerStatus(@NonNull PlayerStatus newStatus, Playable newMedia) { + protected final synchronized void setPlayerStatus(@NonNull PlayerStatus newStatus, Playable newMedia, int position) { Log.d(TAG, this.getClass().getSimpleName() + ": Setting player status to " + newStatus); + PlayerStatus oldStatus = playerStatus; + this.playerStatus = newStatus; setPlayable(newMedia); - if (playerStatus != null) { - Log.d(TAG, "playerStatus: " + playerStatus.toString()); + if (newMedia != null && newStatus != PlayerStatus.INDETERMINATE) { + if (oldStatus == PlayerStatus.PLAYING && newStatus != PlayerStatus.PLAYING) { + callback.onPlaybackPause(newMedia, position); + } else if (oldStatus != PlayerStatus.PLAYING && newStatus == PlayerStatus.PLAYING) { + callback.onPlaybackStart(newMedia, position); + } } 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()); - } - } - } + /** + * @see #setPlayerStatus(PlayerStatus, Playable, int) + */ + protected final void setPlayerStatus(@NonNull PlayerStatus newStatus, Playable newMedia) { + setPlayerStatus(newStatus, newMedia, INVALID_TIME); } public interface PSMPCallback { @@ -328,7 +346,15 @@ public abstract class PlaybackServiceMediaPlayer { boolean onMediaPlayerError(Object inObj, int what, int extra); - boolean endPlayback(Playable media, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers); + void onPostPlayback(@NonNull Playable media, boolean ended, boolean playingNext); + + void onPlaybackStart(@NonNull Playable playable, int position); + + void onPlaybackPause(Playable playable, int position); + + Playable getNextInQueue(Playable currentMedia); + + void onPlaybackEnded(MediaType mediaType, boolean stopPlaying); } /** diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java index 7fd7602a8..0c7d5e718 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java @@ -299,7 +299,8 @@ public class PlaybackServiceTaskManager { private static final String TAG = "SleepTimer"; private static final long UPDATE_INTERVAL = 1000L; private static final long NOTIFICATION_THRESHOLD = 10000; - private long waitingTime; + private final long waitingTime; + private long timeLeft; private final boolean shakeToReset; private final boolean vibrate; private ShakeListener shakeListener; @@ -307,6 +308,7 @@ public class PlaybackServiceTaskManager { public SleepTimer(long waitingTime, boolean shakeToReset, boolean vibrate) { super(); this.waitingTime = waitingTime; + this.timeLeft = waitingTime; this.shakeToReset = shakeToReset; this.vibrate = vibrate; } @@ -316,14 +318,14 @@ public class PlaybackServiceTaskManager { Log.d(TAG, "Starting"); boolean notifiedAlmostExpired = false; long lastTick = System.currentTimeMillis(); - while (waitingTime > 0) { + while (timeLeft > 0) { try { Thread.sleep(UPDATE_INTERVAL); long now = System.currentTimeMillis(); - waitingTime -= now - lastTick; + timeLeft -= now - lastTick; lastTick = now; - if(waitingTime < NOTIFICATION_THRESHOLD && !notifiedAlmostExpired) { + if(timeLeft < NOTIFICATION_THRESHOLD && !notifiedAlmostExpired) { Log.d(TAG, "Sleep timer is about to expire"); if(vibrate) { Vibrator v = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); @@ -337,7 +339,7 @@ public class PlaybackServiceTaskManager { callback.onSleepTimerAlmostExpired(); notifiedAlmostExpired = true; } - if (waitingTime <= 0) { + if (timeLeft <= 0) { Log.d(TAG, "Sleep timer expired"); if(shakeListener != null) { shakeListener.pause(); @@ -358,11 +360,11 @@ public class PlaybackServiceTaskManager { } public long getWaitingTime() { - return waitingTime; + return timeLeft; } public void onShake() { - setSleepTimer(15 * 60 * 1000, shakeToReset, vibrate); + setSleepTimer(waitingTime, shakeToReset, vibrate); callback.onSleepTimerReset(); shakeListener.pause(); shakeListener = null; diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSMedia.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSMedia.java index 7a8b2bc03..839e2ae0c 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSMedia.java +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSMedia.java @@ -7,8 +7,10 @@ import org.xml.sax.Attributes; import java.util.concurrent.TimeUnit; +import de.danoeh.antennapod.core.feed.FeedImage; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.syndication.handler.HandlerState; +import de.danoeh.antennapod.core.syndication.namespace.atom.AtomText; import de.danoeh.antennapod.core.syndication.util.SyndTypeUtils; /** Processes tags from the http://search.yahoo.com/mrss/ namespace. */ @@ -23,22 +25,35 @@ public class NSMedia extends Namespace { private static final String SIZE = "fileSize"; private static final String MIME_TYPE = "type"; private static final String DURATION = "duration"; + private static final String DEFAULT = "isDefault"; + + private static final String IMAGE = "thumbnail"; + private static final String IMAGE_URL = "url"; + + private static final String DESCRIPTION = "description"; + private static final String DESCRIPTION_TYPE = "type"; @Override public SyndElement handleElementStart(String localName, HandlerState state, - Attributes attributes) { + Attributes attributes) { if (CONTENT.equals(localName)) { String url = attributes.getValue(DOWNLOAD_URL); String type = attributes.getValue(MIME_TYPE); - boolean validType; - if(SyndTypeUtils.enclosureTypeValid(type)) { - validType = true; - } else { - type = SyndTypeUtils.getValidMimeTypeFromUrl(url); - validType = type != null; - } - if (state.getCurrentItem() != null && state.getCurrentItem().getMedia() == null && - url != null && validType) { + String defaultStr = attributes.getValue(DEFAULT); + boolean validType; + + boolean isDefault = "true".equals(defaultStr); + + if (SyndTypeUtils.enclosureTypeValid(type)) { + validType = true; + } else { + type = SyndTypeUtils.getValidMimeTypeFromUrl(url); + validType = type != null; + } + + if (state.getCurrentItem() != null && + (state.getCurrentItem().getMedia() == null || isDefault) && + url != null && validType) { long size = 0; String sizeStr = attributes.getValue(SIZE); try { @@ -51,25 +66,50 @@ public class NSMedia extends Namespace { String durationStr = attributes.getValue(DURATION); if (!TextUtils.isEmpty(durationStr)) { try { - long duration = Long.parseLong(durationStr); + long duration = Long.parseLong(durationStr); durationMs = (int) TimeUnit.MILLISECONDS.convert(duration, TimeUnit.SECONDS); } catch (NumberFormatException e) { Log.e(TAG, "Duration \"" + durationStr + "\" could not be parsed"); } } FeedMedia media = new FeedMedia(state.getCurrentItem(), url, size, type); - if(durationMs > 0) { + if (durationMs > 0) { media.setDuration(durationMs); } state.getCurrentItem().setMedia(media); } + } else if (IMAGE.equals(localName)) { + String url = attributes.getValue(IMAGE_URL); + if (url != null) { + FeedImage image = new FeedImage(); + image.setDownload_url(url); + + if (state.getCurrentItem() != null) { + image.setOwner(state.getCurrentItem()); + state.getCurrentItem().setImage(image); + } else { + if (state.getFeed().getImage() == null) { + image.setOwner(state.getFeed()); + state.getFeed().setImage(image); + } + } + } + } else if (DESCRIPTION.equals(localName)) { + String type = attributes.getValue(DESCRIPTION_TYPE); + return new AtomText(localName, this, type); } return new SyndElement(localName, this); } @Override public void handleElementEnd(String localName, HandlerState state) { - + if (DESCRIPTION.equals(localName)) { + String content = state.getContentBuf().toString(); + if (state.getCurrentItem() != null && content != null && + state.getCurrentItem().getDescription() == null) { + state.getCurrentItem().setDescription(content); + } + } } - } + diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java index a37f98469..c4acdb65e 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java @@ -1,5 +1,6 @@ package de.danoeh.antennapod.core.util.playback; +import android.content.Context; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.media.MediaMetadataRetriever; @@ -205,7 +206,12 @@ public class ExternalMedia implements Playable { } @Override - public void onPlaybackCompleted() { + public void onPlaybackPause(Context context) { + + } + + @Override + public void onPlaybackCompleted(Context context) { } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java index 6459d86ed..279c56338 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java @@ -138,14 +138,27 @@ public interface Playable extends Parcelable, void setLastPlayedTime(long lastPlayedTimestamp); /** - * Is called by the PlaybackService when playback starts. + * This method should be called every time playback starts on this object. + * <p/> + * Position held by this Playable should be set accurately before a call to this method is made. */ void onPlaybackStart(); /** - * Is called by the PlaybackService when playback is completed. + * This method should be called every time playback pauses or stops on this object, + * including just before a seeking operation is performed, after which a call to + * {@link #onPlaybackStart()} should be made. If playback completes, calling this method is not + * necessary, as long as a call to {@link #onPlaybackCompleted(Context)} is made. + * <p/> + * Position held by this Playable should be set accurately before a call to this method is made. */ - void onPlaybackCompleted(); + void onPlaybackPause(Context context); + + /** + * This method should be called when playback completes for this object. + * @param context + */ + void onPlaybackCompleted(Context context); /** * Returns an integer that must be unique among all Playable classes. The |