diff --git a/app/build.gradle b/app/build.gradle
index 87dea29de..3dcf7fe1e 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -116,6 +116,8 @@ dependencies {
implementation project(':net:sync:gpoddernet')
implementation project(':net:sync:model')
implementation project(':parser:feed')
+ implementation project(':playback:base')
+ implementation project(':playback:cast')
implementation project(':ui:app-start-intent')
implementation project(':ui:common')
diff --git a/app/src/androidTest/java/de/test/antennapod/playback/PlaybackTest.java b/app/src/androidTest/java/de/test/antennapod/playback/PlaybackTest.java
index 9f7af3a16..2ab2361d7 100644
--- a/app/src/androidTest/java/de/test/antennapod/playback/PlaybackTest.java
+++ b/app/src/androidTest/java/de/test/antennapod/playback/PlaybackTest.java
@@ -11,6 +11,7 @@ import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.ActivityTestRule;
import de.danoeh.antennapod.model.feed.FeedItemFilter;
+import de.danoeh.antennapod.playback.base.PlayerStatus;
import org.awaitility.Awaitility;
import org.hamcrest.Matcher;
import org.junit.After;
@@ -32,7 +33,6 @@ import de.danoeh.antennapod.model.feed.FeedMedia;
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.PlayerStatus;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.util.IntentUtils;
diff --git a/app/src/androidTest/java/de/test/antennapod/service/playback/CancelablePSMPCallback.java b/app/src/androidTest/java/de/test/antennapod/service/playback/CancelablePSMPCallback.java
index 2c164f131..4d57b9b43 100644
--- a/app/src/androidTest/java/de/test/antennapod/service/playback/CancelablePSMPCallback.java
+++ b/app/src/androidTest/java/de/test/antennapod/service/playback/CancelablePSMPCallback.java
@@ -1,9 +1,10 @@
package de.test.antennapod.service.playback;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import de.danoeh.antennapod.model.playback.MediaType;
-import de.danoeh.antennapod.core.service.playback.PlaybackServiceMediaPlayer;
import de.danoeh.antennapod.model.playback.Playable;
+import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer;
public class CancelablePSMPCallback implements PlaybackServiceMediaPlayer.PSMPCallback {
@@ -42,14 +43,6 @@ public class CancelablePSMPCallback implements PlaybackServiceMediaPlayer.PSMPCa
originalCallback.onMediaChanged(reloadUI);
}
- @Override
- public boolean onMediaPlayerInfo(int code, int resourceId) {
- if (isCancelled) {
- return true;
- }
- return originalCallback.onMediaPlayerInfo(code, resourceId);
- }
-
@Override
public void onPostPlayback(@NonNull Playable media, boolean ended, boolean skipped, boolean playingNext) {
if (isCancelled) {
@@ -82,6 +75,15 @@ public class CancelablePSMPCallback implements PlaybackServiceMediaPlayer.PSMPCa
return originalCallback.getNextInQueue(currentMedia);
}
+ @Nullable
+ @Override
+ public Playable findMedia(@NonNull String url) {
+ if (isCancelled) {
+ return null;
+ }
+ return originalCallback.findMedia(url);
+ }
+
@Override
public void onPlaybackEnded(MediaType mediaType, boolean stopPlaying) {
if (isCancelled) {
@@ -89,4 +91,12 @@ public class CancelablePSMPCallback implements PlaybackServiceMediaPlayer.PSMPCa
}
originalCallback.onPlaybackEnded(mediaType, stopPlaying);
}
+
+ @Override
+ public void ensureMediaInfoLoaded(@NonNull Playable media) {
+ if (isCancelled) {
+ return;
+ }
+ originalCallback.ensureMediaInfoLoaded(media);
+ }
}
\ No newline at end of file
diff --git a/app/src/androidTest/java/de/test/antennapod/service/playback/DefaultPSMPCallback.java b/app/src/androidTest/java/de/test/antennapod/service/playback/DefaultPSMPCallback.java
index 090a94d6e..fb55c7ad0 100644
--- a/app/src/androidTest/java/de/test/antennapod/service/playback/DefaultPSMPCallback.java
+++ b/app/src/androidTest/java/de/test/antennapod/service/playback/DefaultPSMPCallback.java
@@ -1,54 +1,59 @@
package de.test.antennapod.service.playback;
import androidx.annotation.NonNull;
-import androidx.annotation.StringRes;
+import androidx.annotation.Nullable;
import de.danoeh.antennapod.model.playback.MediaType;
-import de.danoeh.antennapod.core.service.playback.PlaybackServiceMediaPlayer;
import de.danoeh.antennapod.model.playback.Playable;
+import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer;
public class DefaultPSMPCallback implements PlaybackServiceMediaPlayer.PSMPCallback {
- @Override
- public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) {
+ @Override
+ public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) {
- }
+ }
- @Override
- public void shouldStop() {
+ @Override
+ public void shouldStop() {
- }
+ }
- @Override
- public void onMediaChanged(boolean reloadUI) {
+ @Override
+ public void onMediaChanged(boolean reloadUI) {
- }
+ }
- @Override
- public boolean onMediaPlayerInfo(int code, @StringRes int resourceId) {
- return false;
- }
+ @Override
+ public void onPostPlayback(@NonNull Playable media, boolean ended, boolean skipped, boolean playingNext) {
- @Override
- public void onPostPlayback(@NonNull Playable media, boolean ended, boolean skipped, boolean playingNext) {
+ }
- }
+ @Override
+ public void onPlaybackStart(@NonNull Playable playable, int position) {
- @Override
- public void onPlaybackStart(@NonNull Playable playable, int position) {
+ }
- }
+ @Override
+ public void onPlaybackPause(Playable playable, int position) {
- @Override
- public void onPlaybackPause(Playable playable, int position) {
+ }
- }
+ @Override
+ public Playable getNextInQueue(Playable currentMedia) {
+ return null;
+ }
- @Override
- public Playable getNextInQueue(Playable currentMedia) {
- return null;
- }
+ @Nullable
+ @Override
+ public Playable findMedia(@NonNull String url) {
+ return null;
+ }
- @Override
- public void onPlaybackEnded(MediaType mediaType, boolean stopPlaying) {
+ @Override
+ public void onPlaybackEnded(MediaType mediaType, boolean stopPlaying) {
- }
- }
\ No newline at end of file
+ }
+
+ @Override
+ public void ensureMediaInfoLoaded(@NonNull Playable media) {
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java b/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java
index 87a5fa65c..32298200e 100644
--- a/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java
+++ b/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java
@@ -5,6 +5,8 @@ import android.content.Context;
import androidx.test.filters.MediumTest;
import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting;
+import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer;
+import de.danoeh.antennapod.playback.base.PlayerStatus;
import de.test.antennapod.EspressoTestUtils;
import junit.framework.AssertionFailedError;
@@ -24,8 +26,6 @@ import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.model.feed.FeedPreferences;
import de.danoeh.antennapod.core.service.playback.LocalPSMP;
-import de.danoeh.antennapod.core.service.playback.PlaybackServiceMediaPlayer;
-import de.danoeh.antennapod.core.service.playback.PlayerStatus;
import de.danoeh.antennapod.core.storage.PodDBAdapter;
import de.danoeh.antennapod.model.playback.Playable;
import de.test.antennapod.util.service.download.HTTPBin;
diff --git a/app/src/free/java/de/danoeh/antennapod/config/CastCallbackImpl.java b/app/src/free/java/de/danoeh/antennapod/config/CastCallbackImpl.java
deleted file mode 100644
index fb23dfa1a..000000000
--- a/app/src/free/java/de/danoeh/antennapod/config/CastCallbackImpl.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package de.danoeh.antennapod.config;
-
-import de.danoeh.antennapod.core.CastCallbacks;
-
-class CastCallbackImpl implements CastCallbacks {
-
-}
diff --git a/app/src/free/java/de/danoeh/antennapod/preferences/PreferenceControllerFlavorHelper.java b/app/src/free/java/de/danoeh/antennapod/preferences/PreferenceControllerFlavorHelper.java
deleted file mode 100644
index e096f883f..000000000
--- a/app/src/free/java/de/danoeh/antennapod/preferences/PreferenceControllerFlavorHelper.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package de.danoeh.antennapod.preferences;
-
-import de.danoeh.antennapod.core.preferences.UserPreferences;
-import de.danoeh.antennapod.fragment.preferences.PlaybackPreferencesFragment;
-
-/**
- * Implements functions from PreferenceController that are flavor dependent.
- */
-public class PreferenceControllerFlavorHelper {
-
- public static void setupFlavoredUI(PlaybackPreferencesFragment ui) {
- ui.findPreference(UserPreferences.PREF_CAST_ENABLED).setEnabled(false);
- }
-}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 45c21ce6b..0f8242e63 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -54,6 +54,9 @@
- * 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()}. - *
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
- * 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.
- *
- * 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 Action Provider that extends {@link MediaRouteActionProvider} and allows the client to
- * disable completely the button by calling {@link #setEnabled(boolean)}. It is disabled by default, so if a client wants to initially have it enabled it must call
- * Sets whether the Media Router button should be allowed to become visible or not. It's invisible by default.true
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 true
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 true
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 :-)
- Listtrue
, playback starts after load
- * @param position Where to start the playback (only used if autoPlay is true
.
- * 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 true
, playback starts after load
- * @param position Where to start the playback (only used if autoPlay is true
).
- * 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 true
, playback starts after load
- * @param position Where to start the playback (only used if autoPlay is true
).
- * 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.
- *
- *
- * @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 AntennaPod.FormatVersion
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
- * MAX_VERSION_FORWARD_COMPATIBILITY
. If an update makes the format unreadable for
- * an earlier version, then its version number should be greater than the
- * MAX_VERSION_FORWARD_COMPATIBILITY
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 searchFeedMedia
is set to false
, this method should not run
- * on the GUI thread.
- *
- * @param media The {@link MediaInfo} object to be converted.
- * @param searchFeedMedia If set to true
, 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) {
- Listfalse
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 false
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 false
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;
-
-/**
- * setEnabled(true)
.AntennaPod.FormatVersion
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
+ * MAX_VERSION_FORWARD_COMPATIBILITY
. If an update makes the format unreadable for
+ * an earlier version, then its version number should be greater than the
+ * MAX_VERSION_FORWARD_COMPATIBILITY
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, CastSession castSession) {
+ if (media == null || castSession == null || castSession.getCastDevice() == 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 AUDIO:
+ return castSession.getCastDevice().hasCapability(CastDevice.CAPABILITY_AUDIO_OUT);
+ case VIDEO:
+ return castSession.getCastDevice().hasCapability(CastDevice.CAPABILITY_VIDEO_OUT);
+ default:
+ return false;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Converts {@link MediaInfo} objects into the appropriate implementation of {@link Playable}.
+ * @return {@link Playable} object in a format proper for casting.
+ */
+ public static Playable makeRemoteMedia(MediaInfo media) {
+ 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;
+ }
+ Listfalse
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 false
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 false
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);
+ }
+}
diff --git a/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/MediaInfoCreator.java b/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/MediaInfoCreator.java
new file mode 100644
index 000000000..dd408d4a7
--- /dev/null
+++ b/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/MediaInfoCreator.java
@@ -0,0 +1,135 @@
+package de.danoeh.antennapod.playback.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.feed.Feed;
+import de.danoeh.antennapod.model.feed.FeedItem;
+import de.danoeh.antennapod.model.feed.FeedMedia;
+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);
+ metadata.putString(CastUtils.KEY_STREAM_URL, media.getStreamUrl());
+
+ 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();
+ }
+
+ /**
+ * Converts {@link FeedMedia} objects into a format suitable for sending to a Cast Device.
+ * Before using this method, one should make sure 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 from(FeedMedia media) {
+ if (media == null) {
+ return null;
+ }
+ MediaMetadata metadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_GENERIC);
+ if (media.getItem() == null) {
+ throw new IllegalStateException("item is 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);
+ }
+
+ // Manual because cast does not support embedded images
+ String url = feedItem.getImageUrl() == null ? feedItem.getFeed().getImageUrl() : feedItem.getImageUrl();
+ if (!TextUtils.isEmpty(url)) {
+ metadata.addImage(new WebImage(Uri.parse(url)));
+ }
+ 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(CastUtils.KEY_FEED_URL, feed.getDownload_url());
+ }
+ if (!TextUtils.isEmpty(feed.getLink())) {
+ metadata.putString(CastUtils.KEY_FEED_WEBSITE, feed.getLink());
+ }
+ }
+ if (!TextUtils.isEmpty(feedItem.getItemIdentifier())) {
+ metadata.putString(CastUtils.KEY_EPISODE_IDENTIFIER, feedItem.getItemIdentifier());
+ } else {
+ metadata.putString(CastUtils.KEY_EPISODE_IDENTIFIER, media.getStreamUrl());
+ }
+ if (!TextUtils.isEmpty(feedItem.getLink())) {
+ metadata.putString(CastUtils.KEY_EPISODE_LINK, feedItem.getLink());
+ }
+ }
+ // 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(CastUtils.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(CastUtils.KEY_FORMAT_VERSION, CastUtils.FORMAT_VERSION_VALUE);
+ metadata.putString(CastUtils.KEY_STREAM_URL, media.getStreamUrl());
+
+ 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();
+ }
+}
diff --git a/playback/cast/src/play/res/menu/cast_button.xml b/playback/cast/src/play/res/menu/cast_button.xml
new file mode 100644
index 000000000..6e65bce18
--- /dev/null
+++ b/playback/cast/src/play/res/menu/cast_button.xml
@@ -0,0 +1,11 @@
+
+
diff --git a/settings.gradle b/settings.gradle
index 80fce468f..c7f5e6449 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -10,6 +10,9 @@ include ':net:sync:model'
include ':parser:feed'
include ':parser:media'
+include ':playback:base'
+include ':playback:cast'
+
include ':ui:app-start-intent'
include ':ui:common'
include ':ui:png-icons'
diff --git a/ui/png-icons/src/main/res/drawable/ic_notification_cast_off.xml b/ui/png-icons/src/main/res/drawable/ic_notification_cast_off.xml
deleted file mode 100644
index 3e3accd0b..000000000
--- a/ui/png-icons/src/main/res/drawable/ic_notification_cast_off.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-