summaryrefslogtreecommitdiff
path: root/core/src
diff options
context:
space:
mode:
authorByteHamster <ByteHamster@users.noreply.github.com>2021-11-28 22:19:14 +0100
committerGitHub <noreply@github.com>2021-11-28 22:19:14 +0100
commitf0100e61ac633516082ea112363132c99f7c0b7a (patch)
treef7598c0cee85780b409ab895a8041d1607eec312 /core/src
parentaf2835c59dcb0473aba7a48b38f5abe28dca34d3 (diff)
downloadAntennaPod-f0100e61ac633516082ea112363132c99f7c0b7a.zip
Chromecast rework (#5518)
Diffstat (limited to 'core/src')
-rw-r--r--core/src/free/java/de/danoeh/antennapod/core/CastCallbacks.java7
-rw-r--r--core/src/free/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceFlavorHelper.java54
-rw-r--r--core/src/free/java/de/danoeh/antennapod/core/service/playback/WearMediaSession.java15
-rw-r--r--core/src/free/res/values/strings.xml4
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java (renamed from core/src/free/java/de/danoeh/antennapod/core/ClientConfig.java)2
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java2
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java1
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java18
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java176
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java380
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceNotificationBuilder.java25
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackVolumeUpdater.java2
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/PlayerStatus.java33
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/RewindAfterPauseUtils.java47
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java4
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdater.java2
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdaterJobService.java2
-rw-r--r--core/src/main/res/values-land/dimens.xml4
-rw-r--r--core/src/main/res/values/dimens.xml7
-rw-r--r--core/src/main/res/values/strings.xml19
-rw-r--r--core/src/play/java/de/danoeh/antennapod/core/CastCallbacks.java12
-rw-r--r--core/src/play/java/de/danoeh/antennapod/core/ClientConfig.java64
-rw-r--r--core/src/play/java/de/danoeh/antennapod/core/cast/CastButtonVisibilityManager.java120
-rw-r--r--core/src/play/java/de/danoeh/antennapod/core/cast/CastConsumer.java11
-rw-r--r--core/src/play/java/de/danoeh/antennapod/core/cast/CastManager.java1091
-rw-r--r--core/src/play/java/de/danoeh/antennapod/core/cast/CastUtils.java303
-rw-r--r--core/src/play/java/de/danoeh/antennapod/core/cast/DefaultCastConsumer.java10
-rw-r--r--core/src/play/java/de/danoeh/antennapod/core/cast/MediaInfoCreator.java57
-rw-r--r--core/src/play/java/de/danoeh/antennapod/core/cast/SwitchableMediaRouteActionProvider.java106
-rw-r--r--core/src/play/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceFlavorHelper.java314
-rw-r--r--core/src/play/java/de/danoeh/antennapod/core/service/playback/RemotePSMP.java680
-rw-r--r--core/src/play/java/de/danoeh/antennapod/core/service/playback/WearMediaSession.java28
-rw-r--r--core/src/play/res/values/strings.xml4
-rw-r--r--core/src/test/java/de/danoeh/antennapod/core/service/playback/PlaybackVolumeUpdaterTest.java2
-rw-r--r--core/src/test/java/de/danoeh/antennapod/core/util/RewindAfterPauseUtilTest.java56
35 files changed, 120 insertions, 3542 deletions
diff --git a/core/src/free/java/de/danoeh/antennapod/core/CastCallbacks.java b/core/src/free/java/de/danoeh/antennapod/core/CastCallbacks.java
deleted file mode 100644
index 2e266c736..000000000
--- a/core/src/free/java/de/danoeh/antennapod/core/CastCallbacks.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package de.danoeh.antennapod.core;
-
-/**
- * Callbacks for Chromecast support on the core module
- */
-public interface CastCallbacks {
-}
diff --git a/core/src/free/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceFlavorHelper.java b/core/src/free/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceFlavorHelper.java
deleted file mode 100644
index 837cb1bd0..000000000
--- a/core/src/free/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceFlavorHelper.java
+++ /dev/null
@@ -1,54 +0,0 @@
-package de.danoeh.antennapod.core.service.playback;
-
-import android.content.Context;
-import androidx.annotation.StringRes;
-import android.support.v4.media.session.MediaSessionCompat;
-import android.support.v4.media.session.PlaybackStateCompat;
-
-/**
- * Class intended to work along PlaybackService and provide support for different flavors.
- */
-class PlaybackServiceFlavorHelper {
-
- private final PlaybackService.FlavorHelperCallback callback;
-
- PlaybackServiceFlavorHelper(Context context, PlaybackService.FlavorHelperCallback callback) {
- this.callback = callback;
- }
-
- void initializeMediaPlayer(Context context) {
- callback.setMediaPlayer(new LocalPSMP(context, callback.getMediaPlayerCallback()));
- }
-
- void removeCastConsumer() {
- // no-op
- }
-
- boolean castDisconnect(boolean castDisconnect) {
- return false;
- }
-
- boolean onMediaPlayerInfo(Context context, int code, @StringRes int resourceId) {
- return false;
- }
-
- void registerWifiBroadcastReceiver() {
- // no-op
- }
-
- void unregisterWifiBroadcastReceiver() {
- // no-op
- }
-
- boolean onSharedPreference(String key) {
- return false;
- }
-
- void sessionStateAddActionForWear(PlaybackStateCompat.Builder sessionState, String actionName, CharSequence name, int icon) {
- // no-op
- }
-
- void mediaSessionSetExtraForWear(MediaSessionCompat mediaSession) {
- // no-op
- }
-}
diff --git a/core/src/free/java/de/danoeh/antennapod/core/service/playback/WearMediaSession.java b/core/src/free/java/de/danoeh/antennapod/core/service/playback/WearMediaSession.java
new file mode 100644
index 000000000..373b24bc8
--- /dev/null
+++ b/core/src/free/java/de/danoeh/antennapod/core/service/playback/WearMediaSession.java
@@ -0,0 +1,15 @@
+package de.danoeh.antennapod.core.service.playback;
+
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+
+class WearMediaSession {
+ static void sessionStateAddActionForWear(PlaybackStateCompat.Builder sessionState, String actionName,
+ CharSequence name, int icon) {
+ // no-op
+ }
+
+ static void mediaSessionSetExtraForWear(MediaSessionCompat mediaSession) {
+ // no-op
+ }
+}
diff --git a/core/src/free/res/values/strings.xml b/core/src/free/res/values/strings.xml
deleted file mode 100644
index fb49bbbe7..000000000
--- a/core/src/free/res/values/strings.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources>
- <string name="pref_cast_message" translatable="false">@string/pref_cast_message_free_flavor</string>
-</resources>
diff --git a/core/src/free/java/de/danoeh/antennapod/core/ClientConfig.java b/core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java
index 755bec14e..ac67fb042 100644
--- a/core/src/free/java/de/danoeh/antennapod/core/ClientConfig.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java
@@ -30,8 +30,6 @@ public class ClientConfig {
public static DownloadServiceCallbacks downloadServiceCallbacks;
- public static CastCallbacks castCallbacks;
-
private static boolean initialized = false;
public static synchronized void initialize(Context context) {
diff --git a/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java
index 8d80ef32b..f0c61403f 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java
@@ -8,8 +8,8 @@ import android.util.Log;
import de.danoeh.antennapod.event.PlayerStatusEvent;
import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.model.playback.MediaType;
-import de.danoeh.antennapod.core.service.playback.PlayerStatus;
import de.danoeh.antennapod.model.playback.Playable;
+import de.danoeh.antennapod.playback.base.PlayerStatus;
import org.greenrobot.eventbus.EventBus;
import static de.danoeh.antennapod.model.feed.FeedPreferences.SPEED_USE_GLOBAL;
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java
index 79363e872..7ce06a9fb 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java
@@ -40,6 +40,7 @@ import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
import de.danoeh.antennapod.core.service.download.HttpDownloader;
import de.danoeh.antennapod.core.util.NetworkUtils;
import de.danoeh.antennapod.core.util.playback.IPlayer;
+import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
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 5648024de..34fc7d699 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
@@ -17,8 +17,9 @@ import androidx.media.AudioManagerCompat;
import de.danoeh.antennapod.event.PlayerErrorEvent;
import de.danoeh.antennapod.event.playback.BufferUpdateEvent;
import de.danoeh.antennapod.event.playback.SpeedChangedEvent;
-import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.util.playback.MediaPlayerError;
+import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer;
+import de.danoeh.antennapod.playback.base.PlayerStatus;
import org.antennapod.audio.MediaPlayer;
import java.io.File;
@@ -39,7 +40,7 @@ import de.danoeh.antennapod.model.playback.MediaType;
import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting;
import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils;
import de.danoeh.antennapod.core.preferences.UserPreferences;
-import de.danoeh.antennapod.core.util.RewindAfterPauseUtils;
+import de.danoeh.antennapod.playback.base.RewindAfterPauseUtils;
import de.danoeh.antennapod.core.util.playback.AudioPlayer;
import de.danoeh.antennapod.core.util.playback.IPlayer;
import de.danoeh.antennapod.model.playback.Playable;
@@ -148,7 +149,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
}
public LocalPSMP(@NonNull Context context,
- @NonNull PSMPCallback callback) {
+ @NonNull PlaybackServiceMediaPlayer.PSMPCallback callback) {
super(context, callback);
this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
this.playerLock = new PlayerLock();
@@ -265,9 +266,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
LocalPSMP.this.startWhenPrepared.set(startWhenPrepared);
setPlayerStatus(PlayerStatus.INITIALIZING, media);
try {
- if (media instanceof FeedMedia && ((FeedMedia) media).getItem() == null) {
- ((FeedMedia) media).setItem(DBReader.getFeedItem(((FeedMedia) media).getItemId()));
- }
+ callback.ensureMediaInfoLoaded(media);
callback.onMediaChanged(false);
setPlaybackParams(PlaybackSpeedUtils.getCurrentPlaybackSpeed(media), UserPreferences.isSkipSilence());
if (stream) {
@@ -1098,7 +1097,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
EventBus.getDefault().post(BufferUpdateEvent.ended());
return true;
default:
- return callback.onMediaPlayerInfo(what, 0);
+ return true;
}
}
@@ -1148,4 +1147,9 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
executor.submit(r);
}
}
+
+ @Override
+ public boolean isCasting() {
+ return false;
+ }
}
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 60ccb5c9e..949c0ff9d 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
@@ -37,6 +37,7 @@ import android.widget.Toast;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
@@ -47,6 +48,10 @@ import de.danoeh.antennapod.event.playback.BufferUpdateEvent;
import de.danoeh.antennapod.event.playback.PlaybackServiceEvent;
import de.danoeh.antennapod.event.PlayerErrorEvent;
import de.danoeh.antennapod.event.playback.SleepTimerUpdatedEvent;
+import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer;
+import de.danoeh.antennapod.playback.base.PlayerStatus;
+import de.danoeh.antennapod.playback.cast.CastPsmp;
+import de.danoeh.antennapod.playback.cast.CastStateListener;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
@@ -103,24 +108,10 @@ public class PlaybackService extends MediaBrowserServiceCompat {
*/
private static final String TAG = "PlaybackService";
- /**
- * Parcelable of type Playable.
- */
public static final String EXTRA_PLAYABLE = "PlaybackService.PlayableExtra";
- /**
- * True if cast session should disconnect.
- */
- public static final String EXTRA_CAST_DISCONNECT = "extra.de.danoeh.antennapod.core.service.castDisconnect";
- /**
- * True if media should be streamed.
- */
public static final String EXTRA_SHOULD_STREAM = "extra.de.danoeh.antennapod.core.service.shouldStream";
public static final String EXTRA_ALLOW_STREAM_THIS_TIME = "extra.de.danoeh.antennapod.core.service.allowStream";
public static final String EXTRA_ALLOW_STREAM_ALWAYS = "extra.de.danoeh.antennapod.core.service.allowStreamAlways";
- /**
- * True if playback should be started immediately after media has been
- * prepared.
- */
public static final String EXTRA_START_WHEN_PREPARED = "extra.de.danoeh.antennapod.core.service.startWhenPrepared";
public static final String EXTRA_PREPARE_IMMEDIATELY = "extra.de.danoeh.antennapod.core.service.prepareImmediately";
@@ -200,10 +191,10 @@ public class PlaybackService extends MediaBrowserServiceCompat {
private PlaybackServiceMediaPlayer mediaPlayer;
private PlaybackServiceTaskManager taskManager;
- private PlaybackServiceFlavorHelper flavorHelper;
private PlaybackServiceStateManager stateManager;
private Disposable positionEventTimer;
private PlaybackServiceNotificationBuilder notificationBuilder;
+ private CastStateListener castStateListener;
private String autoSkippedFeedMediaId = null;
@@ -280,7 +271,6 @@ public class PlaybackService extends MediaBrowserServiceCompat {
EventBus.getDefault().register(this);
taskManager = new PlaybackServiceTaskManager(this, taskManagerCallback);
- flavorHelper = new PlaybackServiceFlavorHelper(PlaybackService.this, flavorHelperCallback);
PreferenceManager.getDefaultSharedPreferences(this)
.registerOnSharedPreferenceChangeListener(prefListener);
@@ -305,12 +295,36 @@ public class PlaybackService extends MediaBrowserServiceCompat {
npe.printStackTrace();
}
- flavorHelper.initializeMediaPlayer(PlaybackService.this);
+ recreateMediaPlayer();
mediaSession.setActive(true);
-
+ castStateListener = new CastStateListener(this) {
+ @Override
+ public void onSessionStartedOrEnded() {
+ recreateMediaPlayer();
+ }
+ };
EventBus.getDefault().post(new PlaybackServiceEvent(PlaybackServiceEvent.Action.SERVICE_STARTED));
}
+ void recreateMediaPlayer() {
+ Playable media = null;
+ boolean wasPlaying = false;
+ if (mediaPlayer != null) {
+ media = mediaPlayer.getPlayable();
+ wasPlaying = mediaPlayer.getPlayerStatus() == PlayerStatus.PLAYING;
+ mediaPlayer.pause(true, false);
+ mediaPlayer.shutdown();
+ }
+ mediaPlayer = CastPsmp.getInstanceIfConnected(this, mediaPlayerCallback);
+ if (mediaPlayer == null) {
+ mediaPlayer = new LocalPSMP(this, mediaPlayerCallback); // Cast not supported or not connected
+ }
+ if (media != null) {
+ mediaPlayer.playMediaObject(media, !media.localFileAvailable(), wasPlaying, true);
+ }
+ isCasting = mediaPlayer.isCasting();
+ }
+
@Override
public void onDestroy() {
super.onDestroy();
@@ -324,6 +338,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
stateManager.stopForeground(!UserPreferences.isPersistNotify());
isRunning = false;
currentMediaType = MediaType.UNKNOWN;
+ castStateListener.destroy();
cancelPositionObserver();
PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(prefListener);
@@ -337,8 +352,6 @@ public class PlaybackService extends MediaBrowserServiceCompat {
unregisterReceiver(audioBecomingNoisy);
unregisterReceiver(skipCurrentEpisodeReceiver);
unregisterReceiver(pausePlayCurrentEpisodeReceiver);
- flavorHelper.removeCastConsumer();
- flavorHelper.unregisterWifiBroadcastReceiver();
mediaPlayer.shutdown();
taskManager.shutdown();
}
@@ -483,9 +496,8 @@ public class PlaybackService extends MediaBrowserServiceCompat {
final int keycode = intent.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1);
final boolean hardwareButton = intent.getBooleanExtra(MediaButtonReceiver.EXTRA_HARDWAREBUTTON, false);
- final boolean castDisconnect = intent.getBooleanExtra(EXTRA_CAST_DISCONNECT, false);
Playable playable = intent.getParcelableExtra(EXTRA_PLAYABLE);
- if (keycode == -1 && playable == null && !castDisconnect) {
+ if (keycode == -1 && playable == null) {
Log.e(TAG, "PlaybackService was started with no arguments");
stateManager.stopService();
return Service.START_NOT_STICKY;
@@ -509,7 +521,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
stateManager.stopService();
return Service.START_NOT_STICKY;
}
- } else if (!flavorHelper.castDisconnect(castDisconnect) && playable != null) {
+ } else {
stateManager.validStartCommandWasReceived();
boolean stream = intent.getBooleanExtra(EXTRA_SHOULD_STREAM, true);
boolean allowStreamThisTime = intent.getBooleanExtra(EXTRA_ALLOW_STREAM_THIS_TIME, false);
@@ -553,9 +565,6 @@ public class PlaybackService extends MediaBrowserServiceCompat {
stateManager.stopService();
});
return Service.START_NOT_STICKY;
- } else {
- Log.d(TAG, "Did not handle intent to PlaybackService: " + intent);
- Log.d(TAG, "Extras: " + intent.getExtras());
}
}
@@ -781,8 +790,6 @@ public class PlaybackService extends MediaBrowserServiceCompat {
saveCurrentPosition(true, null, PlaybackServiceMediaPlayer.INVALID_TIME);
}
-
-
@Override
public WidgetUpdater.WidgetState requestWidgetState() {
return new WidgetUpdater.WidgetState(getPlayable(), getStatus(),
@@ -873,11 +880,6 @@ public class PlaybackService extends MediaBrowserServiceCompat {
}
@Override
- public boolean onMediaPlayerInfo(int code, @StringRes int resourceId) {
- return flavorHelper.onMediaPlayerInfo(PlaybackService.this, code, resourceId);
- }
-
- @Override
public void onPostPlayback(@NonNull Playable media, boolean ended, boolean skipped,
boolean playingNext) {
PlaybackService.this.onPostPlayback(media, ended, skipped, playingNext);
@@ -916,10 +918,24 @@ public class PlaybackService extends MediaBrowserServiceCompat {
return PlaybackService.this.getNextInQueue(currentMedia);
}
+ @Nullable
+ @Override
+ public Playable findMedia(@NonNull String url) {
+ FeedItem item = DBReader.getFeedItemByGuidOrEpisodeUrl(null, url);
+ return item != null ? item.getMedia() : null;
+ }
+
@Override
public void onPlaybackEnded(MediaType mediaType, boolean stopPlaying) {
PlaybackService.this.onPlaybackEnded(mediaType, stopPlaying);
}
+
+ @Override
+ public void ensureMediaInfoLoaded(@NonNull Playable media) {
+ if (media instanceof FeedMedia && ((FeedMedia) media).getItem() == null) {
+ ((FeedMedia) media).setItem(DBReader.getFeedItem(((FeedMedia) media).getItemId()));
+ }
+ }
};
@Subscribe(threadMode = ThreadMode.MAIN)
@@ -1248,15 +1264,15 @@ public class PlaybackService extends MediaBrowserServiceCompat {
// This would give the PIP of videos a play button
capabilities = capabilities | PlaybackStateCompat.ACTION_PLAY;
if (uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_WATCH) {
- flavorHelper.sessionStateAddActionForWear(sessionState,
+ WearMediaSession.sessionStateAddActionForWear(sessionState,
CUSTOM_ACTION_REWIND,
getString(R.string.rewind_label),
android.R.drawable.ic_media_rew);
- flavorHelper.sessionStateAddActionForWear(sessionState,
+ WearMediaSession.sessionStateAddActionForWear(sessionState,
CUSTOM_ACTION_FAST_FORWARD,
getString(R.string.fast_forward_label),
android.R.drawable.ic_media_ff);
- flavorHelper.mediaSessionSetExtraForWear(mediaSession);
+ WearMediaSession.mediaSessionSetExtraForWear(mediaSession);
}
}
@@ -1338,7 +1354,6 @@ public class PlaybackService extends MediaBrowserServiceCompat {
notificationBuilder.setPlayable(playable);
notificationBuilder.setMediaSessionToken(mediaSession.getSessionToken());
notificationBuilder.setPlayerStatus(playerStatus);
- notificationBuilder.setCasting(isCasting);
notificationBuilder.updatePosition(getCurrentPosition(), getCurrentPlaybackSpeed());
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
@@ -1901,93 +1916,6 @@ public class PlaybackService extends MediaBrowserServiceCompat {
(sharedPreferences, key) -> {
if (UserPreferences.PREF_LOCKSCREEN_BACKGROUND.equals(key)) {
updateNotificationAndMediaSession(getPlayable());
- } else {
- flavorHelper.onSharedPreference(key);
}
};
-
- interface FlavorHelperCallback {
- PlaybackServiceMediaPlayer.PSMPCallback getMediaPlayerCallback();
-
- void setMediaPlayer(PlaybackServiceMediaPlayer mediaPlayer);
-
- PlaybackServiceMediaPlayer getMediaPlayer();
-
- void setIsCasting(boolean isCasting);
-
- void sendNotificationBroadcast(int type, int code);
-
- void saveCurrentPosition(boolean fromMediaPlayer, Playable playable, int position);
-
- void setupNotification(boolean connected, PlaybackServiceMediaPlayer.PSMPInfo info);
-
- MediaSessionCompat getMediaSession();
-
- Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter);
-
- void unregisterReceiver(BroadcastReceiver receiver);
- }
-
- private final FlavorHelperCallback flavorHelperCallback = new FlavorHelperCallback() {
- @Override
- public PlaybackServiceMediaPlayer.PSMPCallback getMediaPlayerCallback() {
- return PlaybackService.this.mediaPlayerCallback;
- }
-
- @Override
- public void setMediaPlayer(PlaybackServiceMediaPlayer mediaPlayer) {
- PlaybackService.this.mediaPlayer = mediaPlayer;
- }
-
- @Override
- public PlaybackServiceMediaPlayer getMediaPlayer() {
- return PlaybackService.this.mediaPlayer;
- }
-
- @Override
- public void setIsCasting(boolean isCasting) {
- PlaybackService.isCasting = isCasting;
- stateManager.validStartCommandWasReceived();
- }
-
- @Override
- public void sendNotificationBroadcast(int type, int code) {
- PlaybackService.this.sendNotificationBroadcast(type, code);
- }
-
- @Override
- public void saveCurrentPosition(boolean fromMediaPlayer, Playable playable, int position) {
- PlaybackService.this.saveCurrentPosition(fromMediaPlayer, playable, position);
- }
-
- @Override
- public void setupNotification(boolean connected, PlaybackServiceMediaPlayer.PSMPInfo info) {
- if (connected) {
- PlaybackService.this.updateNotificationAndMediaSession(info.playable);
- } else {
- PlayerStatus status = info.playerStatus;
- if (status == PlayerStatus.PLAYING || status == PlayerStatus.SEEKING
- || status == PlayerStatus.PREPARING || UserPreferences.isPersistNotify()) {
- PlaybackService.this.updateNotificationAndMediaSession(info.playable);
- } else if (!UserPreferences.isPersistNotify()) {
- stateManager.stopForeground(true);
- }
- }
- }
-
- @Override
- public MediaSessionCompat getMediaSession() {
- return PlaybackService.this.mediaSession;
- }
-
- @Override
- public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
- return PlaybackService.this.registerReceiver(receiver, filter);
- }
-
- @Override
- public void unregisterReceiver(BroadcastReceiver receiver) {
- PlaybackService.this.unregisterReceiver(receiver);
- }
- };
}
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
deleted file mode 100644
index 623ad58bb..000000000
--- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java
+++ /dev/null
@@ -1,380 +0,0 @@
-package de.danoeh.antennapod.core.service.playback;
-
-import android.content.Context;
-import android.media.AudioManager;
-import android.net.wifi.WifiManager;
-import androidx.annotation.NonNull;
-import androidx.annotation.StringRes;
-import android.util.Log;
-import android.util.Pair;
-import android.view.SurfaceHolder;
-
-import java.util.List;
-import java.util.concurrent.Future;
-
-import de.danoeh.antennapod.model.playback.MediaType;
-import de.danoeh.antennapod.model.playback.Playable;
-
-
-/*
- * An inconvenience of an implementation like this is that some members and methods that once were
- * private are now protected, allowing for access from classes of the same package, namely
- * PlaybackService. A workaround would be to move this to a dedicated package.
- */
-/**
- * Abstract class that allows for different implementations of the PlaybackServiceMediaPlayer for local
- * and remote (cast devices) playback.
- */
-public abstract class PlaybackServiceMediaPlayer {
- private static final String TAG = "PlaybackSvcMediaPlayer";
-
- /**
- * Return value of some PSMP methods if the method call failed.
- */
- static final int INVALID_TIME = -1;
-
- private volatile PlayerStatus oldPlayerStatus;
- volatile PlayerStatus playerStatus;
-
- /**
- * A wifi-lock that is acquired if the media file is being streamed.
- */
- private WifiManager.WifiLock wifiLock;
-
- final PSMPCallback callback;
- final Context context;
-
- PlaybackServiceMediaPlayer(@NonNull Context context,
- @NonNull PSMPCallback callback){
- this.context = context;
- this.callback = callback;
-
- playerStatus = PlayerStatus.STOPPED;
- }
-
- /**
- * Starts or prepares playback of the specified Playable object. If another Playable object is already being played, the currently playing
- * episode will be stopped and replaced with the new Playable object. If the Playable object is already being played, the method will
- * not do anything.
- * Whether playback starts immediately depends on the given parameters. See below for more details.
- * <p/>
- * States:
- * During execution of the method, the object will be in the INITIALIZING state. The end state depends on the given parameters.
- * <p/>
- * If 'prepareImmediately' is set to true, the method will go into PREPARING state and after that into PREPARED state. If
- * 'startWhenPrepared' is set to true, the method will additionally go into PLAYING state.
- * <p/>
- * If an unexpected error occurs while loading the Playable's metadata or while setting the MediaPlayers data source, the object
- * will enter the ERROR state.
- * <p/>
- * This method is executed on an internal executor service.
- *
- * @param playable The Playable object that is supposed to be played. This parameter must not be null.
- * @param stream The type of playback. If false, the Playable object MUST provide access to a locally available file via
- * getLocalMediaUrl. If true, the Playable object MUST provide access to a resource that can be streamed by
- * the Android MediaPlayer via getStreamUrl.
- * @param startWhenPrepared Sets the 'startWhenPrepared' flag. This flag determines whether playback will start immediately after the
- * episode has been prepared for playback. Setting this flag to true does NOT mean that the episode will be prepared
- * for playback immediately (see 'prepareImmediately' parameter for more details)
- * @param prepareImmediately Set to true if the method should also prepare the episode for playback.
- */
- public abstract void playMediaObject(@NonNull Playable playable, boolean stream, boolean startWhenPrepared, boolean prepareImmediately);
-
- /**
- * Resumes playback if the PSMP object is in PREPARED or PAUSED state. If the PSMP object is in an invalid state.
- * nothing will happen.
- * <p/>
- * This method is executed on an internal executor service.
- */
- public abstract void resume();
-
- /**
- * Saves the current position and pauses playback. Note that, if audiofocus
- * is abandoned, the lockscreen controls will also disapear.
- * <p/>
- * This method is executed on an internal executor service.
- *
- * @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 abstract void pause(boolean abandonFocus, boolean reinit);
-
- /**
- * Prepared media player for playback if the service is in the INITALIZED
- * state.
- * <p/>
- * This method is executed on an internal executor service.
- */
- public abstract void prepare();
-
- /**
- * Resets the media player and moves it into INITIALIZED state.
- * <p/>
- * This method is executed on an internal executor service.
- */
- public abstract void reinit();
-
- /**
- * Seeks to the specified position. If the PSMP object is in an invalid state, this method will do nothing.
- * Invalid time values (< 0) will be ignored.
- * <p/>
- * This method is executed on an internal executor service.
- */
- public abstract void seekTo(int t);
-
- /**
- * Seek a specific position from the current position
- *
- * @param d offset from current position (positive or negative)
- */
- public abstract void seekDelta(int d);
-
- /**
- * Returns the duration of the current media object or INVALID_TIME if the duration could not be retrieved.
- */
- public abstract int getDuration();
-
- /**
- * Returns the position of the current media object or INVALID_TIME if the position could not be retrieved.
- */
- public abstract int getPosition();
-
- public abstract boolean isStartWhenPrepared();
-
- public abstract void setStartWhenPrepared(boolean startWhenPrepared);
-
- /**
- * Sets the playback parameters.
- * - Speed
- * - SkipSilence (ExoPlayer only)
- * This method is executed on an internal executor service.
- */
- public abstract void setPlaybackParams(final float speed, final boolean skipSilence);
-
- /**
- * Returns the current playback speed. If the playback speed could not be retrieved, 1 is returned.
- */
- public abstract float getPlaybackSpeed();
-
- /**
- * Sets the playback volume.
- * This method is executed on an internal executor service.
- */
- public abstract void setVolume(float volumeLeft, float volumeRight);
-
- /**
- * Returns true if the mediaplayer can mix stereo down to mono
- */
- public abstract boolean canDownmix();
-
- public abstract void setDownmix(boolean enable);
-
- public abstract MediaType getCurrentMediaType();
-
- public abstract boolean isStreaming();
-
- /**
- * Releases internally used resources. This method should only be called when the object is not used anymore.
- */
- public abstract void shutdown();
-
- /**
- * Releases internally used resources. This method should only be called when the object is not used anymore.
- * This method is executed on an internal executor service.
- */
- public abstract void shutdownQuietly();
-
- public abstract void setVideoSurface(SurfaceHolder surface);
-
- public abstract void resetVideoSurface();
-
- /**
- * Return width and height of the currently playing video as a pair.
- *
- * @return Width and height as a Pair or null if the video size could not be determined. The method might still
- * return an invalid non-null value if the getVideoWidth() and getVideoHeight() methods of the media player return
- * invalid values.
- */
- public abstract Pair<Integer, Integer> getVideoSize();
-
- /**
- * Returns a PSMInfo object that contains information about the current state of the PSMP object.
- *
- * @return The PSMPInfo object.
- */
- public final synchronized PSMPInfo getPSMPInfo() {
- return new PSMPInfo(oldPlayerStatus, playerStatus, getPlayable());
- }
-
- /**
- * Returns the current status, if you need the media and the player status together, you should
- * use getPSMPInfo() to make sure they're properly synchronized. Otherwise a race condition
- * could result in nonsensical results (like a status of PLAYING, but a null playable)
- * @return the current player status
- */
- public synchronized PlayerStatus getPlayerStatus() {
- return playerStatus;
- }
-
- /**
- * Returns the current media, if you need the media and the player status together, you should
- * use getPSMPInfo() to make sure they're properly synchronized. Otherwise a race condition
- * could result in nonsensical results (like a status of PLAYING, but a null playable)
- * @return the current media. May be null
- */
- public abstract Playable getPlayable();
-
- protected abstract void setPlayable(Playable playable);
-
- public abstract List<String> getAudioTracks();
-
- public abstract void setAudioTrack(int track);
-
- public abstract int getSelectedAudioTrack();
-
- public void skip() {
- endPlayback(false, true, true, true);
- }
-
- /**
- * 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, boolean)
- */
- public Future<?> stopPlayback(boolean toStoppedState) {
- return endPlayback(false, false, false, toStoppedState);
- }
-
- /**
- * Internal method that handles end of playback.
- *
- * Currently, it has 5 use cases:
- * <ul>
- * <li>Media playback has completed: call with (true, false, true, true)</li>
- * <li>User asks to skip to next episode: call with (false, true, true, true)</li>
- * <li>Skipping to next episode due to playback error: call with (false, false, true, true)</li>
- * <li>Stopping the media player: call with (false, false, false, true)</li>
- * <li>We want to change the media player implementation: call with (false, false, false, false)</li>
- * </ul>
- *
- * @param hasEnded If true, we assume the current media's playback has ended, for
- * purposes of post playback processing.
- * @param wasSkipped Whether the user chose to skip the episode (by pressing the skip
- * button).
- * @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 hasEnded, boolean wasSkipped,
- boolean shouldContinue, boolean toStoppedState);
-
- /**
- * @return {@code true} if the WifiLock feature should be used, {@code false} otherwise.
- */
- protected abstract boolean shouldLockWifi();
-
- final synchronized void acquireWifiLockIfNecessary() {
- if (shouldLockWifi()) {
- if (wifiLock == null) {
- wifiLock = ((WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE))
- .createWifiLock(WifiManager.WIFI_MODE_FULL, TAG);
- wifiLock.setReferenceCounted(false);
- }
- wifiLock.acquire();
- }
- }
-
- final synchronized void releaseWifiLockIfNecessary() {
- if (wifiLock != null && wifiLock.isHeld()) {
- wifiLock.release();
- }
- }
-
- /**
- * Sets the player status of the PSMP object. PlayerStatus and media attributes have to be set at the same time
- * so that getPSMPInfo can't return an invalid state (e.g. status is PLAYING, but media is null).
- * <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}.
- */
- final synchronized void setPlayerStatus(@NonNull PlayerStatus newStatus, Playable newMedia, int position) {
- Log.d(TAG, this.getClass().getSimpleName() + ": Setting player status to " + newStatus);
-
- this.oldPlayerStatus = playerStatus;
- this.playerStatus = newStatus;
- setPlayable(newMedia);
-
- if (newMedia != null && newStatus != PlayerStatus.INDETERMINATE) {
- if (oldPlayerStatus == PlayerStatus.PLAYING && newStatus != PlayerStatus.PLAYING) {
- callback.onPlaybackPause(newMedia, position);
- } else if (oldPlayerStatus != PlayerStatus.PLAYING && newStatus == PlayerStatus.PLAYING) {
- callback.onPlaybackStart(newMedia, position);
- }
- }
-
- callback.statusChanged(new PSMPInfo(oldPlayerStatus, playerStatus, getPlayable()));
- }
-
- public boolean isAudioChannelInUse() {
- AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
- return (audioManager.getMode() != AudioManager.MODE_NORMAL || audioManager.isMusicActive());
- }
-
- /**
- * @see #setPlayerStatus(PlayerStatus, Playable, int)
- */
- final void setPlayerStatus(@NonNull PlayerStatus newStatus, Playable newMedia) {
- setPlayerStatus(newStatus, newMedia, INVALID_TIME);
- }
-
- public interface PSMPCallback {
- void statusChanged(PSMPInfo newInfo);
-
- void shouldStop();
-
- void onMediaChanged(boolean reloadUI);
-
- boolean onMediaPlayerInfo(int code, @StringRes int resourceId);
-
- void onPostPlayback(@NonNull Playable media, boolean ended, boolean skipped, 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);
- }
-
- /**
- * Holds information about a PSMP object.
- */
- public static class PSMPInfo {
- public final PlayerStatus oldPlayerStatus;
- public PlayerStatus playerStatus;
- public Playable playable;
-
- PSMPInfo(PlayerStatus oldPlayerStatus, PlayerStatus playerStatus, Playable playable) {
- this.oldPlayerStatus = oldPlayerStatus;
- this.playerStatus = playerStatus;
- this.playable = playable;
- }
- }
-}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceNotificationBuilder.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceNotificationBuilder.java
index 5aee8c24c..c348f5773 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceNotificationBuilder.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceNotificationBuilder.java
@@ -31,17 +31,17 @@ import de.danoeh.antennapod.model.playback.Playable;
import java.util.ArrayList;
import java.util.concurrent.ExecutionException;
+import de.danoeh.antennapod.playback.base.PlayerStatus;
import org.apache.commons.lang3.ArrayUtils;
public class PlaybackServiceNotificationBuilder {
private static final String TAG = "PlaybackSrvNotification";
private static Bitmap defaultIcon = null;
- private Context context;
+ private final Context context;
private Playable playable;
private MediaSessionCompat.Token mediaSessionToken;
private PlayerStatus playerStatus;
- private boolean isCasting;
private Bitmap icon;
private String position;
@@ -140,7 +140,7 @@ public class PlaybackServiceNotificationBuilder {
if (playable != null) {
notification.setContentTitle(playable.getFeedTitle());
notification.setContentText(playable.getEpisodeTitle());
- addActions(notification, mediaSessionToken, playerStatus, isCasting);
+ addActions(notification, mediaSessionToken, playerStatus);
if (icon != null) {
notification.setLargeIcon(icon);
@@ -175,23 +175,10 @@ public class PlaybackServiceNotificationBuilder {
}
private void addActions(NotificationCompat.Builder notification, MediaSessionCompat.Token mediaSessionToken,
- PlayerStatus playerStatus, boolean isCasting) {
+ PlayerStatus playerStatus) {
ArrayList<Integer> compactActionList = new ArrayList<>();
int numActions = 0; // we start and 0 and then increment by 1 for each call to addAction
-
- if (isCasting) {
- Intent stopCastingIntent = new Intent(context, PlaybackService.class);
- stopCastingIntent.putExtra(PlaybackService.EXTRA_CAST_DISCONNECT, true);
- PendingIntent stopCastingPendingIntent = PendingIntent.getService(context,
- numActions, stopCastingIntent, PendingIntent.FLAG_UPDATE_CURRENT
- | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0));
- notification.addAction(R.drawable.ic_notification_cast_off,
- context.getString(R.string.cast_disconnect_label),
- stopCastingPendingIntent);
- numActions++;
- }
-
// always let them rewind
PendingIntent rewindButtonPendingIntent = getPendingIntentForMediaAction(
KeyEvent.KEYCODE_MEDIA_REWIND, numActions);
@@ -270,10 +257,6 @@ public class PlaybackServiceNotificationBuilder {
this.playerStatus = playerStatus;
}
- public void setCasting(boolean casting) {
- isCasting = casting;
- }
-
public PlayerStatus getPlayerStatus() {
return playerStatus;
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackVolumeUpdater.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackVolumeUpdater.java
index edb8bc3a9..43837a473 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackVolumeUpdater.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackVolumeUpdater.java
@@ -4,6 +4,8 @@ import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.model.feed.FeedPreferences;
import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting;
import de.danoeh.antennapod.model.playback.Playable;
+import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer;
+import de.danoeh.antennapod.playback.base.PlayerStatus;
class PlaybackVolumeUpdater {
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlayerStatus.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlayerStatus.java
deleted file mode 100644
index 4f2ae34f8..000000000
--- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlayerStatus.java
+++ /dev/null
@@ -1,33 +0,0 @@
-package de.danoeh.antennapod.core.service.playback;
-
-public enum PlayerStatus {
- INDETERMINATE(0), // player is currently changing its state, listeners should wait until the player has left this state.
- ERROR(-1),
- PREPARING(19),
- PAUSED(30),
- PLAYING(40),
- STOPPED(5),
- PREPARED(20),
- SEEKING(29),
- INITIALIZING(9), // playback service is loading the Playable's metadata
- INITIALIZED(10); // playback service was started, data source of media player was set.
-
- private final int statusValue;
- private static final PlayerStatus[] fromOrdinalLookup;
-
- static {
- fromOrdinalLookup = PlayerStatus.values();
- }
-
- PlayerStatus(int val) {
- statusValue = val;
- }
-
- public static PlayerStatus fromOrdinal(int o) {
- return fromOrdinalLookup[o];
- }
-
- public boolean isAtLeast(PlayerStatus other) {
- return other == null || this.statusValue>=other.statusValue;
- }
-}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/RewindAfterPauseUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/RewindAfterPauseUtils.java
deleted file mode 100644
index 813c6d0f7..000000000
--- a/core/src/main/java/de/danoeh/antennapod/core/util/RewindAfterPauseUtils.java
+++ /dev/null
@@ -1,47 +0,0 @@
-package de.danoeh.antennapod.core.util;
-
-import java.util.concurrent.TimeUnit;
-
-/**
- * This class calculates the proper rewind time after the pause and resume.
- * <p>
- * User might loose context if he/she pauses and resumes the media after longer time.
- * Media file should be "rewinded" x seconds after user resumes the playback.
- */
-public class RewindAfterPauseUtils {
- private RewindAfterPauseUtils(){}
-
- public static final long ELAPSED_TIME_FOR_SHORT_REWIND = TimeUnit.MINUTES.toMillis(1);
- public static final long ELAPSED_TIME_FOR_MEDIUM_REWIND = TimeUnit.HOURS.toMillis(1);
- public static final long ELAPSED_TIME_FOR_LONG_REWIND = TimeUnit.DAYS.toMillis(1);
-
- public static final long SHORT_REWIND = TimeUnit.SECONDS.toMillis(3);
- public static final long MEDIUM_REWIND = TimeUnit.SECONDS.toMillis(10);
- public static final long LONG_REWIND = TimeUnit.SECONDS.toMillis(20);
-
- /**
- * @param currentPosition current position in a media file in ms
- * @param lastPlayedTime timestamp when was media paused
- * @return new rewinded position for playback in milliseconds
- */
- public static int calculatePositionWithRewind(int currentPosition, long lastPlayedTime) {
- if (currentPosition > 0 && lastPlayedTime > 0) {
- long elapsedTime = System.currentTimeMillis() - lastPlayedTime;
- long rewindTime = 0;
-
- if (elapsedTime > ELAPSED_TIME_FOR_LONG_REWIND) {
- rewindTime = LONG_REWIND;
- } else if (elapsedTime > ELAPSED_TIME_FOR_MEDIUM_REWIND) {
- rewindTime = MEDIUM_REWIND;
- } else if (elapsedTime > ELAPSED_TIME_FOR_SHORT_REWIND) {
- rewindTime = SHORT_REWIND;
- }
-
- int newPosition = currentPosition - (int) rewindTime;
-
- return Math.max(newPosition, 0);
- } else {
- return currentPosition;
- }
- }
-}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java
index b436d80b2..549171c76 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java
@@ -22,9 +22,9 @@ import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils;
import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
-import de.danoeh.antennapod.core.service.playback.PlaybackServiceMediaPlayer;
-import de.danoeh.antennapod.core.service.playback.PlayerStatus;
import de.danoeh.antennapod.model.playback.Playable;
+import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer;
+import de.danoeh.antennapod.playback.base.PlayerStatus;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
diff --git a/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdater.java b/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdater.java
index 5275e7080..2762fb9fe 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdater.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdater.java
@@ -25,11 +25,11 @@ import de.danoeh.antennapod.model.playback.MediaType;
import de.danoeh.antennapod.core.glide.ApGlideSettings;
import de.danoeh.antennapod.core.receiver.MediaButtonReceiver;
import de.danoeh.antennapod.core.receiver.PlayerWidget;
-import de.danoeh.antennapod.core.service.playback.PlayerStatus;
import de.danoeh.antennapod.core.util.Converter;
import de.danoeh.antennapod.core.feed.util.ImageResourceUtils;
import de.danoeh.antennapod.core.util.TimeSpeedConverter;
import de.danoeh.antennapod.model.playback.Playable;
+import de.danoeh.antennapod.playback.base.PlayerStatus;
import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter;
import de.danoeh.antennapod.ui.appstartintent.VideoPlayerActivityStarter;
diff --git a/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdaterJobService.java b/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdaterJobService.java
index b14fb3b0b..325c508c5 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdaterJobService.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdaterJobService.java
@@ -6,9 +6,9 @@ import androidx.annotation.NonNull;
import androidx.core.app.SafeJobIntentService;
import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils;
import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
-import de.danoeh.antennapod.core.service.playback.PlayerStatus;
import de.danoeh.antennapod.model.playback.Playable;
import de.danoeh.antennapod.core.util.playback.PlayableUtils;
+import de.danoeh.antennapod.playback.base.PlayerStatus;
public class WidgetUpdaterJobService extends SafeJobIntentService {
private static final int JOB_ID = -17001;
diff --git a/core/src/main/res/values-land/dimens.xml b/core/src/main/res/values-land/dimens.xml
deleted file mode 100644
index 73b2b2e98..000000000
--- a/core/src/main/res/values-land/dimens.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources>
- <dimen name="media_router_controller_playback_control_start_padding">@dimen/media_router_controller_playback_control_horizontal_spacing</dimen>
-</resources>
diff --git a/core/src/main/res/values/dimens.xml b/core/src/main/res/values/dimens.xml
index d1e200d1d..4b2247492 100644
--- a/core/src/main/res/values/dimens.xml
+++ b/core/src/main/res/values/dimens.xml
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
-
<dimen name="widget_margin">0dp</dimen>
<dimen name="external_player_height">64dp</dimen>
<dimen name="text_size_micro">12sp</dimen>
@@ -28,11 +27,5 @@
<dimen name="audioplayer_playercontrols_length_big">64dp</dimen>
<dimen name="audioplayer_playercontrols_margin">12dp</dimen>
- <dimen name="media_router_controller_playback_control_vertical_padding">16dp</dimen>
- <dimen name="media_router_controller_playback_control_horizontal_spacing">12dp</dimen>
- <dimen name="media_router_controller_playback_control_start_padding">24dp</dimen>
- <dimen name="media_router_controller_bottom_margin">8dp</dimen>
-
<dimen name="nav_drawer_max_screen_size">480dp</dimen>
-
</resources>
diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml
index 1ab5b2184..59b335bc8 100644
--- a/core/src/main/res/values/strings.xml
+++ b/core/src/main/res/values/strings.xml
@@ -502,9 +502,6 @@
<string name="pref_proxy_title">Proxy</string>
<string name="pref_proxy_sum">Set a network proxy</string>
<string name="pref_no_browser_found">No web browser found.</string>
- <string name="pref_cast_title">Chromecast support</string>
- <string name="pref_cast_message_play_flavor">Enable support for remote media playback on Cast devices (such as Chromecast, Audio Speakers or Android TV)</string>
- <string name="pref_cast_message_free_flavor" tools:ignore="UnusedResources">Chromecast requires third party proprietary libraries that are disabled in this version of AntennaPod</string>
<string name="pref_enqueue_downloaded_title">Enqueue Downloaded</string>
<string name="pref_enqueue_downloaded_summary">Add downloaded episodes to the queue</string>
<string name="media_player_builtin">Built-in Android player (deprecated) </string>
@@ -664,7 +661,6 @@
<string name="pref_pausePlaybackForFocusLoss_title">Pause for Interruptions</string>
<string name="pref_resumeAfterCall_sum">Resume playback after a phone call completes</string>
<string name="pref_resumeAfterCall_title">Resume after Call</string>
- <string name="pref_restart_required">AntennaPod has to be restarted for this change to take effect.</string>
<!-- Online feed view -->
<string name="subscribe_label">Subscribe</string>
@@ -808,21 +804,6 @@
<!-- Subscriptions fragment -->
<string name="subscription_num_columns">Number of columns</string>
- <!-- Casting -->
- <string name="cast_media_route_menu_title">Play on&#8230;</string>
- <string name="cast_disconnect_label">Disconnect the cast session</string>
- <string name="cast_not_castable">Media selected is not compatible with cast device</string>
- <string name="cast_failed_to_play">Failed to start the playback of media</string>
- <string name="cast_failed_to_stop">Failed to stop the playback of media</string>
- <string name="cast_failed_to_pause">Failed to pause the playback of media</string>
- <string name="cast_failed_setting_volume">Failed to set the volume</string>
- <string name="cast_failed_no_connection">No connection to the cast device is present</string>
- <string name="cast_failed_no_connection_trans">Connection to the cast device has been lost. Application is trying to re-establish the connection, if possible. Please wait for a few seconds and try again.</string>
- <string name="cast_failed_status_request">Failed to sync up with the cast device</string>
- <string name="cast_failed_seek">Failed to seek to the new position on the cast device</string>
- <string name="cast_failed_receiver_player_error">Receiver player has encountered a severe error</string>
- <string name="cast_failed_media_error_skipping">Error playing media. Skipping&#8230;</string>
-
<!-- Notification channels -->
<string name="notification_group_errors">Errors</string>
<string name="notification_group_news">News</string>
diff --git a/core/src/play/java/de/danoeh/antennapod/core/CastCallbacks.java b/core/src/play/java/de/danoeh/antennapod/core/CastCallbacks.java
deleted file mode 100644
index 27f985a4c..000000000
--- a/core/src/play/java/de/danoeh/antennapod/core/CastCallbacks.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package de.danoeh.antennapod.core;
-
-import androidx.annotation.Nullable;
-import androidx.mediarouter.app.MediaRouteDialogFactory;
-
-/**
- * Callbacks for Chromecast support on the core module
- */
-public interface CastCallbacks {
-
- @Nullable MediaRouteDialogFactory getMediaRouterDialogFactory();
-}
diff --git a/core/src/play/java/de/danoeh/antennapod/core/ClientConfig.java b/core/src/play/java/de/danoeh/antennapod/core/ClientConfig.java
deleted file mode 100644
index 48de7c6e1..000000000
--- a/core/src/play/java/de/danoeh/antennapod/core/ClientConfig.java
+++ /dev/null
@@ -1,64 +0,0 @@
-package de.danoeh.antennapod.core;
-
-import android.content.Context;
-import android.util.Log;
-import de.danoeh.antennapod.core.cast.CastManager;
-import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
-import de.danoeh.antennapod.core.preferences.SleepTimerPreferences;
-import de.danoeh.antennapod.core.preferences.UsageStatistics;
-import de.danoeh.antennapod.core.preferences.UserPreferences;
-import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
-import de.danoeh.antennapod.core.storage.PodDBAdapter;
-import de.danoeh.antennapod.core.util.NetworkUtils;
-import de.danoeh.antennapod.core.util.gui.NotificationUtils;
-import de.danoeh.antennapod.net.ssl.SslProviderInstaller;
-
-import java.io.File;
-
-/**
- * Stores callbacks for core classes like Services, DB classes etc. and other configuration variables.
- * Apps using the core module of AntennaPod should register implementations of all interfaces here.
- */
-public class ClientConfig {
- private static final String TAG = "ClientConfig";
-
- private ClientConfig(){}
-
- /**
- * Should be used when setting User-Agent header for HTTP-requests.
- */
- public static String USER_AGENT;
-
- public static ApplicationCallbacks applicationCallbacks;
-
- public static DownloadServiceCallbacks downloadServiceCallbacks;
-
- public static CastCallbacks castCallbacks;
-
- private static boolean initialized = false;
-
- public static synchronized void initialize(Context context) {
- if (initialized) {
- return;
- }
- PodDBAdapter.init(context);
- UserPreferences.init(context);
- UsageStatistics.init(context);
- PlaybackPreferences.init(context);
- SslProviderInstaller.install(context);
- NetworkUtils.init(context);
- // Don't initialize Cast-related logic unless it is enabled, to avoid the unnecessary
- // Google Play Service usage.
- // Down side: when the user decides to enable casting, AntennaPod needs to be restarted
- // for it to take effect.
- if (UserPreferences.isCastEnabled()) {
- CastManager.init(context);
- } else {
- Log.v(TAG, "Cast is disabled. All Cast-related initialization will be skipped.");
- }
- AntennapodHttpClient.setCacheDirectory(new File(context.getCacheDir(), "okhttp"));
- SleepTimerPreferences.init(context);
- NotificationUtils.createChannels(context);
- initialized = true;
- }
-}
diff --git a/core/src/play/java/de/danoeh/antennapod/core/cast/CastButtonVisibilityManager.java b/core/src/play/java/de/danoeh/antennapod/core/cast/CastButtonVisibilityManager.java
deleted file mode 100644
index 8d0e40116..000000000
--- a/core/src/play/java/de/danoeh/antennapod/core/cast/CastButtonVisibilityManager.java
+++ /dev/null
@@ -1,120 +0,0 @@
-package de.danoeh.antennapod.core.cast;
-
-import android.util.Log;
-import android.view.Menu;
-import android.view.MenuItem;
-
-import de.danoeh.antennapod.core.R;
-
-public class CastButtonVisibilityManager {
- private static final String TAG = "CastBtnVisibilityMgr";
- private final CastManager castManager;
- private volatile boolean prefEnabled = false;
- private volatile boolean viewRequested = false;
- private volatile boolean resumed = false;
- private volatile boolean connected = false;
- private volatile int showAsAction = MenuItem.SHOW_AS_ACTION_IF_ROOM;
- private Menu menu;
- public SwitchableMediaRouteActionProvider mediaRouteActionProvider;
-
- public CastButtonVisibilityManager(CastManager castManager) {
- this.castManager = castManager;
- }
-
- public synchronized void setPrefEnabled(boolean newValue) {
- if (prefEnabled != newValue && resumed && (viewRequested || connected)) {
- if (newValue) {
- castManager.incrementUiCounter();
- } else {
- castManager.decrementUiCounter();
- }
- }
- prefEnabled = newValue;
- if (mediaRouteActionProvider != null) {
- mediaRouteActionProvider.setEnabled(prefEnabled && (viewRequested || connected));
- }
- }
-
- public synchronized void setResumed(boolean newValue) {
- if (resumed == newValue) {
- Log.e(TAG, "resumed should never change to the same value");
- return;
- }
- resumed = newValue;
- if (prefEnabled && (viewRequested || connected)) {
- if (resumed) {
- castManager.incrementUiCounter();
- } else {
- castManager.decrementUiCounter();
- }
- }
- }
-
- public synchronized void setViewRequested(boolean newValue) {
- if (viewRequested != newValue && resumed && prefEnabled && !connected) {
- if (newValue) {
- castManager.incrementUiCounter();
- } else {
- castManager.decrementUiCounter();
- }
- }
- viewRequested = newValue;
- if (mediaRouteActionProvider != null) {
- mediaRouteActionProvider.setEnabled(prefEnabled && (viewRequested || connected));
- }
- }
-
- public synchronized void setConnected(boolean newValue) {
- if (connected != newValue && resumed && prefEnabled && !prefEnabled) {
- if (newValue) {
- castManager.incrementUiCounter();
- } else {
- castManager.decrementUiCounter();
- }
- }
- connected = newValue;
- if (mediaRouteActionProvider != null) {
- mediaRouteActionProvider.setEnabled(prefEnabled && (viewRequested || connected));
- }
- }
-
- public synchronized boolean shouldEnable() {
- return prefEnabled && viewRequested;
- }
-
- public void setMenu(Menu menu) {
- setViewRequested(false);
- showAsAction = MenuItem.SHOW_AS_ACTION_IF_ROOM;
- this.menu = menu;
- setShowAsAction();
- }
-
- public void requestCastButton(int showAsAction) {
- setViewRequested(true);
- this.showAsAction = showAsAction;
- setShowAsAction();
- }
-
- public void onConnected() {
- setConnected(true);
- setShowAsAction();
- }
-
- public void onDisconnected() {
- setConnected(false);
- setShowAsAction();
- }
-
- private void setShowAsAction() {
- if (menu == null) {
- Log.d(TAG, "setShowAsAction() without a menu");
- return;
- }
- MenuItem item = menu.findItem(R.id.media_route_menu_item);
- if (item == null) {
- Log.e(TAG, "setShowAsAction(), but cast button not inflated");
- return;
- }
- item.setShowAsAction(connected ? MenuItem.SHOW_AS_ACTION_ALWAYS : showAsAction);
- }
-}
diff --git a/core/src/play/java/de/danoeh/antennapod/core/cast/CastConsumer.java b/core/src/play/java/de/danoeh/antennapod/core/cast/CastConsumer.java
deleted file mode 100644
index 213dd1875..000000000
--- a/core/src/play/java/de/danoeh/antennapod/core/cast/CastConsumer.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package de.danoeh.antennapod.core.cast;
-
-import com.google.android.libraries.cast.companionlibrary.cast.callbacks.VideoCastConsumer;
-
-public interface CastConsumer extends VideoCastConsumer{
-
- /**
- * Called when the stream's volume is changed.
- */
- void onStreamVolumeChanged(double value, boolean isMute);
-}
diff --git a/core/src/play/java/de/danoeh/antennapod/core/cast/CastManager.java b/core/src/play/java/de/danoeh/antennapod/core/cast/CastManager.java
deleted file mode 100644
index dd07b9cd8..000000000
--- a/core/src/play/java/de/danoeh/antennapod/core/cast/CastManager.java
+++ /dev/null
@@ -1,1091 +0,0 @@
-/*
- * Copyright (C) 2015 Google Inc. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- * ------------------------------------------------------------------------
- *
- * Changes made by Domingos Lopes <domingos86lopes@gmail.com>
- *
- * original can be found at http://www.github.com/googlecast/CastCompanionLibrary-android
- */
-
-package de.danoeh.antennapod.core.cast;
-
-import android.content.Context;
-import androidx.annotation.NonNull;
-import androidx.core.view.ActionProvider;
-import androidx.core.view.MenuItemCompat;
-import androidx.mediarouter.media.MediaRouter;
-import android.util.Log;
-import android.view.MenuItem;
-
-import com.google.android.gms.cast.ApplicationMetadata;
-import com.google.android.gms.cast.Cast;
-import com.google.android.gms.cast.CastDevice;
-import com.google.android.gms.cast.CastMediaControlIntent;
-import com.google.android.gms.cast.CastStatusCodes;
-import com.google.android.gms.cast.MediaInfo;
-import com.google.android.gms.cast.MediaMetadata;
-import com.google.android.gms.cast.MediaQueueItem;
-import com.google.android.gms.cast.MediaStatus;
-import com.google.android.gms.cast.RemoteMediaPlayer;
-import com.google.android.gms.common.ConnectionResult;
-import com.google.android.gms.common.GoogleApiAvailability;
-import com.google.android.libraries.cast.companionlibrary.cast.BaseCastManager;
-import com.google.android.libraries.cast.companionlibrary.cast.CastConfiguration;
-import com.google.android.libraries.cast.companionlibrary.cast.exceptions.CastException;
-import com.google.android.libraries.cast.companionlibrary.cast.exceptions.NoConnectionException;
-import com.google.android.libraries.cast.companionlibrary.cast.exceptions.OnFailedListener;
-import com.google.android.libraries.cast.companionlibrary.cast.exceptions.TransientNetworkDisconnectionException;
-
-import org.json.JSONObject;
-
-import java.io.IOException;
-import java.util.List;
-import java.util.Locale;
-import java.util.Set;
-import java.util.concurrent.CopyOnWriteArraySet;
-
-import de.danoeh.antennapod.core.ClientConfig;
-import de.danoeh.antennapod.core.R;
-
-import static com.google.android.gms.cast.RemoteMediaPlayer.RESUME_STATE_PLAY;
-import static com.google.android.gms.cast.RemoteMediaPlayer.RESUME_STATE_UNCHANGED;
-
-/**
- * A subclass of {@link BaseCastManager} that is suitable for casting video contents (it
- * also provides a single custom data channel/namespace if an out-of-band communication is
- * needed).
- * <p>
- * Clients need to initialize this class by calling
- * {@link #init(android.content.Context)} in the Application's
- * {@code onCreate()} method. To access the (singleton) instance of this class, clients
- * need to call {@link #getInstance()}.
- * <p>This
- * class manages various states of the remote cast device. Client applications, however, can
- * complement the default behavior of this class by hooking into various callbacks that it provides
- * (see {@link CastConsumer}).
- * Since the number of these callbacks is usually much larger than what a single application might
- * be interested in, there is a no-op implementation of this interface (see
- * {@link DefaultCastConsumer}) that applications can subclass to override only those methods that
- * they are interested in. Since this library depends on the cast functionalities provided by the
- * Google Play services, the library checks to ensure that the right version of that service is
- * installed. It also provides a simple static method {@code checkGooglePlayServices()} that clients
- * can call at an early stage of their applications to provide a dialog for users if they need to
- * update/activate their Google Play Services library.
- *
- * @see CastConfiguration
- */
-public class CastManager extends BaseCastManager implements OnFailedListener {
- public static final String TAG = "CastManager";
-
- public static final String CAST_APP_ID = CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID;
-
- private MediaStatus mediaStatus;
- private static CastManager INSTANCE;
- private RemoteMediaPlayer remoteMediaPlayer;
- private int state = MediaStatus.PLAYER_STATE_IDLE;
- private final Set<CastConsumer> castConsumers = new CopyOnWriteArraySet<>();
-
- public static final int QUEUE_OPERATION_LOAD = 1;
- public static final int QUEUE_OPERATION_APPEND = 9;
-
- private CastManager(Context context, CastConfiguration castConfiguration) {
- super(context, castConfiguration);
- Log.d(TAG, "CastManager is instantiated");
- }
-
- public static synchronized CastManager init(Context context) {
- if (INSTANCE == null) {
- CastConfiguration castConfiguration = new CastConfiguration.Builder(CAST_APP_ID)
- .enableDebug()
- .enableAutoReconnect()
- .enableWifiReconnection()
- .setLaunchOptions(true, Locale.getDefault())
- .setMediaRouteDialogFactory(ClientConfig.castCallbacks.getMediaRouterDialogFactory())
- .build();
- Log.d(TAG, "New instance of CastManager is created");
- if (ConnectionResult.SUCCESS != GoogleApiAvailability.getInstance()
- .isGooglePlayServicesAvailable(context)) {
- Log.e(TAG, "Couldn't find the appropriate version of Google Play Services");
- }
- INSTANCE = new CastManager(context, castConfiguration);
- }
- return INSTANCE;
- }
-
- /**
- * Returns a (singleton) instance of this class. Clients should call this method in order to
- * get a hold of this singleton instance, only after it is initialized. If it is not initialized
- * yet, an {@link IllegalStateException} will be thrown.
- *
- */
- public static CastManager getInstance() {
- if (INSTANCE == null) {
- String msg = "No CastManager instance was found, did you forget to initialize it?";
- Log.e(TAG, msg);
- throw new IllegalStateException(msg);
- }
- return INSTANCE;
- }
-
- public static boolean isInitialized() {
- return INSTANCE != null;
- }
-
- /**
- * Returns the active {@link RemoteMediaPlayer} instance. Since there are a number of media
- * control APIs that this library do not provide a wrapper for, client applications can call
- * those methods directly after obtaining an instance of the active {@link RemoteMediaPlayer}.
- */
- public final RemoteMediaPlayer getRemoteMediaPlayer() {
- return remoteMediaPlayer;
- }
-
- /*
- * A simple check to make sure remoteMediaPlayer is not null
- */
- private void checkRemoteMediaPlayerAvailable() throws NoConnectionException {
- if (remoteMediaPlayer == null) {
- throw new NoConnectionException();
- }
- }
-
- /**
- * Indicates if the remote media is currently playing (or buffering).
- *
- * @throws NoConnectionException
- * @throws TransientNetworkDisconnectionException
- */
- public boolean isRemoteMediaPlaying() throws TransientNetworkDisconnectionException,
- NoConnectionException {
- checkConnectivity();
- return state == MediaStatus.PLAYER_STATE_BUFFERING
- || state == MediaStatus.PLAYER_STATE_PLAYING;
- }
-
- /**
- * Returns <code>true</code> if the remote connected device is playing a movie.
- *
- * @throws NoConnectionException
- * @throws TransientNetworkDisconnectionException
- */
- public boolean isRemoteMediaPaused() throws TransientNetworkDisconnectionException,
- NoConnectionException {
- checkConnectivity();
- return state == MediaStatus.PLAYER_STATE_PAUSED;
- }
-
- /**
- * Returns <code>true</code> only if there is a media on the remote being played, paused or
- * buffered.
- *
- * @throws NoConnectionException
- * @throws TransientNetworkDisconnectionException
- */
- public boolean isRemoteMediaLoaded() throws TransientNetworkDisconnectionException,
- NoConnectionException {
- checkConnectivity();
- return isRemoteMediaPaused() || isRemoteMediaPlaying();
- }
-
- /**
- * Gets the remote's system volume. It internally detects what type of volume is used.
- *
- * @throws NoConnectionException If no connectivity to the device exists
- * @throws TransientNetworkDisconnectionException If framework is still trying to recover from
- * a possibly transient loss of network
- */
- public double getStreamVolume() throws TransientNetworkDisconnectionException, NoConnectionException {
- checkConnectivity();
- checkRemoteMediaPlayerAvailable();
- return remoteMediaPlayer.getMediaStatus().getStreamVolume();
- }
-
- /**
- * Sets the stream volume.
- *
- * @param volume Should be a value between 0 and 1, inclusive.
- * @throws NoConnectionException
- * @throws TransientNetworkDisconnectionException
- * @throws CastException If setting system volume fails
- */
- public void setStreamVolume(double volume) throws CastException,
- TransientNetworkDisconnectionException, NoConnectionException {
- checkConnectivity();
- if (volume > 1.0) {
- volume = 1.0;
- } else if (volume < 0) {
- volume = 0.0;
- }
-
- RemoteMediaPlayer mediaPlayer = getRemoteMediaPlayer();
- if (mediaPlayer == null) {
- throw new NoConnectionException();
- }
- mediaPlayer.setStreamVolume(mApiClient, volume).setResultCallback(
- (result) -> {
- if (!result.getStatus().isSuccess()) {
- onFailed(R.string.cast_failed_setting_volume,
- result.getStatus().getStatusCode());
- } else {
- CastManager.this.onStreamVolumeChanged();
- }
- });
- }
-
- /**
- * Returns <code>true</code> if remote Stream is muted.
- *
- * @throws NoConnectionException
- * @throws TransientNetworkDisconnectionException
- */
- public boolean isStreamMute() throws TransientNetworkDisconnectionException, NoConnectionException {
- checkConnectivity();
- checkRemoteMediaPlayerAvailable();
- return remoteMediaPlayer.getMediaStatus().isMute();
- }
-
- /**
- * Returns the duration of the media that is loaded, in milliseconds.
- *
- * @throws NoConnectionException
- * @throws TransientNetworkDisconnectionException
- */
- public long getMediaDuration() throws TransientNetworkDisconnectionException,
- NoConnectionException {
- checkConnectivity();
- checkRemoteMediaPlayerAvailable();
- return remoteMediaPlayer.getStreamDuration();
- }
-
- /**
- * Returns the current (approximate) position of the current media, in milliseconds.
- *
- * @throws NoConnectionException
- * @throws TransientNetworkDisconnectionException
- */
- public long getCurrentMediaPosition() throws TransientNetworkDisconnectionException,
- NoConnectionException {
- checkConnectivity();
- checkRemoteMediaPlayerAvailable();
- return remoteMediaPlayer.getApproximateStreamPosition();
- }
-
- public int getApplicationStandbyState() throws IllegalStateException {
- Log.d(TAG, "getApplicationStandbyState()");
- return Cast.CastApi.getStandbyState(mApiClient);
- }
-
- private void onApplicationDisconnected(int errorCode) {
- Log.d(TAG, "onApplicationDisconnected() reached with error code: " + errorCode);
- mApplicationErrorCode = errorCode;
- for (CastConsumer consumer : castConsumers) {
- consumer.onApplicationDisconnected(errorCode);
- }
- if (mMediaRouter != null) {
- Log.d(TAG, "onApplicationDisconnected(): Cached RouteInfo: " + getRouteInfo());
- Log.d(TAG, "onApplicationDisconnected(): Selected RouteInfo: "
- + mMediaRouter.getSelectedRoute());
- if (getRouteInfo() == null || mMediaRouter.getSelectedRoute().equals(getRouteInfo())) {
- Log.d(TAG, "onApplicationDisconnected(): Setting route to default");
- mMediaRouter.selectRoute(mMediaRouter.getDefaultRoute());
- }
- }
- onDeviceSelected(null /* CastDevice */, null /* RouteInfo */);
- }
-
- private void onApplicationStatusChanged() {
- if (!isConnected()) {
- return;
- }
- try {
- String appStatus = Cast.CastApi.getApplicationStatus(mApiClient);
- Log.d(TAG, "onApplicationStatusChanged() reached: " + appStatus);
- for (CastConsumer consumer : castConsumers) {
- consumer.onApplicationStatusChanged(appStatus);
- }
- } catch (IllegalStateException e) {
- Log.e(TAG, "onApplicationStatusChanged()", e);
- }
- }
-
- private void onDeviceVolumeChanged() {
- Log.d(TAG, "onDeviceVolumeChanged() reached");
- double volume;
- try {
- volume = getDeviceVolume();
- boolean isMute = isDeviceMute();
- for (CastConsumer consumer : castConsumers) {
- consumer.onVolumeChanged(volume, isMute);
- }
- } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
- Log.e(TAG, "Failed to get volume", e);
- }
-
- }
-
- private void onStreamVolumeChanged() {
- Log.d(TAG, "onStreamVolumeChanged() reached");
- double volume;
- try {
- volume = getStreamVolume();
- boolean isMute = isStreamMute();
- for (CastConsumer consumer : castConsumers) {
- consumer.onStreamVolumeChanged(volume, isMute);
- }
- } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
- Log.e(TAG, "Failed to get volume", e);
- }
- }
-
- @Override
- protected void onApplicationConnected(ApplicationMetadata appMetadata,
- String applicationStatus, String sessionId, boolean wasLaunched) {
- Log.d(TAG, "onApplicationConnected() reached with sessionId: " + sessionId
- + ", and mReconnectionStatus=" + mReconnectionStatus);
- mApplicationErrorCode = NO_APPLICATION_ERROR;
- if (mReconnectionStatus == RECONNECTION_STATUS_IN_PROGRESS) {
- // we have tried to reconnect and successfully launched the app, so
- // it is time to select the route and make the cast icon happy :-)
- List<MediaRouter.RouteInfo> routes = mMediaRouter.getRoutes();
- if (routes != null) {
- String routeId = mPreferenceAccessor.getStringFromPreference(PREFS_KEY_ROUTE_ID);
- for (MediaRouter.RouteInfo routeInfo : routes) {
- if (routeId.equals(routeInfo.getId())) {
- // found the right route
- Log.d(TAG, "Found the correct route during reconnection attempt");
- mReconnectionStatus = RECONNECTION_STATUS_FINALIZED;
- mMediaRouter.selectRoute(routeInfo);
- break;
- }
- }
- }
- }
- try {
- //attachDataChannel();
- attachMediaChannel();
- mSessionId = sessionId;
- // saving device for future retrieval; we only save the last session info
- mPreferenceAccessor.saveStringToPreference(PREFS_KEY_SESSION_ID, mSessionId);
- remoteMediaPlayer.requestStatus(mApiClient).setResultCallback(result -> {
- if (!result.getStatus().isSuccess()) {
- onFailed(R.string.cast_failed_status_request,
- result.getStatus().getStatusCode());
- }
- });
- for (CastConsumer consumer : castConsumers) {
- consumer.onApplicationConnected(appMetadata, mSessionId, wasLaunched);
- }
- } catch (TransientNetworkDisconnectionException e) {
- Log.e(TAG, "Failed to attach media/data channel due to network issues", e);
- onFailed(R.string.cast_failed_no_connection_trans, NO_STATUS_CODE);
- } catch (NoConnectionException e) {
- Log.e(TAG, "Failed to attach media/data channel due to network issues", e);
- onFailed(R.string.cast_failed_no_connection, NO_STATUS_CODE);
- }
- }
-
- /*
- * (non-Javadoc)
- * @see com.google.android.libraries.cast.companionlibrary.cast.BaseCastManager
- * #onConnectivityRecovered()
- */
- @Override
- public void onConnectivityRecovered() {
- reattachMediaChannel();
- //reattachDataChannel();
- super.onConnectivityRecovered();
- }
-
- /*
- * (non-Javadoc)
- * @see com.google.android.gms.cast.CastClient.Listener#onApplicationStopFailed (int)
- */
- @Override
- public void onApplicationStopFailed(int errorCode) {
- for (CastConsumer consumer : castConsumers) {
- consumer.onApplicationStopFailed(errorCode);
- }
- }
-
- @Override
- public void onApplicationConnectionFailed(int errorCode) {
- Log.d(TAG, "onApplicationConnectionFailed() reached with errorCode: " + errorCode);
- mApplicationErrorCode = errorCode;
- if (mReconnectionStatus == RECONNECTION_STATUS_IN_PROGRESS) {
- if (errorCode == CastStatusCodes.APPLICATION_NOT_RUNNING) {
- // while trying to re-establish session, we found out that the app is not running
- // so we need to disconnect
- mReconnectionStatus = RECONNECTION_STATUS_INACTIVE;
- onDeviceSelected(null /* CastDevice */, null /* RouteInfo */);
- }
- } else {
- for (CastConsumer consumer : castConsumers) {
- consumer.onApplicationConnectionFailed(errorCode);
- }
- onDeviceSelected(null /* CastDevice */, null /* RouteInfo */);
- if (mMediaRouter != null) {
- Log.d(TAG, "onApplicationConnectionFailed(): Setting route to default");
- mMediaRouter.selectRoute(mMediaRouter.getDefaultRoute());
- }
- }
- }
-
- /**
- * Loads a media. For this to succeed, you need to have successfully launched the application.
- *
- * @param media The media to be loaded
- * @param autoPlay If <code>true</code>, playback starts after load
- * @param position Where to start the playback (only used if autoPlay is <code>true</code>.
- * Units is milliseconds.
- * @throws NoConnectionException
- * @throws TransientNetworkDisconnectionException
- */
- public void loadMedia(MediaInfo media, boolean autoPlay, int position)
- throws TransientNetworkDisconnectionException, NoConnectionException {
- loadMedia(media, autoPlay, position, null);
- }
-
- /**
- * Loads a media. For this to succeed, you need to have successfully launched the application.
- *
- * @param media The media to be loaded
- * @param autoPlay If <code>true</code>, playback starts after load
- * @param position Where to start the playback (only used if autoPlay is <code>true</code>).
- * Units is milliseconds.
- * @param customData Optional {@link JSONObject} data to be passed to the cast device
- * @throws NoConnectionException
- * @throws TransientNetworkDisconnectionException
- */
- public void loadMedia(MediaInfo media, boolean autoPlay, int position, JSONObject customData)
- throws TransientNetworkDisconnectionException, NoConnectionException {
- loadMedia(media, null, autoPlay, position, customData);
- }
-
- /**
- * Loads a media. For this to succeed, you need to have successfully launched the application.
- *
- * @param media The media to be loaded
- * @param activeTracks An array containing the list of track IDs to be set active for this
- * media upon a successful load
- * @param autoPlay If <code>true</code>, playback starts after load
- * @param position Where to start the playback (only used if autoPlay is <code>true</code>).
- * Units is milliseconds.
- * @param customData Optional {@link JSONObject} data to be passed to the cast device
- * @throws NoConnectionException
- * @throws TransientNetworkDisconnectionException
- */
- public void loadMedia(MediaInfo media, final long[] activeTracks, boolean autoPlay,
- int position, JSONObject customData)
- throws TransientNetworkDisconnectionException, NoConnectionException {
- Log.d(TAG, "loadMedia");
- checkConnectivity();
- if (media == null) {
- return;
- }
- if (remoteMediaPlayer == null) {
- Log.e(TAG, "Trying to load a video with no active media session");
- throw new NoConnectionException();
- }
-
- Log.d(TAG, "remoteMediaPlayer.load() with media=" + media.getMetadata().getString(MediaMetadata.KEY_TITLE)
- + ", position=" + position + ", autoplay=" + autoPlay);
- remoteMediaPlayer.load(mApiClient, media, autoPlay, position, activeTracks, customData)
- .setResultCallback(result -> {
- for (CastConsumer consumer : castConsumers) {
- consumer.onMediaLoadResult(result.getStatus().getStatusCode());
- }
- });
- }
-
- /**
- * Loads and optionally starts playback of a new queue of media items.
- *
- * @param items Array of items to load, in the order that they should be played. Must not be
- * {@code null} or empty.
- * @param startIndex The array index of the item in the {@code items} array that should be
- * played first (i.e., it will become the currentItem).If {@code repeatMode}
- * is {@link MediaStatus#REPEAT_MODE_REPEAT_OFF} playback will end when the
- * last item in the array is played.
- * <p>
- * This may be useful for continuation scenarios where the user was already
- * using the sender application and in the middle decides to cast. This lets
- * the sender application avoid mapping between the local and remote queue
- * positions and/or avoid issuing an extra request to update the queue.
- * <p>
- * This value must be less than the length of {@code items}.
- * @param repeatMode The repeat playback mode for the queue. One of
- * {@link MediaStatus#REPEAT_MODE_REPEAT_OFF},
- * {@link MediaStatus#REPEAT_MODE_REPEAT_ALL},
- * {@link MediaStatus#REPEAT_MODE_REPEAT_SINGLE} and
- * {@link MediaStatus#REPEAT_MODE_REPEAT_ALL_AND_SHUFFLE}.
- * @param customData Custom application-specific data to pass along with the request, may be
- * {@code null}.
- * @throws TransientNetworkDisconnectionException
- * @throws NoConnectionException
- */
- public void queueLoad(final MediaQueueItem[] items, final int startIndex, final int repeatMode,
- final JSONObject customData)
- throws TransientNetworkDisconnectionException, NoConnectionException {
- Log.d(TAG, "queueLoad");
- checkConnectivity();
- if (items == null || items.length == 0) {
- return;
- }
- if (remoteMediaPlayer == null) {
- Log.e(TAG, "Trying to queue one or more videos with no active media session");
- throw new NoConnectionException();
- }
- Log.d(TAG, "remoteMediaPlayer.queueLoad() with " + items.length + "items, starting at "
- + startIndex);
- remoteMediaPlayer
- .queueLoad(mApiClient, items, startIndex, repeatMode, customData)
- .setResultCallback(result -> {
- for (CastConsumer consumer : castConsumers) {
- consumer.onMediaQueueOperationResult(QUEUE_OPERATION_LOAD,
- result.getStatus().getStatusCode());
- }
- });
- }
-
- /**
- * Plays the loaded media.
- *
- * @param position Where to start the playback. Units is milliseconds.
- * @throws NoConnectionException
- * @throws TransientNetworkDisconnectionException
- */
- public void play(int position) throws TransientNetworkDisconnectionException,
- NoConnectionException {
- checkConnectivity();
- Log.d(TAG, "attempting to play media at position " + position + " seconds");
- if (remoteMediaPlayer == null) {
- Log.e(TAG, "Trying to play a video with no active media session");
- throw new NoConnectionException();
- }
- seekAndPlay(position);
- }
-
- /**
- * Resumes the playback from where it was left (can be the beginning).
- *
- * @param customData Optional {@link JSONObject} data to be passed to the cast device
- * @throws NoConnectionException
- * @throws TransientNetworkDisconnectionException
- */
- public void play(JSONObject customData) throws
- TransientNetworkDisconnectionException, NoConnectionException {
- Log.d(TAG, "play(customData)");
- checkConnectivity();
- if (remoteMediaPlayer == null) {
- Log.e(TAG, "Trying to play a video with no active media session");
- throw new NoConnectionException();
- }
- remoteMediaPlayer.play(mApiClient, customData)
- .setResultCallback(result -> {
- if (!result.getStatus().isSuccess()) {
- onFailed(R.string.cast_failed_to_play,
- result.getStatus().getStatusCode());
- }
- });
- }
-
- /**
- * Resumes the playback from where it was left (can be the beginning).
- *
- * @throws CastException
- * @throws NoConnectionException
- * @throws TransientNetworkDisconnectionException
- */
- public void play() throws CastException, TransientNetworkDisconnectionException,
- NoConnectionException {
- play(null);
- }
-
- /**
- * Stops the playback of media/stream
- *
- * @param customData Optional {@link JSONObject}
- * @throws TransientNetworkDisconnectionException
- * @throws NoConnectionException
- */
- public void stop(JSONObject customData) throws
- TransientNetworkDisconnectionException, NoConnectionException {
- Log.d(TAG, "stop()");
- checkConnectivity();
- if (remoteMediaPlayer == null) {
- Log.e(TAG, "Trying to stop a stream with no active media session");
- throw new NoConnectionException();
- }
- remoteMediaPlayer.stop(mApiClient, customData).setResultCallback(
- result -> {
- if (!result.getStatus().isSuccess()) {
- onFailed(R.string.cast_failed_to_stop,
- result.getStatus().getStatusCode());
- }
- }
- );
- }
-
- /**
- * Stops the playback of media/stream
- *
- * @throws CastException
- * @throws TransientNetworkDisconnectionException
- * @throws NoConnectionException
- */
- public void stop() throws CastException,
- TransientNetworkDisconnectionException, NoConnectionException {
- stop(null);
- }
-
- /**
- * Pauses the playback.
- *
- * @throws CastException
- * @throws NoConnectionException
- * @throws TransientNetworkDisconnectionException
- */
- public void pause() throws CastException, TransientNetworkDisconnectionException,
- NoConnectionException {
- pause(null);
- }
-
- /**
- * Pauses the playback.
- *
- * @param customData Optional {@link JSONObject} data to be passed to the cast device
- * @throws NoConnectionException
- * @throws TransientNetworkDisconnectionException
- */
- public void pause(JSONObject customData) throws
- TransientNetworkDisconnectionException, NoConnectionException {
- Log.d(TAG, "attempting to pause media");
- checkConnectivity();
- if (remoteMediaPlayer == null) {
- Log.e(TAG, "Trying to pause a video with no active media session");
- throw new NoConnectionException();
- }
- remoteMediaPlayer.pause(mApiClient, customData)
- .setResultCallback(result -> {
- if (!result.getStatus().isSuccess()) {
- onFailed(R.string.cast_failed_to_pause,
- result.getStatus().getStatusCode());
- }
- });
- }
-
- /**
- * Seeks to the given point without changing the state of the player, i.e. after seek is
- * completed, it resumes what it was doing before the start of seek.
- *
- * @param position in milliseconds
- * @throws NoConnectionException
- * @throws TransientNetworkDisconnectionException
- */
- public void seek(int position) throws TransientNetworkDisconnectionException,
- NoConnectionException {
- Log.d(TAG, "attempting to seek media");
- checkConnectivity();
- if (remoteMediaPlayer == null) {
- Log.e(TAG, "Trying to seek a video with no active media session");
- throw new NoConnectionException();
- }
- Log.d(TAG, "remoteMediaPlayer.seek() to position " + position);
- remoteMediaPlayer.seek(mApiClient, position, RESUME_STATE_UNCHANGED).setResultCallback(result -> {
- if (!result.getStatus().isSuccess()) {
- onFailed(R.string.cast_failed_seek, result.getStatus().getStatusCode());
- }
- });
- }
-
- /**
- * Fast forwards the media by the given amount. If {@code lengthInMillis} is negative, it
- * rewinds the media.
- *
- * @param lengthInMillis The amount to fast forward the media, given in milliseconds
- * @throws TransientNetworkDisconnectionException
- * @throws NoConnectionException
- */
- public void forward(int lengthInMillis) throws TransientNetworkDisconnectionException,
- NoConnectionException {
- Log.d(TAG, "forward(): attempting to forward media by " + lengthInMillis);
- checkConnectivity();
- if (remoteMediaPlayer == null) {
- Log.e(TAG, "Trying to seek a video with no active media session");
- throw new NoConnectionException();
- }
- long position = remoteMediaPlayer.getApproximateStreamPosition() + lengthInMillis;
- seek((int) position);
- }
-
- /**
- * Seeks to the given point and starts playback regardless of the starting state.
- *
- * @param position in milliseconds
- * @throws NoConnectionException
- * @throws TransientNetworkDisconnectionException
- */
- public void seekAndPlay(int position) throws TransientNetworkDisconnectionException,
- NoConnectionException {
- Log.d(TAG, "attempting to seek media");
- checkConnectivity();
- if (remoteMediaPlayer == null) {
- Log.e(TAG, "Trying to seekAndPlay a video with no active media session");
- throw new NoConnectionException();
- }
- Log.d(TAG, "remoteMediaPlayer.seek() to position " + position + "and play");
- remoteMediaPlayer.seek(mApiClient, position, RESUME_STATE_PLAY)
- .setResultCallback(result -> {
- if (!result.getStatus().isSuccess()) {
- onFailed(R.string.cast_failed_seek, result.getStatus().getStatusCode());
- }
- });
- }
-
- private void attachMediaChannel() throws TransientNetworkDisconnectionException,
- NoConnectionException {
- Log.d(TAG, "attachMediaChannel()");
- checkConnectivity();
- if (remoteMediaPlayer == null) {
- remoteMediaPlayer = new RemoteMediaPlayer();
-
- remoteMediaPlayer.setOnStatusUpdatedListener(
- () -> {
- Log.d(TAG, "RemoteMediaPlayer::onStatusUpdated() is reached");
- CastManager.this.onRemoteMediaPlayerStatusUpdated();
- }
- );
-
- remoteMediaPlayer.setOnPreloadStatusUpdatedListener(
- () -> {
- Log.d(TAG, "RemoteMediaPlayer::onPreloadStatusUpdated() is reached");
- CastManager.this.onRemoteMediaPreloadStatusUpdated();
- });
-
-
- remoteMediaPlayer.setOnMetadataUpdatedListener(
- () -> {
- Log.d(TAG, "RemoteMediaPlayer::onMetadataUpdated() is reached");
- CastManager.this.onRemoteMediaPlayerMetadataUpdated();
- }
- );
-
- remoteMediaPlayer.setOnQueueStatusUpdatedListener(
- () -> {
- Log.d(TAG, "RemoteMediaPlayer::onQueueStatusUpdated() is reached");
- mediaStatus = remoteMediaPlayer.getMediaStatus();
- if (mediaStatus != null
- && mediaStatus.getQueueItems() != null) {
- List<MediaQueueItem> queueItems = mediaStatus
- .getQueueItems();
- int itemId = mediaStatus.getCurrentItemId();
- MediaQueueItem item = mediaStatus
- .getQueueItemById(itemId);
- int repeatMode = mediaStatus.getQueueRepeatMode();
- onQueueUpdated(queueItems, item, repeatMode, false);
- } else {
- onQueueUpdated(null, null,
- MediaStatus.REPEAT_MODE_REPEAT_OFF, false);
- }
- });
-
- }
- try {
- Log.d(TAG, "Registering MediaChannel namespace");
- Cast.CastApi.setMessageReceivedCallbacks(mApiClient, remoteMediaPlayer.getNamespace(),
- remoteMediaPlayer);
- } catch (IOException | IllegalStateException e) {
- Log.e(TAG, "attachMediaChannel()", e);
- }
- }
-
- private void reattachMediaChannel() {
- if (remoteMediaPlayer != null && mApiClient != null) {
- try {
- Log.d(TAG, "Registering MediaChannel namespace");
- Cast.CastApi.setMessageReceivedCallbacks(mApiClient,
- remoteMediaPlayer.getNamespace(), remoteMediaPlayer);
- } catch (IOException | IllegalStateException e) {
- Log.e(TAG, "reattachMediaChannel()", e);
- }
- }
- }
-
- private void detachMediaChannel() {
- Log.d(TAG, "trying to detach media channel");
- if (remoteMediaPlayer != null) {
- try {
- Cast.CastApi.removeMessageReceivedCallbacks(mApiClient,
- remoteMediaPlayer.getNamespace());
- } catch (IOException | IllegalStateException e) {
- Log.e(TAG, "detachMediaChannel()", e);
- }
- remoteMediaPlayer = null;
- }
- }
-
- /**
- * Returns the latest retrieved value for the {@link MediaStatus}. This value is updated
- * whenever the onStatusUpdated callback is called.
- */
- public final MediaStatus getMediaStatus() {
- return mediaStatus;
- }
-
- /*
- * This is called by onStatusUpdated() of the RemoteMediaPlayer
- */
- private void onRemoteMediaPlayerStatusUpdated() {
- Log.d(TAG, "onRemoteMediaPlayerStatusUpdated() reached");
- if (mApiClient == null || remoteMediaPlayer == null) {
- Log.d(TAG, "mApiClient or remoteMediaPlayer is null, so will not proceed");
- return;
- }
- mediaStatus = remoteMediaPlayer.getMediaStatus();
- if (mediaStatus == null) {
- Log.d(TAG, "MediaStatus is null, so will not proceed");
- return;
- } else {
- List<MediaQueueItem> queueItems = mediaStatus.getQueueItems();
- if (queueItems != null) {
- int itemId = mediaStatus.getCurrentItemId();
- MediaQueueItem item = mediaStatus.getQueueItemById(itemId);
- int repeatMode = mediaStatus.getQueueRepeatMode();
- onQueueUpdated(queueItems, item, repeatMode, false);
- } else {
- onQueueUpdated(null, null, MediaStatus.REPEAT_MODE_REPEAT_OFF, false);
- }
- state = mediaStatus.getPlayerState();
- int idleReason = mediaStatus.getIdleReason();
-
- if (state == MediaStatus.PLAYER_STATE_PLAYING) {
- Log.d(TAG, "onRemoteMediaPlayerStatusUpdated(): Player status = playing");
- } else if (state == MediaStatus.PLAYER_STATE_PAUSED) {
- Log.d(TAG, "onRemoteMediaPlayerStatusUpdated(): Player status = paused");
- } else if (state == MediaStatus.PLAYER_STATE_IDLE) {
- Log.d(TAG, "onRemoteMediaPlayerStatusUpdated(): Player status = IDLE with reason: "
- + idleReason);
- if (idleReason == MediaStatus.IDLE_REASON_ERROR) {
- // something bad happened on the cast device
- Log.d(TAG, "onRemoteMediaPlayerStatusUpdated(): IDLE reason = ERROR");
- onFailed(R.string.cast_failed_receiver_player_error, NO_STATUS_CODE);
- }
- } else if (state == MediaStatus.PLAYER_STATE_BUFFERING) {
- Log.d(TAG, "onRemoteMediaPlayerStatusUpdated(): Player status = buffering");
- } else {
- Log.d(TAG, "onRemoteMediaPlayerStatusUpdated(): Player status = unknown");
- }
- }
- for (CastConsumer consumer : castConsumers) {
- consumer.onRemoteMediaPlayerStatusUpdated();
- }
- if (mediaStatus != null) {
- double volume = mediaStatus.getStreamVolume();
- boolean isMute = mediaStatus.isMute();
- for (CastConsumer consumer : castConsumers) {
- consumer.onStreamVolumeChanged(volume, isMute);
- }
- }
- }
-
- private void onRemoteMediaPreloadStatusUpdated() {
- MediaQueueItem item = null;
- mediaStatus = remoteMediaPlayer.getMediaStatus();
- if (mediaStatus != null) {
- item = mediaStatus.getQueueItemById(mediaStatus.getPreloadedItemId());
- }
- Log.d(TAG, "onRemoteMediaPreloadStatusUpdated() " + item);
- for (CastConsumer consumer : castConsumers) {
- consumer.onRemoteMediaPreloadStatusUpdated(item);
- }
- }
-
- /*
- * This is called by onQueueStatusUpdated() of RemoteMediaPlayer
- */
- private void onQueueUpdated(List<MediaQueueItem> queueItems, MediaQueueItem item,
- int repeatMode, boolean shuffle) {
- Log.d(TAG, "onQueueUpdated() reached");
- Log.d(TAG, String.format(Locale.US, "Queue Items size: %d, Item: %s, Repeat Mode: %d, Shuffle: %s",
- queueItems == null ? 0 : queueItems.size(), item, repeatMode, shuffle));
- for (CastConsumer consumer : castConsumers) {
- consumer.onMediaQueueUpdated(queueItems, item, repeatMode, shuffle);
- }
- }
-
- /*
- * This is called by onMetadataUpdated() of RemoteMediaPlayer
- */
- public void onRemoteMediaPlayerMetadataUpdated() {
- Log.d(TAG, "onRemoteMediaPlayerMetadataUpdated() reached");
- for (CastConsumer consumer : castConsumers) {
- consumer.onRemoteMediaPlayerMetadataUpdated();
- }
- }
-
- /**
- * Registers a {@link CastConsumer} interface with this class.
- * Registered listeners will be notified of changes to a variety of
- * lifecycle and media status changes through the callbacks that the interface provides.
- *
- * @see DefaultCastConsumer
- */
- public synchronized void addCastConsumer(CastConsumer listener) {
- if (listener != null) {
- addBaseCastConsumer(listener);
- castConsumers.add(listener);
- Log.d(TAG, "Successfully added the new CastConsumer listener " + listener);
- }
- }
-
- /**
- * Unregisters a {@link CastConsumer}.
- */
- public synchronized void removeCastConsumer(CastConsumer listener) {
- if (listener != null) {
- removeBaseCastConsumer(listener);
- castConsumers.remove(listener);
- }
- }
-
- @Override
- protected void onDeviceUnselected() {
- detachMediaChannel();
- //removeDataChannel();
- state = MediaStatus.PLAYER_STATE_IDLE;
- mediaStatus = null;
- }
-
- @Override
- protected Cast.CastOptions.Builder getCastOptionBuilder(CastDevice device) {
- Cast.CastOptions.Builder builder = new Cast.CastOptions.Builder(mSelectedCastDevice, new CastListener());
- if (isFeatureEnabled(CastConfiguration.FEATURE_DEBUGGING)) {
- builder.setVerboseLoggingEnabled(true);
- }
- return builder;
- }
-
- @Override
- public void onConnectionFailed(ConnectionResult result) {
- super.onConnectionFailed(result);
- state = MediaStatus.PLAYER_STATE_IDLE;
- mediaStatus = null;
- }
-
- @Override
- public void onDisconnected(boolean stopAppOnExit, boolean clearPersistedConnectionData,
- boolean setDefaultRoute) {
- super.onDisconnected(stopAppOnExit, clearPersistedConnectionData, setDefaultRoute);
- state = MediaStatus.PLAYER_STATE_IDLE;
- mediaStatus = null;
- }
-
- class CastListener extends Cast.Listener {
-
- /*
- * (non-Javadoc)
- * @see com.google.android.gms.cast.Cast.Listener#onApplicationDisconnected (int)
- */
- @Override
- public void onApplicationDisconnected(int statusCode) {
- CastManager.this.onApplicationDisconnected(statusCode);
- }
-
- /*
- * (non-Javadoc)
- * @see com.google.android.gms.cast.Cast.Listener#onApplicationStatusChanged ()
- */
- @Override
- public void onApplicationStatusChanged() {
- CastManager.this.onApplicationStatusChanged();
- }
-
- @Override
- public void onVolumeChanged() {
- CastManager.this.onDeviceVolumeChanged();
- }
- }
-
- @Override
- public void onFailed(int resourceId, int statusCode) {
- Log.d(TAG, "onFailed: " + mContext.getString(resourceId) + ", code: " + statusCode);
- super.onFailed(resourceId, statusCode);
- }
-
- /**
- * Checks whether the selected Cast Device has the specified audio or video capabilities.
- *
- * @param capability capability from:
- * <ul>
- * <li>{@link CastDevice#CAPABILITY_AUDIO_IN}</li>
- * <li>{@link CastDevice#CAPABILITY_AUDIO_OUT}</li>
- * <li>{@link CastDevice#CAPABILITY_VIDEO_IN}</li>
- * <li>{@link CastDevice#CAPABILITY_VIDEO_OUT}</li>
- * </ul>
- * @param defaultVal value to return whenever there's no device selected.
- * @return {@code true} if the selected device has the specified capability,
- * {@code false} otherwise.
- */
- public boolean hasCapability(final int capability, final boolean defaultVal) {
- if (mSelectedCastDevice != null) {
- return mSelectedCastDevice.hasCapability(capability);
- } else {
- return defaultVal;
- }
- }
-
- /**
- * Adds and wires up the Switchable Media Router cast button. It returns a reference to the
- * {@link SwitchableMediaRouteActionProvider} associated with the button if the caller needs
- * such reference. It is assumed that the enclosing
- * {@link android.app.Activity} inherits (directly or indirectly) from
- * {@link androidx.appcompat.app.AppCompatActivity}.
- *
- * @param menuItem MenuItem of the Media Router cast button.
- */
- public final SwitchableMediaRouteActionProvider addMediaRouterButton(@NonNull MenuItem menuItem) {
- ActionProvider actionProvider = MenuItemCompat.getActionProvider(menuItem);
- if (!(actionProvider instanceof SwitchableMediaRouteActionProvider)) {
- Log.wtf(TAG, "MenuItem provided to addMediaRouterButton() is not compatible with " +
- "SwitchableMediaRouteActionProvider." +
- ((actionProvider == null) ? " Its action provider is null!" : ""),
- new ClassCastException());
- return null;
- }
- SwitchableMediaRouteActionProvider mediaRouteActionProvider =
- (SwitchableMediaRouteActionProvider) actionProvider;
- mediaRouteActionProvider.setRouteSelector(mMediaRouteSelector);
- if (mCastConfiguration.getMediaRouteDialogFactory() != null) {
- mediaRouteActionProvider.setDialogFactory(mCastConfiguration.getMediaRouteDialogFactory());
- }
- return mediaRouteActionProvider;
- }
-
- /* (non-Javadoc)
- * These methods startReconnectionService and stopReconnectionService simply override the ones
- * from BaseCastManager with empty implementations because we handle the service ourselves, but
- * need to allow BaseCastManager to save current network information.
- */
- @Override
- protected void startReconnectionService(long mediaDurationLeft) {
- // Do nothing
- }
-
- @Override
- protected void stopReconnectionService() {
- // Do nothing
- }
-}
diff --git a/core/src/play/java/de/danoeh/antennapod/core/cast/CastUtils.java b/core/src/play/java/de/danoeh/antennapod/core/cast/CastUtils.java
deleted file mode 100644
index e1f52aa9f..000000000
--- a/core/src/play/java/de/danoeh/antennapod/core/cast/CastUtils.java
+++ /dev/null
@@ -1,303 +0,0 @@
-package de.danoeh.antennapod.core.cast;
-
-import android.content.ContentResolver;
-import android.net.Uri;
-import android.text.TextUtils;
-import android.util.Log;
-
-import com.google.android.gms.cast.CastDevice;
-import com.google.android.gms.cast.MediaInfo;
-import com.google.android.gms.cast.MediaMetadata;
-import com.google.android.gms.common.images.WebImage;
-
-import java.util.Calendar;
-import java.util.List;
-
-import de.danoeh.antennapod.model.feed.Feed;
-import de.danoeh.antennapod.model.feed.FeedItem;
-import de.danoeh.antennapod.model.feed.FeedMedia;
-import de.danoeh.antennapod.model.playback.Playable;
-import de.danoeh.antennapod.model.playback.RemoteMedia;
-import de.danoeh.antennapod.core.storage.DBReader;
-
-/**
- * Helper functions for Cast support.
- */
-public class CastUtils {
- private CastUtils(){}
-
- private static final String TAG = "CastUtils";
-
- public static final String KEY_MEDIA_ID = "de.danoeh.antennapod.core.cast.MediaId";
-
- public static final String KEY_EPISODE_IDENTIFIER = "de.danoeh.antennapod.core.cast.EpisodeId";
- public static final String KEY_EPISODE_LINK = "de.danoeh.antennapod.core.cast.EpisodeLink";
- public static final String KEY_FEED_URL = "de.danoeh.antennapod.core.cast.FeedUrl";
- public static final String KEY_FEED_WEBSITE = "de.danoeh.antennapod.core.cast.FeedWebsite";
- public static final String KEY_EPISODE_NOTES = "de.danoeh.antennapod.core.cast.EpisodeNotes";
-
- /**
- * The field <code>AntennaPod.FormatVersion</code> specifies which version of MediaMetaData
- * fields we're using. Future implementations should try to be backwards compatible with earlier
- * versions, and earlier versions should be forward compatible until the version indicated by
- * <code>MAX_VERSION_FORWARD_COMPATIBILITY</code>. If an update makes the format unreadable for
- * an earlier version, then its version number should be greater than the
- * <code>MAX_VERSION_FORWARD_COMPATIBILITY</code> value set on the earlier one, so that it
- * doesn't try to parse the object.
- */
- public static final String KEY_FORMAT_VERSION = "de.danoeh.antennapod.core.cast.FormatVersion";
- public static final int FORMAT_VERSION_VALUE = 1;
- public static final int MAX_VERSION_FORWARD_COMPATIBILITY = 9999;
-
- public static boolean isCastable(Playable media) {
- if (media == null) {
- return false;
- }
- if (media instanceof FeedMedia || media instanceof RemoteMedia) {
- String url = media.getStreamUrl();
- if (url == null || url.isEmpty()) {
- return false;
- }
- if (url.startsWith(ContentResolver.SCHEME_CONTENT)) {
- return false; // Local feed
- }
- switch (media.getMediaType()) {
- case UNKNOWN:
- return false;
- case AUDIO:
- return CastManager.getInstance().hasCapability(CastDevice.CAPABILITY_AUDIO_OUT, true);
- case VIDEO:
- return CastManager.getInstance().hasCapability(CastDevice.CAPABILITY_VIDEO_OUT, true);
- }
- }
- return false;
- }
-
- /**
- * Converts {@link FeedMedia} objects into a format suitable for sending to a Cast Device.
- * Before using this method, one should make sure {@link #isCastable(Playable)} returns
- * {@code true}. This method should not run on the main thread.
- *
- * @param media The {@link FeedMedia} object to be converted.
- * @return {@link MediaInfo} object in a format proper for casting.
- */
- public static MediaInfo convertFromFeedMedia(FeedMedia media){
- if (media == null) {
- return null;
- }
- MediaMetadata metadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_GENERIC);
- if (media.getItem() == null) {
- media.setItem(DBReader.getFeedItem(media.getItemId()));
- }
- FeedItem feedItem = media.getItem();
- if (feedItem != null) {
- metadata.putString(MediaMetadata.KEY_TITLE, media.getEpisodeTitle());
- String subtitle = media.getFeedTitle();
- if (subtitle != null) {
- metadata.putString(MediaMetadata.KEY_SUBTITLE, subtitle);
- }
-
- if (!TextUtils.isEmpty(feedItem.getImageLocation())) {
- metadata.addImage(new WebImage(Uri.parse(feedItem.getImageLocation())));
- }
- Calendar calendar = Calendar.getInstance();
- calendar.setTime(media.getItem().getPubDate());
- metadata.putDate(MediaMetadata.KEY_RELEASE_DATE, calendar);
- Feed feed = feedItem.getFeed();
- if (feed != null) {
- if (!TextUtils.isEmpty(feed.getAuthor())) {
- metadata.putString(MediaMetadata.KEY_ARTIST, feed.getAuthor());
- }
- if (!TextUtils.isEmpty(feed.getDownload_url())) {
- metadata.putString(KEY_FEED_URL, feed.getDownload_url());
- }
- if (!TextUtils.isEmpty(feed.getLink())) {
- metadata.putString(KEY_FEED_WEBSITE, feed.getLink());
- }
- }
- if (!TextUtils.isEmpty(feedItem.getItemIdentifier())) {
- metadata.putString(KEY_EPISODE_IDENTIFIER, feedItem.getItemIdentifier());
- } else {
- metadata.putString(KEY_EPISODE_IDENTIFIER, media.getStreamUrl());
- }
- if (!TextUtils.isEmpty(feedItem.getLink())) {
- metadata.putString(KEY_EPISODE_LINK, feedItem.getLink());
- }
- try {
- DBReader.loadDescriptionOfFeedItem(feedItem);
- metadata.putString(KEY_EPISODE_NOTES, feedItem.getDescription());
- } catch (Exception e) {
- Log.e(TAG, "Unable to load FeedMedia notes", e);
- }
- }
- // This field only identifies the id on the device that has the original version.
- // Idea is to perhaps, on a first approach, check if the version on the local DB with the
- // same id matches the remote object, and if not then search for episode and feed identifiers.
- // This at least should make media recognition for a single device much quicker.
- metadata.putInt(KEY_MEDIA_ID, ((Long) media.getIdentifier()).intValue());
- // A way to identify different casting media formats in case we change it in the future and
- // senders with different versions share a casting device.
- metadata.putInt(KEY_FORMAT_VERSION, FORMAT_VERSION_VALUE);
-
- MediaInfo.Builder builder = new MediaInfo.Builder(media.getStreamUrl())
- .setContentType(media.getMime_type())
- .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
- .setMetadata(metadata);
- if (media.getDuration() > 0) {
- builder.setStreamDuration(media.getDuration());
- }
- return builder.build();
- }
-
- //TODO make unit tests for all the conversion methods
- /**
- * Converts {@link MediaInfo} objects into the appropriate implementation of {@link Playable}.
- *
- * Unless <code>searchFeedMedia</code> is set to <code>false</code>, this method should not run
- * on the GUI thread.
- *
- * @param media The {@link MediaInfo} object to be converted.
- * @param searchFeedMedia If set to <code>true</code>, the database will be queried to find a
- * {@link FeedMedia} instance that matches {@param media}.
- * @return {@link Playable} object in a format proper for casting.
- */
- public static Playable getPlayable(MediaInfo media, boolean searchFeedMedia) {
- Log.d(TAG, "getPlayable called with searchFeedMedia=" + searchFeedMedia);
- if (media == null) {
- Log.d(TAG, "MediaInfo object provided is null, not converting to any Playable instance");
- return null;
- }
- MediaMetadata metadata = media.getMetadata();
- int version = metadata.getInt(KEY_FORMAT_VERSION);
- if (version <= 0 || version > MAX_VERSION_FORWARD_COMPATIBILITY) {
- Log.w(TAG, "MediaInfo object obtained from the cast device is not compatible with this" +
- "version of AntennaPod CastUtils, curVer=" + FORMAT_VERSION_VALUE +
- ", object version=" + version);
- return null;
- }
- Playable result = null;
- if (searchFeedMedia) {
- long mediaId = metadata.getInt(KEY_MEDIA_ID);
- if (mediaId > 0) {
- FeedMedia fMedia = DBReader.getFeedMedia(mediaId);
- if (fMedia != null) {
- if (matches(media, fMedia)) {
- result = fMedia;
- Log.d(TAG, "FeedMedia object obtained matches the MediaInfo provided. id=" + mediaId);
- } else {
- Log.d(TAG, "FeedMedia object obtained does NOT match the MediaInfo provided. id=" + mediaId);
- }
- } else {
- Log.d(TAG, "Unable to find in database a FeedMedia with id=" + mediaId);
- }
- }
- if (result == null) {
- FeedItem feedItem = DBReader.getFeedItemByGuidOrEpisodeUrl(null,
- metadata.getString(KEY_EPISODE_IDENTIFIER));
- if (feedItem != null) {
- result = feedItem.getMedia();
- Log.d(TAG, "Found episode that matches the MediaInfo provided. Using its media, if existing.");
- }
- }
- }
- if (result == null) {
- List<WebImage> imageList = metadata.getImages();
- String imageUrl = null;
- if (!imageList.isEmpty()) {
- imageUrl = imageList.get(0).getUrl().toString();
- }
- String notes = metadata.getString(KEY_EPISODE_NOTES);
- result = new RemoteMedia(media.getContentId(),
- metadata.getString(KEY_EPISODE_IDENTIFIER),
- metadata.getString(KEY_FEED_URL),
- metadata.getString(MediaMetadata.KEY_SUBTITLE),
- metadata.getString(MediaMetadata.KEY_TITLE),
- metadata.getString(KEY_EPISODE_LINK),
- metadata.getString(MediaMetadata.KEY_ARTIST),
- imageUrl,
- metadata.getString(KEY_FEED_WEBSITE),
- media.getContentType(),
- metadata.getDate(MediaMetadata.KEY_RELEASE_DATE).getTime(),
- notes);
- Log.d(TAG, "Converted MediaInfo into RemoteMedia");
- }
- if (result.getDuration() == 0 && media.getStreamDuration() > 0) {
- result.setDuration((int) media.getStreamDuration());
- }
- return result;
- }
-
- /**
- * Compares a {@link MediaInfo} instance with a {@link FeedMedia} one and evaluates whether they
- * represent the same podcast episode.
- *
- * @param info the {@link MediaInfo} object to be compared.
- * @param media the {@link FeedMedia} object to be compared.
- * @return <true>true</true> if there's a match, <code>false</code> otherwise.
- *
- * @see RemoteMedia#equals(Object)
- */
- public static boolean matches(MediaInfo info, FeedMedia media) {
- if (info == null || media == null) {
- return false;
- }
- if (!TextUtils.equals(info.getContentId(), media.getStreamUrl())) {
- return false;
- }
- MediaMetadata metadata = info.getMetadata();
- FeedItem fi = media.getItem();
- if (fi == null || metadata == null ||
- !TextUtils.equals(metadata.getString(KEY_EPISODE_IDENTIFIER), fi.getItemIdentifier())) {
- return false;
- }
- Feed feed = fi.getFeed();
- return feed != null && TextUtils.equals(metadata.getString(KEY_FEED_URL), feed.getDownload_url());
- }
-
- /**
- * Compares a {@link MediaInfo} instance with a {@link RemoteMedia} one and evaluates whether they
- * represent the same podcast episode.
- *
- * @param info the {@link MediaInfo} object to be compared.
- * @param media the {@link RemoteMedia} object to be compared.
- * @return <true>true</true> if there's a match, <code>false</code> otherwise.
- *
- * @see RemoteMedia#equals(Object)
- */
- public static boolean matches(MediaInfo info, RemoteMedia media) {
- if (info == null || media == null) {
- return false;
- }
- if (!TextUtils.equals(info.getContentId(), media.getStreamUrl())) {
- return false;
- }
- MediaMetadata metadata = info.getMetadata();
- return metadata != null &&
- TextUtils.equals(metadata.getString(KEY_EPISODE_IDENTIFIER), media.getEpisodeIdentifier()) &&
- TextUtils.equals(metadata.getString(KEY_FEED_URL), media.getFeedUrl());
- }
-
- /**
- * Compares a {@link MediaInfo} instance with a {@link Playable} and evaluates whether they
- * represent the same podcast episode. Useful every time we get a MediaInfo from the Cast Device
- * and want to avoid unnecessary conversions.
- *
- * @param info the {@link MediaInfo} object to be compared.
- * @param media the {@link Playable} object to be compared.
- * @return <true>true</true> if there's a match, <code>false</code> otherwise.
- *
- * @see RemoteMedia#equals(Object)
- */
- public static boolean matches(MediaInfo info, Playable media) {
- if (info == null || media == null) {
- return false;
- }
- if (media instanceof RemoteMedia) {
- return matches(info, (RemoteMedia) media);
- }
- return media instanceof FeedMedia && matches(info, (FeedMedia) media);
- }
-
-
- //TODO Queue handling perhaps
-}
diff --git a/core/src/play/java/de/danoeh/antennapod/core/cast/DefaultCastConsumer.java b/core/src/play/java/de/danoeh/antennapod/core/cast/DefaultCastConsumer.java
deleted file mode 100644
index fe4183d54..000000000
--- a/core/src/play/java/de/danoeh/antennapod/core/cast/DefaultCastConsumer.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package de.danoeh.antennapod.core.cast;
-
-import com.google.android.libraries.cast.companionlibrary.cast.callbacks.VideoCastConsumerImpl;
-
-public class DefaultCastConsumer extends VideoCastConsumerImpl implements CastConsumer {
- @Override
- public void onStreamVolumeChanged(double value, boolean isMute) {
- // no-op
- }
-}
diff --git a/core/src/play/java/de/danoeh/antennapod/core/cast/MediaInfoCreator.java b/core/src/play/java/de/danoeh/antennapod/core/cast/MediaInfoCreator.java
deleted file mode 100644
index 00011ef05..000000000
--- a/core/src/play/java/de/danoeh/antennapod/core/cast/MediaInfoCreator.java
+++ /dev/null
@@ -1,57 +0,0 @@
-package de.danoeh.antennapod.core.cast;
-
-import android.net.Uri;
-import android.text.TextUtils;
-import com.google.android.gms.cast.MediaInfo;
-import com.google.android.gms.cast.MediaMetadata;
-import com.google.android.gms.common.images.WebImage;
-import de.danoeh.antennapod.model.playback.RemoteMedia;
-import java.util.Calendar;
-
-public class MediaInfoCreator {
- public static MediaInfo from(RemoteMedia media) {
- MediaMetadata metadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_GENERIC);
-
- metadata.putString(MediaMetadata.KEY_TITLE, media.getEpisodeTitle());
- metadata.putString(MediaMetadata.KEY_SUBTITLE, media.getFeedTitle());
- if (!TextUtils.isEmpty(media.getImageLocation())) {
- metadata.addImage(new WebImage(Uri.parse(media.getImageLocation())));
- }
- Calendar calendar = Calendar.getInstance();
- calendar.setTime(media.getPubDate());
- metadata.putDate(MediaMetadata.KEY_RELEASE_DATE, calendar);
- if (!TextUtils.isEmpty(media.getFeedAuthor())) {
- metadata.putString(MediaMetadata.KEY_ARTIST, media.getFeedAuthor());
- }
- if (!TextUtils.isEmpty(media.getFeedUrl())) {
- metadata.putString(CastUtils.KEY_FEED_URL, media.getFeedUrl());
- }
- if (!TextUtils.isEmpty(media.getFeedLink())) {
- metadata.putString(CastUtils.KEY_FEED_WEBSITE, media.getFeedLink());
- }
- if (!TextUtils.isEmpty(media.getEpisodeIdentifier())) {
- metadata.putString(CastUtils.KEY_EPISODE_IDENTIFIER, media.getEpisodeIdentifier());
- } else {
- metadata.putString(CastUtils.KEY_EPISODE_IDENTIFIER, media.getDownloadUrl());
- }
- if (!TextUtils.isEmpty(media.getEpisodeLink())) {
- metadata.putString(CastUtils.KEY_EPISODE_LINK, media.getEpisodeLink());
- }
- String notes = media.getNotes();
- if (notes != null) {
- metadata.putString(CastUtils.KEY_EPISODE_NOTES, notes);
- }
- // Default id value
- metadata.putInt(CastUtils.KEY_MEDIA_ID, 0);
- metadata.putInt(CastUtils.KEY_FORMAT_VERSION, CastUtils.FORMAT_VERSION_VALUE);
-
- MediaInfo.Builder builder = new MediaInfo.Builder(media.getDownloadUrl())
- .setContentType(media.getMimeType())
- .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
- .setMetadata(metadata);
- if (media.getDuration() > 0) {
- builder.setStreamDuration(media.getDuration());
- }
- return builder.build();
- }
-}
diff --git a/core/src/play/java/de/danoeh/antennapod/core/cast/SwitchableMediaRouteActionProvider.java b/core/src/play/java/de/danoeh/antennapod/core/cast/SwitchableMediaRouteActionProvider.java
deleted file mode 100644
index 5a6a0aa2b..000000000
--- a/core/src/play/java/de/danoeh/antennapod/core/cast/SwitchableMediaRouteActionProvider.java
+++ /dev/null
@@ -1,106 +0,0 @@
-package de.danoeh.antennapod.core.cast;
-
-import android.app.Activity;
-import android.content.Context;
-import android.content.ContextWrapper;
-import androidx.fragment.app.FragmentActivity;
-import androidx.fragment.app.FragmentManager;
-import androidx.mediarouter.app.MediaRouteActionProvider;
-import androidx.mediarouter.app.MediaRouteChooserDialogFragment;
-import androidx.mediarouter.app.MediaRouteControllerDialogFragment;
-import androidx.mediarouter.media.MediaRouter;
-import android.util.Log;
-
-/**
- * <p>Action Provider that extends {@link MediaRouteActionProvider} and allows the client to
- * disable completely the button by calling {@link #setEnabled(boolean)}.</p>
- *
- * <p>It is disabled by default, so if a client wants to initially have it enabled it must call
- * <code>setEnabled(true)</code>.</p>
- */
-public class SwitchableMediaRouteActionProvider extends MediaRouteActionProvider {
- public static final String TAG = "SwitchblMediaRtActProv";
-
- private static final String CHOOSER_FRAGMENT_TAG =
- "android.support.v7.mediarouter:MediaRouteChooserDialogFragment";
- private static final String CONTROLLER_FRAGMENT_TAG =
- "android.support.v7.mediarouter:MediaRouteControllerDialogFragment";
- private boolean enabled;
-
- public SwitchableMediaRouteActionProvider(Context context) {
- super(context);
- enabled = false;
- }
-
- /**
- * <p>Sets whether the Media Router button should be allowed to become visible or not.</p>
- *
- * <p>It's invisible by default.</p>
- */
- public void setEnabled(boolean newVal) {
- enabled = newVal;
- refreshVisibility();
- }
-
- @Override
- public boolean isVisible() {
- return enabled && super.isVisible();
- }
-
- @Override
- public boolean onPerformDefaultAction() {
- if (!super.onPerformDefaultAction()) {
- // there is no button, but we should still show the dialog if it's the case.
- if (!isVisible()) {
- return false;
- }
- FragmentManager fm = getFragmentManager();
- if (fm == null) {
- return false;
- }
- MediaRouter.RouteInfo route = MediaRouter.getInstance(getContext()).getSelectedRoute();
- if (route.isDefault() || !route.matchesSelector(getRouteSelector())) {
- if (fm.findFragmentByTag(CHOOSER_FRAGMENT_TAG) != null) {
- Log.w(TAG, "showDialog(): Route chooser dialog already showing!");
- return false;
- }
- MediaRouteChooserDialogFragment f =
- getDialogFactory().onCreateChooserDialogFragment();
- f.setRouteSelector(getRouteSelector());
- f.show(fm, CHOOSER_FRAGMENT_TAG);
- } else {
- if (fm.findFragmentByTag(CONTROLLER_FRAGMENT_TAG) != null) {
- Log.w(TAG, "showDialog(): Route controller dialog already showing!");
- return false;
- }
- MediaRouteControllerDialogFragment f =
- getDialogFactory().onCreateControllerDialogFragment();
- f.show(fm, CONTROLLER_FRAGMENT_TAG);
- }
- return true;
-
- } else {
- return true;
- }
- }
-
- private FragmentManager getFragmentManager() {
- Activity activity = getActivity();
- if (activity instanceof FragmentActivity) {
- return ((FragmentActivity)activity).getSupportFragmentManager();
- }
- return null;
- }
-
- private Activity getActivity() {
- // Gross way of unwrapping the Activity so we can get the FragmentManager
- Context context = getContext();
- while (context instanceof ContextWrapper) {
- if (context instanceof Activity) {
- return (Activity)context;
- }
- context = ((ContextWrapper)context).getBaseContext();
- }
- return null;
- }
-}
diff --git a/core/src/play/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceFlavorHelper.java b/core/src/play/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceFlavorHelper.java
deleted file mode 100644
index 41fd01441..000000000
--- a/core/src/play/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceFlavorHelper.java
+++ /dev/null
@@ -1,314 +0,0 @@
-package de.danoeh.antennapod.core.service.playback;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.net.NetworkInfo;
-import android.net.wifi.WifiManager;
-import android.os.Bundle;
-import androidx.annotation.NonNull;
-import androidx.annotation.StringRes;
-import android.support.v4.media.session.MediaSessionCompat;
-import android.support.v4.media.session.PlaybackStateCompat;
-import androidx.mediarouter.media.MediaRouter;
-import android.support.wearable.media.MediaControlConstants;
-import android.util.Log;
-import android.widget.Toast;
-
-import com.google.android.gms.cast.ApplicationMetadata;
-import com.google.android.libraries.cast.companionlibrary.cast.BaseCastManager;
-
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-import de.danoeh.antennapod.core.cast.CastConsumer;
-import de.danoeh.antennapod.core.cast.CastManager;
-import de.danoeh.antennapod.core.cast.DefaultCastConsumer;
-import de.danoeh.antennapod.event.MessageEvent;
-import de.danoeh.antennapod.model.playback.MediaType;
-import de.danoeh.antennapod.core.preferences.UserPreferences;
-import de.danoeh.antennapod.core.util.NetworkUtils;
-import org.greenrobot.eventbus.EventBus;
-
-/**
- * Class intended to work along PlaybackService and provide support for different flavors.
- */
-public class PlaybackServiceFlavorHelper {
- public static final String TAG = "PlaybackSrvFlavorHelper";
-
- /**
- * Time in seconds during which the CastManager will try to reconnect to the Cast Device after
- * the Wifi Connection is regained.
- */
- private static final int RECONNECTION_ATTEMPT_PERIOD_S = 15;
- /**
- * Stores the state of the cast playback just before it disconnects.
- */
- private volatile PlaybackServiceMediaPlayer.PSMPInfo infoBeforeCastDisconnection;
-
- private boolean wifiConnectivity = true;
- private BroadcastReceiver wifiBroadcastReceiver;
-
- private CastManager castManager;
- private MediaRouter mediaRouter;
- private PlaybackService.FlavorHelperCallback callback;
- private CastConsumer castConsumer;
-
- PlaybackServiceFlavorHelper(Context context, PlaybackService.FlavorHelperCallback callback) {
- this.callback = callback;
- if (!CastManager.isInitialized()) {
- return;
- }
- mediaRouter = MediaRouter.getInstance(context.getApplicationContext());
- setCastConsumer(context);
- }
-
- void initializeMediaPlayer(Context context) {
- if (!CastManager.isInitialized()) {
- callback.setMediaPlayer(new LocalPSMP(context, callback.getMediaPlayerCallback()));
- return;
- }
- castManager = CastManager.getInstance();
- castManager.addCastConsumer(castConsumer);
- boolean isCasting = castManager.isConnected();
- callback.setIsCasting(isCasting);
- if (isCasting) {
- if (UserPreferences.isCastEnabled()) {
- onCastAppConnected(context, false);
- } else {
- castManager.disconnect();
- }
- } else {
- callback.setMediaPlayer(new LocalPSMP(context, callback.getMediaPlayerCallback()));
- }
- }
-
- void removeCastConsumer() {
- if (!CastManager.isInitialized()) {
- return;
- }
- castManager.removeCastConsumer(castConsumer);
- }
-
- boolean castDisconnect(boolean castDisconnect) {
- if (!CastManager.isInitialized()) {
- return false;
- }
- if (castDisconnect) {
- castManager.disconnect();
- }
- return castDisconnect;
- }
-
- boolean onMediaPlayerInfo(Context context, int code, @StringRes int resourceId) {
- if (!CastManager.isInitialized()) {
- return false;
- }
- switch (code) {
- case RemotePSMP.CAST_ERROR:
- EventBus.getDefault().post(new MessageEvent(context.getString(resourceId)));
- return true;
- case RemotePSMP.CAST_ERROR_PRIORITY_HIGH:
- Toast.makeText(context, resourceId, Toast.LENGTH_SHORT).show();
- return true;
- default:
- return false;
- }
- }
-
- private void setCastConsumer(Context context) {
- castConsumer = new DefaultCastConsumer() {
- @Override
- public void onApplicationConnected(ApplicationMetadata appMetadata, String sessionId, boolean wasLaunched) {
- onCastAppConnected(context, wasLaunched);
- }
-
- @Override
- public void onDisconnectionReason(int reason) {
- Log.d(TAG, "onDisconnectionReason() with code " + reason);
- // This is our final chance to update the underlying stream position
- // In onDisconnected(), the underlying CastPlayback#mVideoCastConsumer
- // is disconnected and hence we update our local value of stream position
- // to the latest position.
- PlaybackServiceMediaPlayer mediaPlayer = callback.getMediaPlayer();
- if (mediaPlayer != null) {
- callback.saveCurrentPosition(true, null, PlaybackServiceMediaPlayer.INVALID_TIME);
- infoBeforeCastDisconnection = mediaPlayer.getPSMPInfo();
- if (reason != BaseCastManager.DISCONNECT_REASON_EXPLICIT &&
- infoBeforeCastDisconnection.playerStatus == PlayerStatus.PLAYING) {
- // If it's NOT based on user action, we shouldn't automatically resume local playback
- infoBeforeCastDisconnection.playerStatus = PlayerStatus.PAUSED;
- }
- }
- }
-
- @Override
- public void onDisconnected() {
- Log.d(TAG, "onDisconnected()");
- callback.setIsCasting(false);
- PlaybackServiceMediaPlayer.PSMPInfo info = infoBeforeCastDisconnection;
- infoBeforeCastDisconnection = null;
- PlaybackServiceMediaPlayer mediaPlayer = callback.getMediaPlayer();
- if (info == null && mediaPlayer != null) {
- info = mediaPlayer.getPSMPInfo();
- }
- if (info == null) {
- info = new PlaybackServiceMediaPlayer.PSMPInfo(PlayerStatus.INDETERMINATE,
- PlayerStatus.STOPPED, null);
- }
- switchMediaPlayer(new LocalPSMP(context, callback.getMediaPlayerCallback()),
- info, true);
- if (info.playable != null) {
- callback.sendNotificationBroadcast(PlaybackService.NOTIFICATION_TYPE_RELOAD,
- info.playable.getMediaType() == MediaType.AUDIO ?
- PlaybackService.EXTRA_CODE_AUDIO : PlaybackService.EXTRA_CODE_VIDEO);
- } else {
- Log.d(TAG, "Cast session disconnected, but no current media");
- callback.sendNotificationBroadcast(PlaybackService.NOTIFICATION_TYPE_PLAYBACK_END, 0);
- }
- // hardware volume buttons control the local device volume
- mediaRouter.setMediaSessionCompat(null);
- unregisterWifiBroadcastReceiver();
- callback.setupNotification(false, info);
- }
- };
- }
-
- private void onCastAppConnected(Context context, boolean wasLaunched) {
- Log.d(TAG, "A cast device application was " + (wasLaunched ? "launched" : "joined"));
- callback.setIsCasting(true);
- PlaybackServiceMediaPlayer.PSMPInfo info = null;
- PlaybackServiceMediaPlayer mediaPlayer = callback.getMediaPlayer();
- if (mediaPlayer != null) {
- info = mediaPlayer.getPSMPInfo();
- if (info.playerStatus == PlayerStatus.PLAYING) {
- // could be pause, but this way we make sure the new player will get the correct position,
- // since pause runs asynchronously and we could be directing the new player to play even before
- // the old player gives us back the position.
- callback.saveCurrentPosition(true, null, PlaybackServiceMediaPlayer.INVALID_TIME);
- }
- }
- if (info == null) {
- info = new PlaybackServiceMediaPlayer.PSMPInfo(PlayerStatus.INDETERMINATE, PlayerStatus.STOPPED, null);
- }
- callback.sendNotificationBroadcast(PlaybackService.NOTIFICATION_TYPE_RELOAD,
- PlaybackService.EXTRA_CODE_CAST);
- RemotePSMP remotePSMP = new RemotePSMP(context, callback.getMediaPlayerCallback());
- switchMediaPlayer(remotePSMP, info, wasLaunched);
- remotePSMP.init();
- // hardware volume buttons control the remote device volume
- mediaRouter.setMediaSessionCompat(callback.getMediaSession());
- registerWifiBroadcastReceiver();
- callback.setupNotification(true, info);
- }
-
- private void switchMediaPlayer(@NonNull PlaybackServiceMediaPlayer newPlayer,
- @NonNull PlaybackServiceMediaPlayer.PSMPInfo info,
- boolean wasLaunched) {
- PlaybackServiceMediaPlayer mediaPlayer = callback.getMediaPlayer();
- if (mediaPlayer != null) {
- try {
- mediaPlayer.stopPlayback(false).get(2, TimeUnit.SECONDS);
- } catch (InterruptedException | ExecutionException | TimeoutException e) {
- Log.e(TAG, "There was a problem stopping playback while switching media players", e);
- }
- mediaPlayer.shutdownQuietly();
- }
- mediaPlayer = newPlayer;
- callback.setMediaPlayer(mediaPlayer);
- Log.d(TAG, "switched to " + mediaPlayer.getClass().getSimpleName());
- if (!wasLaunched) {
- PlaybackServiceMediaPlayer.PSMPInfo candidate = mediaPlayer.getPSMPInfo();
- if (candidate.playable != null &&
- candidate.playerStatus.isAtLeast(PlayerStatus.PREPARING)) {
- // do not automatically send new media to cast device
- info.playable = null;
- }
- }
- if (info.playable != null) {
- mediaPlayer.playMediaObject(info.playable,
- !info.playable.localFileAvailable(),
- info.playerStatus == PlayerStatus.PLAYING,
- info.playerStatus.isAtLeast(PlayerStatus.PREPARING));
- }
- }
-
- void registerWifiBroadcastReceiver() {
- if (!CastManager.isInitialized()) {
- return;
- }
- if (wifiBroadcastReceiver != null) {
- return;
- }
- wifiBroadcastReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- if (intent.getAction().equals(WifiManager.NETWORK_STATE_CHANGED_ACTION)) {
- NetworkInfo info = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO);
- boolean isConnected = info.isConnected();
- //apparently this method gets called twice when a change happens, but one run is enough.
- if (isConnected && !wifiConnectivity) {
- wifiConnectivity = true;
- castManager.startCastDiscovery();
- castManager.reconnectSessionIfPossible(RECONNECTION_ATTEMPT_PERIOD_S, NetworkUtils.getWifiSsid());
- } else {
- wifiConnectivity = isConnected;
- }
- }
- }
- };
- callback.registerReceiver(wifiBroadcastReceiver,
- new IntentFilter(WifiManager.NETWORK_STATE_CHANGED_ACTION));
- }
-
- void unregisterWifiBroadcastReceiver() {
- if (!CastManager.isInitialized()) {
- return;
- }
- if (wifiBroadcastReceiver != null) {
- callback.unregisterReceiver(wifiBroadcastReceiver);
- wifiBroadcastReceiver = null;
- }
- }
-
- boolean onSharedPreference(String key) {
- if (!CastManager.isInitialized()) {
- return false;
- }
- if (UserPreferences.PREF_CAST_ENABLED.equals(key)) {
- if (!UserPreferences.isCastEnabled()) {
- if (castManager.isConnecting() || castManager.isConnected()) {
- Log.d(TAG, "Disconnecting cast device due to a change in user preferences");
- castManager.disconnect();
- }
- }
- return true;
- }
- return false;
- }
-
- void sessionStateAddActionForWear(PlaybackStateCompat.Builder sessionState, String actionName, CharSequence name, int icon) {
- if (!CastManager.isInitialized()) {
- return;
- }
- PlaybackStateCompat.CustomAction.Builder actionBuilder =
- new PlaybackStateCompat.CustomAction.Builder(actionName, name, icon);
- Bundle actionExtras = new Bundle();
- actionExtras.putBoolean(MediaControlConstants.EXTRA_CUSTOM_ACTION_SHOW_ON_WEAR, true);
- actionBuilder.setExtras(actionExtras);
-
- sessionState.addCustomAction(actionBuilder.build());
- }
-
- void mediaSessionSetExtraForWear(MediaSessionCompat mediaSession) {
- if (!CastManager.isInitialized()) {
- return;
- }
- Bundle sessionExtras = new Bundle();
- sessionExtras.putBoolean(MediaControlConstants.EXTRA_RESERVE_SLOT_SKIP_TO_PREVIOUS, true);
- sessionExtras.putBoolean(MediaControlConstants.EXTRA_RESERVE_SLOT_SKIP_TO_NEXT, true);
- mediaSession.setExtras(sessionExtras);
- }
-}
diff --git a/core/src/play/java/de/danoeh/antennapod/core/service/playback/RemotePSMP.java b/core/src/play/java/de/danoeh/antennapod/core/service/playback/RemotePSMP.java
deleted file mode 100644
index 38b469e8e..000000000
--- a/core/src/play/java/de/danoeh/antennapod/core/service/playback/RemotePSMP.java
+++ /dev/null
@@ -1,680 +0,0 @@
-package de.danoeh.antennapod.core.service.playback;
-
-import android.content.Context;
-import android.media.MediaPlayer;
-import androidx.annotation.NonNull;
-import android.util.Log;
-import android.util.Pair;
-import android.view.SurfaceHolder;
-
-import com.google.android.gms.cast.Cast;
-import com.google.android.gms.cast.CastStatusCodes;
-import com.google.android.gms.cast.MediaInfo;
-import com.google.android.gms.cast.MediaStatus;
-import com.google.android.libraries.cast.companionlibrary.cast.exceptions.CastException;
-import com.google.android.libraries.cast.companionlibrary.cast.exceptions.NoConnectionException;
-import com.google.android.libraries.cast.companionlibrary.cast.exceptions.TransientNetworkDisconnectionException;
-
-import de.danoeh.antennapod.core.cast.MediaInfoCreator;
-
-import java.util.Collections;
-import java.util.List;
-import java.util.concurrent.Future;
-import java.util.concurrent.FutureTask;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-import de.danoeh.antennapod.core.R;
-import de.danoeh.antennapod.core.cast.CastConsumer;
-import de.danoeh.antennapod.core.cast.CastManager;
-import de.danoeh.antennapod.core.cast.CastUtils;
-import de.danoeh.antennapod.core.cast.DefaultCastConsumer;
-import de.danoeh.antennapod.core.storage.DBReader;
-import de.danoeh.antennapod.model.playback.RemoteMedia;
-import de.danoeh.antennapod.model.feed.FeedMedia;
-import de.danoeh.antennapod.model.playback.MediaType;
-import de.danoeh.antennapod.core.util.RewindAfterPauseUtils;
-import de.danoeh.antennapod.model.playback.Playable;
-
-/**
- * Implementation of PlaybackServiceMediaPlayer suitable for remote playback on Cast Devices.
- */
-public class RemotePSMP extends PlaybackServiceMediaPlayer {
-
- public static final String TAG = "RemotePSMP";
-
- public static final int CAST_ERROR = 3001;
-
- public static final int CAST_ERROR_PRIORITY_HIGH = 3005;
-
- private final CastManager castMgr;
-
- private volatile Playable media;
- private volatile MediaType mediaType;
- private volatile MediaInfo remoteMedia;
- private volatile int remoteState;
-
- private final AtomicBoolean isBuffering;
-
- private final AtomicBoolean startWhenPrepared;
-
- public RemotePSMP(@NonNull Context context, @NonNull PSMPCallback callback) {
- super(context, callback);
-
- castMgr = CastManager.getInstance();
- media = null;
- mediaType = null;
- startWhenPrepared = new AtomicBoolean(false);
- isBuffering = new AtomicBoolean(false);
- remoteState = MediaStatus.PLAYER_STATE_UNKNOWN;
- }
-
- public void init() {
- try {
- if (castMgr.isConnected() && castMgr.isRemoteMediaLoaded()) {
- onRemoteMediaPlayerStatusUpdated();
- }
- } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
- Log.e(TAG, "Unable to do initial check for loaded media", e);
- }
-
- castMgr.addCastConsumer(castConsumer);
- }
-
- private CastConsumer castConsumer = new DefaultCastConsumer() {
- @Override
- public void onRemoteMediaPlayerMetadataUpdated() {
- RemotePSMP.this.onRemoteMediaPlayerStatusUpdated();
- }
-
- @Override
- public void onRemoteMediaPlayerStatusUpdated() {
- RemotePSMP.this.onRemoteMediaPlayerStatusUpdated();
- }
-
- @Override
- public void onMediaLoadResult(int statusCode) {
- if (playerStatus == PlayerStatus.PREPARING) {
- if (statusCode == CastStatusCodes.SUCCESS) {
- setPlayerStatus(PlayerStatus.PREPARED, media);
- if (media.getDuration() == 0) {
- Log.d(TAG, "Setting duration of media");
- try {
- media.setDuration((int) castMgr.getMediaDuration());
- } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
- Log.e(TAG, "Unable to get remote media's duration");
- }
- }
- } else if (statusCode != CastStatusCodes.REPLACED){
- Log.d(TAG, "Remote media failed to load");
- setPlayerStatus(PlayerStatus.INITIALIZED, media);
- }
- } else {
- Log.d(TAG, "onMediaLoadResult called, but Player Status wasn't in preparing state, so we ignore the result");
- }
- }
-
- @Override
- public void onApplicationStatusChanged(String appStatus) {
- if (playerStatus != PlayerStatus.PLAYING) {
- Log.d(TAG, "onApplicationStatusChanged, but no media was playing");
- return;
- }
- boolean playbackEnded = false;
- try {
- int standbyState = castMgr.getApplicationStandbyState();
- Log.d(TAG, "standbyState: " + standbyState);
- playbackEnded = standbyState == Cast.STANDBY_STATE_YES;
- } catch (IllegalStateException e) {
- Log.d(TAG, "unable to get standbyState on onApplicationStatusChanged()");
- }
- if (playbackEnded) {
- // This is an unconventional thing to occur...
- Log.w(TAG, "Somehow, Chromecast went from playing directly to standby mode");
- endPlayback(false, false, true, true);
- }
- }
-
- @Override
- public void onFailed(int resourceId, int statusCode) {
- callback.onMediaPlayerInfo(CAST_ERROR, resourceId);
- }
- };
-
- private void setBuffering(boolean buffering) {
- if (buffering && isBuffering.compareAndSet(false, true)) {
- callback.onMediaPlayerInfo(MediaPlayer.MEDIA_INFO_BUFFERING_START, 0);
- } else if (!buffering && isBuffering.compareAndSet(true, false)) {
- callback.onMediaPlayerInfo(MediaPlayer.MEDIA_INFO_BUFFERING_END, 0);
- }
- }
-
- private Playable localVersion(MediaInfo info){
- if (info == null) {
- return null;
- }
- if (CastUtils.matches(info, media)) {
- return media;
- }
- return CastUtils.getPlayable(info, true);
- }
-
- private MediaInfo remoteVersion(Playable playable) {
- if (playable == null) {
- return null;
- }
- if (CastUtils.matches(remoteMedia, playable)) {
- return remoteMedia;
- }
- if (playable instanceof FeedMedia) {
- return CastUtils.convertFromFeedMedia((FeedMedia) playable);
- }
- if (playable instanceof RemoteMedia) {
- return MediaInfoCreator.from((RemoteMedia) playable);
- }
- return null;
- }
-
- private void onRemoteMediaPlayerStatusUpdated() {
- MediaStatus status = castMgr.getMediaStatus();
- if (status == null) {
- Log.d(TAG, "Received null MediaStatus");
- return;
- } else {
- Log.d(TAG, "Received remote status/media update. New state=" + status.getPlayerState());
- }
- int state = status.getPlayerState();
- int oldState = remoteState;
- remoteMedia = status.getMediaInfo();
- boolean mediaChanged = !CastUtils.matches(remoteMedia, media);
- boolean stateChanged = state != oldState;
- if (!mediaChanged && !stateChanged) {
- Log.d(TAG, "Both media and state haven't changed, so nothing to do");
- return;
- }
- Playable currentMedia = mediaChanged ? localVersion(remoteMedia) : media;
- Playable oldMedia = media;
- int position = (int) status.getStreamPosition();
- // check for incompatible states
- if ((state == MediaStatus.PLAYER_STATE_PLAYING || state == MediaStatus.PLAYER_STATE_PAUSED)
- && currentMedia == null) {
- Log.w(TAG, "RemoteMediaPlayer returned playing or pausing state, but with no media");
- state = MediaStatus.PLAYER_STATE_UNKNOWN;
- stateChanged = oldState != MediaStatus.PLAYER_STATE_UNKNOWN;
- }
-
- if (stateChanged) {
- remoteState = state;
- }
-
- if (mediaChanged && stateChanged && oldState == MediaStatus.PLAYER_STATE_PLAYING &&
- state != MediaStatus.PLAYER_STATE_IDLE) {
- callback.onPlaybackPause(null, INVALID_TIME);
- // We don't want setPlayerStatus to handle the onPlaybackPause callback
- setPlayerStatus(PlayerStatus.INDETERMINATE, currentMedia);
- }
-
- setBuffering(state == MediaStatus.PLAYER_STATE_BUFFERING);
-
- switch (state) {
- case MediaStatus.PLAYER_STATE_PLAYING:
- if (!stateChanged) {
- //These steps are necessary because they won't be performed by setPlayerStatus()
- if (position >= 0) {
- currentMedia.setPosition(position);
- }
- currentMedia.onPlaybackStart();
- }
- setPlayerStatus(PlayerStatus.PLAYING, currentMedia, position);
- break;
- case MediaStatus.PLAYER_STATE_PAUSED:
- setPlayerStatus(PlayerStatus.PAUSED, currentMedia, position);
- break;
- case MediaStatus.PLAYER_STATE_BUFFERING:
- setPlayerStatus((mediaChanged || playerStatus == PlayerStatus.PREPARING) ?
- PlayerStatus.PREPARING : PlayerStatus.SEEKING,
- currentMedia,
- currentMedia != null ? currentMedia.getPosition() : INVALID_TIME);
- break;
- case MediaStatus.PLAYER_STATE_IDLE:
- int reason = status.getIdleReason();
- switch (reason) {
- case MediaStatus.IDLE_REASON_CANCELED:
- // Essentially means stopped at the request of a user
- callback.onPlaybackEnded(null, true);
- setPlayerStatus(PlayerStatus.STOPPED, currentMedia);
- if (oldMedia != null) {
- if (position >= 0) {
- oldMedia.setPosition(position);
- }
- callback.onPostPlayback(oldMedia, false, false, false);
- }
- // onPlaybackEnded pretty much takes care of updating the UI
- return;
- case MediaStatus.IDLE_REASON_INTERRUPTED:
- // Means that a request to load a different media was sent
- // Not sure if currentMedia already reflects the to be loaded one
- if (mediaChanged && oldState == MediaStatus.PLAYER_STATE_PLAYING) {
- callback.onPlaybackPause(null, INVALID_TIME);
- setPlayerStatus(PlayerStatus.INDETERMINATE, currentMedia);
- }
- setPlayerStatus(PlayerStatus.PREPARING, currentMedia);
- break;
- case MediaStatus.IDLE_REASON_NONE:
- // This probably only happens when we connected but no command has been sent yet.
- setPlayerStatus(PlayerStatus.INITIALIZED, currentMedia);
- break;
- case MediaStatus.IDLE_REASON_FINISHED:
- // This is our onCompletionListener...
- if (mediaChanged && currentMedia != null) {
- media = currentMedia;
- }
- endPlayback(true, false, true, true);
- return;
- case MediaStatus.IDLE_REASON_ERROR:
- Log.w(TAG, "Got an error status from the Chromecast. Skipping, if possible, to the next episode...");
- callback.onMediaPlayerInfo(CAST_ERROR_PRIORITY_HIGH,
- R.string.cast_failed_media_error_skipping);
- endPlayback(false, false, true, true);
- return;
- }
- break;
- case MediaStatus.PLAYER_STATE_UNKNOWN:
- if (playerStatus != PlayerStatus.INDETERMINATE || media != currentMedia) {
- setPlayerStatus(PlayerStatus.INDETERMINATE, currentMedia);
- }
- break;
- default:
- Log.wtf(TAG, "Remote media state undetermined!");
- }
- if (mediaChanged) {
- callback.onMediaChanged(true);
- if (oldMedia != null) {
- callback.onPostPlayback(oldMedia, false, false, currentMedia != null);
- }
- }
- }
-
- @Override
- public void playMediaObject(@NonNull final Playable playable, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) {
- Log.d(TAG, "playMediaObject() called");
- playMediaObject(playable, false, stream, startWhenPrepared, prepareImmediately);
- }
-
- /**
- * Internal implementation of playMediaObject. This method has an additional parameter that allows the caller to force a media player reset even if
- * the given playable parameter is the same object as the currently playing media.
- *
- * @see #playMediaObject(Playable, boolean, boolean, boolean)
- */
- private void playMediaObject(@NonNull final Playable playable, final boolean forceReset, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) {
- if (!CastUtils.isCastable(playable)) {
- Log.d(TAG, "media provided is not compatible with cast device");
- callback.onMediaPlayerInfo(CAST_ERROR_PRIORITY_HIGH, R.string.cast_not_castable);
- Playable nextPlayable = playable;
- do {
- nextPlayable = callback.getNextInQueue(nextPlayable);
- } while (nextPlayable != null && !CastUtils.isCastable(nextPlayable));
- if (nextPlayable != null) {
- playMediaObject(nextPlayable, forceReset, stream, startWhenPrepared, prepareImmediately);
- }
- return;
- }
-
- if (media != null) {
- if (!forceReset && media.getIdentifier().equals(playable.getIdentifier())
- && playerStatus == PlayerStatus.PLAYING) {
- // episode is already playing -> ignore method call
- Log.d(TAG, "Method call to playMediaObject was ignored: media file already playing.");
- return;
- } else {
- // set temporarily to pause in order to update list with current position
- boolean isPlaying = playerStatus == PlayerStatus.PLAYING;
- int position = media.getPosition();
- try {
- isPlaying = castMgr.isRemoteMediaPlaying();
- position = (int) castMgr.getCurrentMediaPosition();
- } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
- Log.e(TAG, "Unable to determine whether media was playing, falling back to stored player status", e);
- }
- if (isPlaying) {
- callback.onPlaybackPause(media, position);
- }
- if (!media.getIdentifier().equals(playable.getIdentifier())) {
- final Playable oldMedia = media;
- callback.onPostPlayback(oldMedia, false, false, true);
- }
-
- setPlayerStatus(PlayerStatus.INDETERMINATE, null);
- }
- }
-
- this.media = playable;
- remoteMedia = remoteVersion(playable);
- this.mediaType = media.getMediaType();
- this.startWhenPrepared.set(startWhenPrepared);
- setPlayerStatus(PlayerStatus.INITIALIZING, media);
- if (media instanceof FeedMedia && ((FeedMedia) media).getItem() == null) {
- ((FeedMedia) media).setItem(DBReader.getFeedItem(((FeedMedia) media).getItemId()));
- }
- callback.onMediaChanged(true);
- setPlayerStatus(PlayerStatus.INITIALIZED, media);
- if (prepareImmediately) {
- prepare();
- }
- }
-
- @Override
- public void resume() {
- try {
- if (playerStatus == PlayerStatus.PREPARED && media.getPosition() > 0) {
- int newPosition = RewindAfterPauseUtils.calculatePositionWithRewind(
- media.getPosition(),
- media.getLastPlayedTime());
- castMgr.play(newPosition);
- } else {
- castMgr.play();
- }
- } catch (CastException | TransientNetworkDisconnectionException | NoConnectionException e) {
- Log.e(TAG, "Unable to resume remote playback", e);
- }
- }
-
- @Override
- public void pause(boolean abandonFocus, boolean reinit) {
- try {
- if (castMgr.isRemoteMediaPlaying()) {
- castMgr.pause();
- }
- } catch (CastException | TransientNetworkDisconnectionException | NoConnectionException e) {
- Log.e(TAG, "Unable to pause", e);
- }
- }
-
- @Override
- public void prepare() {
- if (playerStatus == PlayerStatus.INITIALIZED) {
- Log.d(TAG, "Preparing media player");
- setPlayerStatus(PlayerStatus.PREPARING, media);
- try {
- int position = media.getPosition();
- if (position > 0) {
- position = RewindAfterPauseUtils.calculatePositionWithRewind(
- position,
- media.getLastPlayedTime());
- }
- castMgr.loadMedia(remoteMedia, startWhenPrepared.get(), position);
- } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
- Log.e(TAG, "Error loading media", e);
- setPlayerStatus(PlayerStatus.INITIALIZED, media);
- }
- }
- }
-
- @Override
- public void reinit() {
- Log.d(TAG, "reinit() called");
- if (media != null) {
- playMediaObject(media, true, false, startWhenPrepared.get(), false);
- } else {
- Log.d(TAG, "Call to reinit was ignored: media was null");
- }
- }
-
- @Override
- public void seekTo(int t) {
- //TODO check other seek implementations and see if there's no issue with sending too many seek commands to the remote media player
- try {
- if (castMgr.isRemoteMediaLoaded()) {
- setPlayerStatus(PlayerStatus.SEEKING, media);
- castMgr.seek(t);
- } else if (media != null && playerStatus == PlayerStatus.INITIALIZED){
- media.setPosition(t);
- startWhenPrepared.set(false);
- prepare();
- }
- } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
- Log.e(TAG, "Unable to seek", e);
- }
- }
-
- @Override
- public void seekDelta(int d) {
- int position = getPosition();
- if (position != INVALID_TIME) {
- seekTo(position + d);
- } else {
- Log.e(TAG, "getPosition() returned INVALID_TIME in seekDelta");
- }
- }
-
- @Override
- public int getDuration() {
- int retVal = INVALID_TIME;
- boolean prepared;
- try {
- prepared = castMgr.isRemoteMediaLoaded();
- } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
- Log.e(TAG, "Unable to check if remote media is loaded", e);
- prepared = playerStatus.isAtLeast(PlayerStatus.PREPARED);
- }
- if (prepared) {
- try {
- retVal = (int) castMgr.getMediaDuration();
- } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
- Log.e(TAG, "Unable to determine remote media's duration", e);
- }
- }
- if(retVal == INVALID_TIME && media != null && media.getDuration() > 0) {
- retVal = media.getDuration();
- }
- Log.d(TAG, "getDuration() -> " + retVal);
- return retVal;
- }
-
- @Override
- public int getPosition() {
- int retVal = INVALID_TIME;
- boolean prepared;
- try {
- prepared = castMgr.isRemoteMediaLoaded();
- } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
- Log.e(TAG, "Unable to check if remote media is loaded", e);
- prepared = playerStatus.isAtLeast(PlayerStatus.PREPARED);
- }
- if (prepared) {
- try {
- retVal = (int) castMgr.getCurrentMediaPosition();
- } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
- Log.e(TAG, "Unable to determine remote media's position", e);
- }
- }
- if(retVal <= 0 && media != null && media.getPosition() >= 0) {
- retVal = media.getPosition();
- }
- Log.d(TAG, "getPosition() -> " + retVal);
- return retVal;
- }
-
- @Override
- public boolean isStartWhenPrepared() {
- return startWhenPrepared.get();
- }
-
- @Override
- public void setStartWhenPrepared(boolean startWhenPrepared) {
- this.startWhenPrepared.set(startWhenPrepared);
- }
-
- @Override
- public void setPlaybackParams(float speed, boolean skipSilence) {
- //Can be safely ignored as neither set speed not skipSilence is supported
- }
-
- @Override
- public float getPlaybackSpeed() {
- return 1;
- }
-
- @Override
- public void setVolume(float volumeLeft, float volumeRight) {
- Log.d(TAG, "Setting the Stream volume on Remote Media Player");
- double volume = (volumeLeft+volumeRight)/2;
- if (volume > 1.0) {
- volume = 1.0;
- }
- if (volume < 0.0) {
- volume = 0.0;
- }
- try {
- castMgr.setStreamVolume(volume);
- } catch (TransientNetworkDisconnectionException | NoConnectionException | CastException e) {
- Log.e(TAG, "Unable to set the volume", e);
- }
- }
-
- @Override
- public boolean canDownmix() {
- return false;
- }
-
- @Override
- public void setDownmix(boolean enable) {
- throw new UnsupportedOperationException("Setting downmix unsupported in Remote Media Player");
- }
-
- @Override
- public MediaType getCurrentMediaType() {
- return mediaType;
- }
-
- @Override
- public boolean isStreaming() {
- return true;
- }
-
- @Override
- public void shutdown() {
- castMgr.removeCastConsumer(castConsumer);
- }
-
- @Override
- public void shutdownQuietly() {
- shutdown();
- }
-
- @Override
- public void setVideoSurface(SurfaceHolder surface) {
- throw new UnsupportedOperationException("Setting Video Surface unsupported in Remote Media Player");
- }
-
- @Override
- public void resetVideoSurface() {
- Log.e(TAG, "Resetting Video Surface unsupported in Remote Media Player");
- }
-
- @Override
- public Pair<Integer, Integer> getVideoSize() {
- return null;
- }
-
- @Override
- public Playable getPlayable() {
- return media;
- }
-
- @Override
- protected void setPlayable(Playable playable) {
- if (playable != media) {
- media = playable;
- remoteMedia = remoteVersion(playable);
- }
- }
-
- @Override
- public List<String> getAudioTracks() {
- return Collections.emptyList();
- }
-
- public void setAudioTrack(int track) {
-
- }
-
- public int getSelectedAudioTrack() {
- return -1;
- }
-
- @Override
- protected Future<?> endPlayback(boolean hasEnded, boolean wasSkipped, boolean shouldContinue,
- boolean toStoppedState) {
- Log.d(TAG, "endPlayback() called");
- boolean isPlaying = playerStatus == PlayerStatus.PLAYING;
- if (playerStatus != PlayerStatus.INDETERMINATE) {
- setPlayerStatus(PlayerStatus.INDETERMINATE, media);
- }
- if (media != null && wasSkipped) {
- // current position only really matters when we skip
- int position = getPosition();
- if (position >= 0) {
- media.setPosition(position);
- }
- }
- final Playable currentMedia = media;
- Playable nextMedia = null;
- if (shouldContinue) {
- nextMedia = callback.getNextInQueue(currentMedia);
-
- boolean playNextEpisode = isPlaying && nextMedia != null;
- 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, true /*TODO for now we always stream*/, playNextEpisode, playNextEpisode);
- }
- }
- if (shouldContinue || toStoppedState) {
- boolean shouldPostProcess = true;
- if (nextMedia == null) {
- try {
- castMgr.stop();
- shouldPostProcess = false;
- } catch (CastException | TransientNetworkDisconnectionException | NoConnectionException e) {
- Log.e(TAG, "Unable to stop playback", e);
- callback.onPlaybackEnded(null, true);
- stop();
- }
- }
- if (shouldPostProcess) {
- // Otherwise we rely on the chromecast callback to tell us the playback has stopped.
- callback.onPostPlayback(currentMedia, hasEnded, wasSkipped, nextMedia != null);
- }
- } else if (isPlaying) {
- callback.onPlaybackPause(currentMedia,
- currentMedia != null ? currentMedia.getPosition() : INVALID_TIME);
- }
-
- FutureTask<?> future = new FutureTask<>(() -> {}, null);
- future.run();
- return future;
- }
-
- private void stop() {
- if (playerStatus == PlayerStatus.INDETERMINATE) {
- setPlayerStatus(PlayerStatus.STOPPED, null);
- } else {
- Log.d(TAG, "Ignored call to stop: Current player state is: " + playerStatus);
- }
- }
-
- @Override
- protected boolean shouldLockWifi() {
- return false;
- }
-}
diff --git a/core/src/play/java/de/danoeh/antennapod/core/service/playback/WearMediaSession.java b/core/src/play/java/de/danoeh/antennapod/core/service/playback/WearMediaSession.java
new file mode 100644
index 000000000..2167d9f2c
--- /dev/null
+++ b/core/src/play/java/de/danoeh/antennapod/core/service/playback/WearMediaSession.java
@@ -0,0 +1,28 @@
+package de.danoeh.antennapod.core.service.playback;
+
+import android.os.Bundle;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+import android.support.wearable.media.MediaControlConstants;
+
+public class WearMediaSession {
+ public static final String TAG = "WearMediaSession";
+
+ static void sessionStateAddActionForWear(PlaybackStateCompat.Builder sessionState, String actionName,
+ CharSequence name, int icon) {
+ PlaybackStateCompat.CustomAction.Builder actionBuilder =
+ new PlaybackStateCompat.CustomAction.Builder(actionName, name, icon);
+ Bundle actionExtras = new Bundle();
+ actionExtras.putBoolean(MediaControlConstants.EXTRA_CUSTOM_ACTION_SHOW_ON_WEAR, true);
+ actionBuilder.setExtras(actionExtras);
+
+ sessionState.addCustomAction(actionBuilder.build());
+ }
+
+ static void mediaSessionSetExtraForWear(MediaSessionCompat mediaSession) {
+ Bundle sessionExtras = new Bundle();
+ sessionExtras.putBoolean(MediaControlConstants.EXTRA_RESERVE_SLOT_SKIP_TO_PREVIOUS, true);
+ sessionExtras.putBoolean(MediaControlConstants.EXTRA_RESERVE_SLOT_SKIP_TO_NEXT, true);
+ mediaSession.setExtras(sessionExtras);
+ }
+}
diff --git a/core/src/play/res/values/strings.xml b/core/src/play/res/values/strings.xml
deleted file mode 100644
index 7307849d2..000000000
--- a/core/src/play/res/values/strings.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources>
- <string name="pref_cast_message" translatable="false">@string/pref_cast_message_play_flavor</string>
-</resources>
diff --git a/core/src/test/java/de/danoeh/antennapod/core/service/playback/PlaybackVolumeUpdaterTest.java b/core/src/test/java/de/danoeh/antennapod/core/service/playback/PlaybackVolumeUpdaterTest.java
index 4890c471a..92c0e8e3d 100644
--- a/core/src/test/java/de/danoeh/antennapod/core/service/playback/PlaybackVolumeUpdaterTest.java
+++ b/core/src/test/java/de/danoeh/antennapod/core/service/playback/PlaybackVolumeUpdaterTest.java
@@ -6,6 +6,8 @@ import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.model.feed.FeedPreferences;
import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting;
import de.danoeh.antennapod.model.playback.Playable;
+import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer;
+import de.danoeh.antennapod.playback.base.PlayerStatus;
import org.junit.Before;
import org.junit.Test;
diff --git a/core/src/test/java/de/danoeh/antennapod/core/util/RewindAfterPauseUtilTest.java b/core/src/test/java/de/danoeh/antennapod/core/util/RewindAfterPauseUtilTest.java
deleted file mode 100644
index dc64f6ae0..000000000
--- a/core/src/test/java/de/danoeh/antennapod/core/util/RewindAfterPauseUtilTest.java
+++ /dev/null
@@ -1,56 +0,0 @@
-package de.danoeh.antennapod.core.util;
-
-import org.junit.Test;
-
-import static org.junit.Assert.assertEquals;
-
-/**
- * Tests for {@link RewindAfterPauseUtils}.
- */
-public class RewindAfterPauseUtilTest {
-
- @Test
- public void testCalculatePositionWithRewindNoRewind() {
- final int ORIGINAL_POSITION = 10000;
- long lastPlayed = System.currentTimeMillis();
- int position = RewindAfterPauseUtils.calculatePositionWithRewind(ORIGINAL_POSITION, lastPlayed);
-
- assertEquals(ORIGINAL_POSITION, position);
- }
-
- @Test
- public void testCalculatePositionWithRewindSmallRewind() {
- final int ORIGINAL_POSITION = 10000;
- long lastPlayed = System.currentTimeMillis() - RewindAfterPauseUtils.ELAPSED_TIME_FOR_SHORT_REWIND - 1000;
- int position = RewindAfterPauseUtils.calculatePositionWithRewind(ORIGINAL_POSITION, lastPlayed);
-
- assertEquals(ORIGINAL_POSITION - RewindAfterPauseUtils.SHORT_REWIND, position);
- }
-
- @Test
- public void testCalculatePositionWithRewindMediumRewind() {
- final int ORIGINAL_POSITION = 10000;
- long lastPlayed = System.currentTimeMillis() - RewindAfterPauseUtils.ELAPSED_TIME_FOR_MEDIUM_REWIND - 1000;
- int position = RewindAfterPauseUtils.calculatePositionWithRewind(ORIGINAL_POSITION, lastPlayed);
-
- assertEquals(ORIGINAL_POSITION - RewindAfterPauseUtils.MEDIUM_REWIND, position);
- }
-
- @Test
- public void testCalculatePositionWithRewindLongRewind() {
- final int ORIGINAL_POSITION = 30000;
- long lastPlayed = System.currentTimeMillis() - RewindAfterPauseUtils.ELAPSED_TIME_FOR_LONG_REWIND - 1000;
- int position = RewindAfterPauseUtils.calculatePositionWithRewind(ORIGINAL_POSITION, lastPlayed);
-
- assertEquals(ORIGINAL_POSITION - RewindAfterPauseUtils.LONG_REWIND, position);
- }
-
- @Test
- public void testCalculatePositionWithRewindNegativeNumber() {
- final int ORIGINAL_POSITION = 100;
- long lastPlayed = System.currentTimeMillis() - RewindAfterPauseUtils.ELAPSED_TIME_FOR_LONG_REWIND - 1000;
- int position = RewindAfterPauseUtils.calculatePositionWithRewind(ORIGINAL_POSITION, lastPlayed);
-
- assertEquals(0, position);
- }
-}